diff --git a/core/lib/Drupal/Core/Template/Attribute.php b/core/lib/Drupal/Core/Template/Attribute.php
index ead5d05..a20b663 100644
--- a/core/lib/Drupal/Core/Template/Attribute.php
+++ b/core/lib/Drupal/Core/Template/Attribute.php
@@ -80,8 +80,18 @@ public function offsetSet($name, $value) {
    *   An AttributeValueBase representation of the attribute's value.
    */
   protected function createAttributeValue($name, $value) {
-    if (is_array($value)) {
-      $value = new AttributeArray($name, $value);
+    // If the value is already an AttributeValueBase object, return it
+    // straight away.
+    if ($value instanceOf AttributeValueBase) {
+      return $value;
+    }
+    // An array value or 'class' attribute name are forced to always be an
+    // AttributeArray value for consistency.
+    if (is_array($value) || $name == 'class') {
+      // Cast the value to an array if the value was passed in as a string.
+      // @todo Decide to fix all the broken instances of class as a string
+      // in core or cast them.
+      $value = new AttributeArray($name, (array) $value);
     }
     elseif (is_bool($value)) {
       $value = new AttributeBoolean($name, $value);
diff --git a/core/lib/Drupal/Core/Template/AttributeArray.php b/core/lib/Drupal/Core/Template/AttributeArray.php
index 95e0ef3..eb8b456 100644
--- a/core/lib/Drupal/Core/Template/AttributeArray.php
+++ b/core/lib/Drupal/Core/Template/AttributeArray.php
@@ -70,6 +70,44 @@ public function __toString() {
   }
 
   /**
+   * Adds the argument values by merging them on to the value array.
+   *
+   * @return $this
+   */
+  public function add() {
+    $args = func_get_args();
+    foreach ($args as $arg) {
+      // Merge the values passed in from the value array.
+      // The argument is cast to an array to support comma separated single
+      // values or one or more array arguments.
+      $this->value = array_merge($this->value, (array) $arg);
+    }
+
+    // Filter out any empty values.
+    $this->value = array_filter($this->value);
+    return $this;
+  }
+
+  /**
+   * Removes the argument values from the value array.
+   *
+   * @return $this
+   */
+  public function remove() {
+    $args = func_get_args();
+    foreach ($args as $arg) {
+      // Remove the values passed in from the value array.
+      // The argument is cast to an array to support comma separated single
+      // values or one or more array arguments.
+      $this->value = array_diff($this->value, (array) $arg);
+    }
+
+    // Filter out any empty values.
+    $this->value = array_filter($this->value);
+    return $this;
+  }
+
+  /**
    * Implements IteratorAggregate::getIterator().
    */
   public function getIterator() {
diff --git a/core/lib/Drupal/Core/Template/TwigExtension.php b/core/lib/Drupal/Core/Template/TwigExtension.php
index 4cb31ca..7b372f7 100644
--- a/core/lib/Drupal/Core/Template/TwigExtension.php
+++ b/core/lib/Drupal/Core/Template/TwigExtension.php
@@ -50,6 +50,9 @@ public function getFilters() {
 
       // Array filters.
       new \Twig_SimpleFilter('without', 'twig_without'),
+
+      // CSS class and ID filters.
+      new \Twig_SimpleFilter('class', 'drupal_html_class'),
     );
   }
 
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index 28c826f..b7870bf 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -667,25 +667,6 @@ function template_preprocess_node(&$variables) {
 
   // Add article ARIA role.
   $variables['attributes']['role'] = 'article';
-
-  // Gather node classes.
-  $variables['attributes']['class'][] = 'node';
-  $variables['attributes']['class'][] = drupal_html_class('node--type-' . $node->bundle());
-  if ($node->isPromoted()) {
-    $variables['attributes']['class'][] = 'node--promoted';
-  }
-  if ($node->isSticky()) {
-    $variables['attributes']['class'][] = 'node--sticky';
-  }
-  if (!$node->isPublished()) {
-    $variables['attributes']['class'][] = 'node--unpublished';
-  }
-  if ($variables['view_mode']) {
-    $variables['attributes']['class'][] = drupal_html_class('node--view-mode-' . $variables['view_mode']);
-  }
-  if (isset($variables['preview'])) {
-    $variables['attributes']['class'][] = 'node--preview';
-  }
 }
 
 /**
diff --git a/core/modules/node/templates/node.html.twig b/core/modules/node/templates/node.html.twig
index c80eecb..4294859 100644
--- a/core/modules/node/templates/node.html.twig
+++ b/core/modules/node/templates/node.html.twig
@@ -77,7 +77,17 @@
  * @ingroup themeable
  */
 #}
-<article{{ attributes }}>
+{% set classes = [
+  'node',
+  'node--type-' ~ node.bundle|class,
+  node.promoted ? 'node--promoted',
+  node.sticky ? 'node--sticky',
+  not node.published ? 'node--unpublished',
+  preview ? 'node--preview',
+  'node--view-mode-' ~ view_mode|class
+]
+%}
+<article class="{{ attributes.class.add(classes) }}"{{ attributes|without('class') }}>
 
   {{ title_prefix }}
   {% if not page %}
diff --git a/core/modules/system/src/Tests/Theme/TwigFilterTest.php b/core/modules/system/src/Tests/Theme/TwigFilterTest.php
index 9b514d7..c132f94 100644
--- a/core/modules/system/src/Tests/Theme/TwigFilterTest.php
+++ b/core/modules/system/src/Tests/Theme/TwigFilterTest.php
@@ -120,6 +120,10 @@ public function testTwigWithoutFilter() {
         'expected' => '<div><span id="quotes" checked class="red green blue">All attributes again.</span></div>',
         'message' => 'All attributes printed again.',
       ),
+      array(
+        'expected' => '<div id="quotes"><span class="gray-like-a-bunny bem__ized--top-feature" id="quotes--2">ID and class.</span></div>',
+        'message' => 'Class and ID filtered.',
+      ),
     );
 
     foreach ($elements as $element) {
diff --git a/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.filter.html.twig b/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.filter.html.twig
index 28e5c15..a0979dd 100644
--- a/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.filter.html.twig
+++ b/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.filter.html.twig
@@ -20,3 +20,4 @@
 <div><span data-id="{{ attributes.id }}"{{ attributes|without('id') }}>Without string attribute.</span></div>
 <div><span{{ attributes|without('id', 'class') }}>Without either nor class attributes.</span></div>
 <div><span{{ attributes }}>All attributes again.</span></div>
+<div id="{{ attributes.id }}"><span class="{{ 'Gray like a bunny!'|class }} {{ 'BEM__ized--Top Feature'|class }}" id="{{ attributes.id }}">ID and class.</span></div>
diff --git a/core/tests/Drupal/Tests/Core/Template/AttributeTest.php b/core/tests/Drupal/Tests/Core/Template/AttributeTest.php
index af5e332..e1a1a98 100644
--- a/core/tests/Drupal/Tests/Core/Template/AttributeTest.php
+++ b/core/tests/Drupal/Tests/Core/Template/AttributeTest.php
@@ -66,6 +66,56 @@ public function testRemove() {
     $this->assertFalse(isset($attribute['class']));
   }
 
+
+  /**
+   * Tests adding class attributes with the AttributeArray helper method.
+   */
+  public function testAddClasses() {
+    $attribute = new Attribute(array('class' => array('example-class')));
+
+    // Add one class.
+    $attribute['class']->add('aa');
+    $this->assertEquals(new AttributeArray('class', array('example-class', 'aa')), $attribute['class']);
+
+    // Add multiple classes.
+    $attribute['class']->add('xx', 'yy');
+    $this->assertEquals(new AttributeArray('class', array('example-class', 'aa', 'xx', 'yy')), $attribute['class']);
+
+    // Add an array of classes.
+    $attribute['class']->add(array('red', 'green', 'blue'));
+    $this->assertEquals(new AttributeArray('class', array('example-class', 'aa', 'xx', 'yy', 'red', 'green', 'blue')), $attribute['class']);
+
+  }
+
+  /**
+   * Tests removing class attributes with the AttributeArray helper method.
+   */
+  public function testRemoveClasses() {
+    $attribute = new Attribute(array('class' => array('example-class', 'aa', 'xx', 'yy', 'red', 'green', 'blue')));
+
+    // Remove one class.
+    $attribute['class']->remove('example-class');
+    $this->assertFalse(in_array('example-class', $attribute['class']->value()));
+
+    // Remove multiple classes.
+    $attribute['class']->remove('xx', 'yy');
+    $this->assertFalse(in_array(array('xx', 'yy'), $attribute['class']->value()));
+
+    // Remove an array of classes.
+    $attribute['class']->remove(array('red', 'green', 'blue'));
+    $this->assertFalse(in_array(array('red', 'green', 'blue'), $attribute['class']->value()));
+  }
+
+  /**
+   * Tests removing class attributes with the AttributeArray helper method.
+   */
+  public function testChainAddRemoveClasses() {
+    $attribute = new Attribute(array('class' => array('example-class', 'red', 'green', 'blue')));
+    unset($attribute['class']);
+    $this->assertFalse(isset($attribute['class']));
+  }
+
+
   /**
    * Tests iterating on the values of the attribute.
    */
