diff --git a/src/Template/TwigExtension.php b/src/Template/TwigExtension.php
index 1c3e3457f65da99027794a58b6bc3dbeba38edcc..50bf13e310d5a9cc8fd1aa9af1be4eefa5879c28 100644
--- a/src/Template/TwigExtension.php
+++ b/src/Template/TwigExtension.php
@@ -2,8 +2,11 @@
 
 namespace Drupal\components\Template;
 
+use ArrayAccess;
 use Exception;
+use Twig\Error\RuntimeError;
 use Twig\Extension\AbstractExtension;
+use Twig\TwigFilter;
 use Twig\TwigFunction;
 
 /**
@@ -20,6 +23,16 @@ class TwigExtension extends AbstractExtension {
     ];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getFilters() {
+    return [
+      'set' => new TwigFilter('set', ['Drupal\components\Template\TwigExtension', 'setFilter']),
+      'add' => new TwigFilter('add', ['Drupal\components\Template\TwigExtension', 'addFilter']),
+    ];
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -92,4 +105,96 @@ class TwigExtension extends AbstractExtension {
     return $render_array;
   }
 
+  /**
+   * Recursively merges an array into the element, replacing existing values.
+   *
+   * @code
+   * {{ form|set( {'element': {'attributes': {'placeholder': 'Label'}}} ) }}
+   * @endcode
+   *
+   * @param array|iterable|\Traversable $element
+   *   The parent renderable array to merge into.
+   * @param iterable|array $array
+   *   The array to merge.
+   *
+   * @return array
+   *   The merged renderable array.
+   *
+   * @throws \Twig\Error\RuntimeError
+   *   When $element is not an array or "Traversable".
+   */
+  public static function setFilter($element, $array) {
+    if (!twig_test_iterable($element)) {
+      throw new RuntimeError(sprintf('The set filter only works with arrays or "Traversable", got "%s" as first argument.', gettype($element)));
+    }
+
+    return array_replace_recursive($element, $array);
+  }
+
+  /**
+   * Adds a deeply-nested property on an array.
+   *
+   * If the deeply-nested property exists, the existing data will be replaced
+   * with the new value, unless the existing data is an array. In which case,
+   * the new value will be merged into the existing array.
+   *
+   * @code
+   * {{ form|add( 'element.attributes.class', 'new-class' ) }}
+   * @endcode
+   *
+   * @param array|iterable|\Traversable $element
+   *   The parent renderable array to merge into.
+   * @param string $path
+   *   The dotted-path to the deeply nested element to replace.
+   * @param mixed $value
+   *   The value to set.
+   *
+   * @return array
+   *   The merged renderable array.
+   *
+   * @throws \Twig\Error\RuntimeError
+   *   When $element is not an array or "Traversable".
+   */
+  public static function addFilter($element, string $path, $value) {
+    if (!twig_test_iterable($element)) {
+      throw new RuntimeError(sprintf('The add filter only works with arrays or "Traversable", got "%s" as first argument.', gettype($element)));
+    }
+
+    if ($element instanceof ArrayAccess) {
+      $filtered_element = clone $element;
+    }
+    else {
+      $filtered_element = $element;
+    }
+
+    // Convert the dotted path into an array of keys.
+    $path = explode('.', $path);
+    $last_path = array_pop($path);
+
+    // Traverse the element down the path, creating arrays as needed.
+    $child_element =& $filtered_element;
+    foreach ($path as $child_path) {
+      if (!isset($child_element[$child_path])) {
+        $child_element[$child_path] = [];
+      }
+      $child_element =& $child_element[$child_path];
+    }
+
+    // If the targeted child element is an array, add the value to it.
+    if (isset($child_element[$last_path]) && is_array($child_element[$last_path])) {
+      if (is_array($value)) {
+        $child_element[$last_path] = array_merge($child_element[$last_path], $value);
+      }
+      else {
+        $child_element[$last_path][] = $value;
+      }
+    }
+    else {
+      // Otherwise, replace the target element with the given value.
+      $child_element[$last_path] = $value;
+    }
+
+    return $filtered_element;
+  }
+
 }
diff --git a/tests/src/Unit/Template/TwigExtensionTest.php b/tests/src/Unit/Template/TwigExtensionTest.php
index daa322db25463afa8db519bdc8777b86627381e5..9c4e866f0d4ec31c64f52e4e709cb6a81b715b64 100644
--- a/tests/src/Unit/Template/TwigExtensionTest.php
+++ b/tests/src/Unit/Template/TwigExtensionTest.php
@@ -180,4 +180,207 @@ class TwigExtensionTest extends UnitTestCase {
     }
   }
 
+  /**
+   * Tests the set filter.
+   *
+   * @covers ::setFilter
+   */
+  public function testSetFilter() {
+    try {
+      TwigExtension::setFilter('not-an-array', ['key' => 'value']);
+      $this->fail('Expected Exception, none was thrown.');
+    }
+    catch (Exception $e) {
+      $this->assertContains('The set filter only works with arrays or "Traversable", got "string" as first argument.', $e->getMessage());
+    }
+
+    $element = [
+      'existing' => 'value',
+      'element' => [
+        'type' => 'element',
+        'attributes' => [
+          'class' => ['old-value-1', 'old-value-2'],
+          'id' => 'element',
+        ],
+      ],
+    ];
+    $value = [
+      'element' => [
+        'attributes' => [
+          'class' => ['new-value'],
+          'placeholder' => 'Label',
+        ],
+      ],
+    ];
+    $expected = [
+      'existing' => 'value',
+      'element' => [
+        'type' => 'element',
+        'attributes' => [
+          'class' => ['new-value', 'old-value-2'],
+          'id' => 'element',
+          'placeholder' => 'Label',
+        ],
+      ],
+    ];
+    try {
+      $result = TwigExtension::setFilter($element, $value);
+      $this->assertEquals($expected, $result);
+      $this->assertEquals(array_replace_recursive($element, $value), $result);
+    }
+    catch (Exception $e) {
+      $this->fail('No Exception expected; "' . $e->getMessage() . '" thrown.');
+    }
+  }
+
+  /**
+   * Tests the add filter.
+   *
+   * @covers ::addFilter
+   */
+  public function testAddFilter() {
+    try {
+      TwigExtension::addFilter('not-an-array', 'key', 'value');
+      $this->fail('Expected Exception, none was thrown.');
+    }
+    catch (Exception $e) {
+      $this->assertContains('The add filter only works with arrays or "Traversable", got "string" as first argument.', $e->getMessage());
+    }
+
+    $data = [
+      'existing' => 'value',
+      'element' => [
+        'type' => 'element',
+        'attributes' => [
+          'class' => ['old-value-1', 'old-value-2'],
+          'id' => 'element',
+        ],
+      ],
+    ];
+
+    // Test replacing a value.
+    $element = $data;
+    $result = NULL;
+    $expected = [
+      'existing' => 'value',
+      'element' => [
+        'type' => 'element',
+        'attributes' => [
+          'class' => ['old-value-1', 'old-value-2'],
+          'id' => 'new-value',
+        ],
+      ],
+    ];
+    try {
+      $result = TwigExtension::addFilter($element, 'element.attributes.id', 'new-value');
+    }
+    catch (Exception $e) {
+      $this->fail('No Exception expected; "' . $e->getMessage() . '" thrown.');
+    }
+    $this->assertEquals($expected, $result, 'Failed to replace a value.');
+
+    // Test setting a new property on an existing array.
+    $element = $data;
+    $result = NULL;
+    $expected = [
+      'existing' => 'value',
+      'element' => [
+        'type' => 'element',
+        'attributes' => [
+          'class' => ['old-value-1', 'old-value-2'],
+          'id' => 'element',
+          'placeholder' => 'new-value',
+        ],
+      ],
+    ];
+    try {
+      $result = TwigExtension::addFilter($element, 'element.attributes.placeholder', 'new-value');
+    }
+    catch (Exception $e) {
+      $this->fail('No Exception expected; "' . $e->getMessage() . '" thrown.');
+    }
+    $this->assertEquals($expected, $result, 'Failed setting a new property value.');
+
+    // Test targeting an existing array with a string.
+    $element = $data;
+    $result = NULL;
+    $expected = [
+      'existing' => 'value',
+      'element' => [
+        'type' => 'element',
+        'attributes' => [
+          'class' => ['old-value-1', 'old-value-2', 'new-value'],
+          'id' => 'element',
+        ],
+      ],
+    ];
+    try {
+      $result = TwigExtension::addFilter(
+        $element,
+        'element.attributes.class',
+        'new-value'
+      );
+    }
+    catch (Exception $e) {
+      $this->fail('No Exception expected; "' . $e->getMessage() . '" thrown.');
+    }
+    $this->assertEquals($expected, $result, 'Failed adding into a targeted array.');
+
+    // Test targeting an existing array with an array.
+    $element = $data;
+    $result = NULL;
+    $expected = [
+      'existing' => 'value',
+      'element' => [
+        'type' => 'element',
+        'attributes' => [
+          'class' => [
+            'old-value-1',
+            'old-value-2',
+            'new-value-1',
+            'new-value-2',
+          ],
+          'id' => 'element',
+        ],
+      ],
+    ];
+    try {
+      $result = TwigExtension::addFilter(
+        $element,
+        'element.attributes.class',
+        ['new-value-1', 'new-value-2']
+      );
+    }
+    catch (Exception $e) {
+      $this->fail('No Exception expected; "' . $e->getMessage() . '" thrown.');
+    }
+    $this->assertEquals($expected, $result, 'Failed merging a targeted array.');
+
+    // Test targeting a non-existent parent property.
+    $element = $data;
+    $result = NULL;
+    $expected = [
+      'existing' => 'value',
+      'element' => [
+        'type' => 'element',
+        'attributes' => [
+          'class' => ['old-value-1', 'old-value-2'],
+          'id' => 'element',
+        ],
+      ],
+      'new-element' => ['attributes' => ['class' => ['new-value']]],
+    ];
+    try {
+      $result = TwigExtension::addFilter(
+        $element,
+        'new-element.attributes.class',
+        ['new-value']
+      );
+    }
+    catch (Exception $e) {
+      $this->fail('No Exception expected; "' . $e->getMessage() . '" thrown.');
+    }
+    $this->assertEquals($expected, $result, 'Failed adding new branch to an element.');
+  }
+
 }