diff --git a/core/modules/ckeditor/ckeditor.admin.inc b/core/modules/ckeditor/ckeditor.admin.inc
index e99518a..cb36652 100644
--- a/core/modules/ckeditor/ckeditor.admin.inc
+++ b/core/modules/ckeditor/ckeditor.admin.inc
@@ -30,18 +30,29 @@ function template_preprocess_ckeditor_settings_toolbar(&$variables) {
       $buttons[$button_name] = $button;
     }
   }
+  $button_groups = array();
   $variables['active_buttons'] = array();
-  foreach ($editor->settings['toolbar']['buttons'] as $row_number => $row) {
-    foreach ($row as $button_name) {
-      if (isset($buttons[$button_name])) {
-        $variables['active_buttons'][$row_number][] = $buttons[$button_name];
-        if (empty($buttons[$button_name]['multiple'])) {
-          unset($buttons[$button_name]);
+  foreach ($editor->settings['toolbar']['rows'] as $row_number => $row) {
+    $button_groups[$row_number] = array();
+    foreach ($row as $group) {
+      foreach ($group['items'] as $button_name) {
+        if (isset($buttons[$button_name])) {
+          // Save a reference to the button's configured toolbar group.
+          $buttons[$button_name]['group'] = $group['name'];
+          $variables['active_buttons'][$row_number][] = $buttons[$button_name];
+          if (empty($buttons[$button_name]['multiple'])) {
+            unset($buttons[$button_name]);
+          }
+          // Create a list of all the toolbar button groups.
+          if (!in_array($group['name'], $button_groups[$row_number])) {
+            array_push($button_groups[$row_number], $group['name']);
+          }
         }
       }
     }
   }
   $variables['disabled_buttons'] = array_diff_key($buttons, $variables['multiple_buttons']);
+  $variables['button_groups'] = $button_groups;
 }
 
 /**
@@ -66,7 +77,7 @@ function theme_ckeditor_settings_toolbar($variables) {
         '#uri' => $button['image' . $rtl],
         '#title' => $button['label'],
       );
-      $value = '<a href="#" class="cke_button" role="button" title="' . $button['label'] . '" aria-label="' . $button['label'] . '"><span class="cke_button_icon">' . drupal_render($image) . '</span></a>';
+      $value = '<a href="#" role="button" title="' . $button['label'] . '" aria-label="' . $button['label'] . '"><span class="cke_button_icon">' . drupal_render($image) . '</span></a>';
     }
     else {
       $value = '?';
@@ -80,8 +91,13 @@ function theme_ckeditor_settings_toolbar($variables) {
     // Build the button item.
     $button_item = array(
       'value' => $value,
-      'data-button-name' => $button['name'],
+      'data-drupal-ckeditor-button-name' => $button['name'],
+      'class' => array('ckeditor-button'),
     );
+    // If this button has group information, add it to the attributes.
+    if (!empty($button['group'])) {
+      $button_item['group'] = $button['group'];
+    }
     if (!empty($button['attributes'])) {
       $button_item = array_merge($button_item, $button['attributes']);
     }
@@ -110,6 +126,7 @@ function theme_ckeditor_settings_toolbar($variables) {
   $print_buttons = function($buttons) {
     $output = '';
     foreach ($buttons as $button) {
+      unset($button['group']);
       $value = $button['value'];
       unset($button['value']);
       $attributes = (string) new Attribute($button);
@@ -118,6 +135,19 @@ function theme_ckeditor_settings_toolbar($variables) {
     return $output;
   };
 
+  $print_button_group = function($buttons, $group_name, $print_buttons) {
+    $group = drupal_html_class($group_name);
+
+    $output = '';
+    $output .= "<li class=\"ckeditor-toolbar-group\" role=\"presentation\" data-drupal-ckeditor-type=\"group\" data-drupal-ckeditor-toolbar-group-name=\"{$group_name}\" tabindex=\"0\">";
+    $output .= "<h3 class=\"ckeditor-toolbar-group-name\" id=\"ckeditor-toolbar-group-aria-label-for-{$group}\">{$group_name}</h3>";
+    $output .= "<ul class=\"ckeditor-buttons ckeditor-toolbar-group-buttons\" role=\"toolbar\" data-drupal-ckeditor-button-sorting=\"target\" aria-labelledby=\"ckeditor-toolbar-group-aria-label-for-{$group}\">";
+    $output .= $print_buttons($buttons);
+    $output .= "</ul></li>";
+
+    return $output;
+  };
+
   // We don't use theme_item_list() below in case there are no buttons in the
   // active or disabled list, as theme_item_list() will not print an empty UL.
   $output = '';
@@ -125,43 +155,45 @@ function theme_ckeditor_settings_toolbar($variables) {
   $output .= '<legend id="ckeditor-button-configuration">' . t('Toolbar configuration') . '</legend>';
   $output .= '<div class="fieldset-wrapper">';
 
-  // aria-live region for outputing aural information about the state of the
-  // configuration.
-  $output .= '<div id="ckeditor-button-configuration-aria-live" class="visually-hidden" aria-live="polite"></div>';
-
-  $output .= '<div id="ckeditor-button-description" class="fieldset-description">' . t('Move a button into the <em>Active toolbar</em> to enable it, or into the list of <em>Available buttons</em> to disable it. Use dividers to create button groups. Buttons may be moved with the mouse or keyboard arrow keys.') . '</div>';
+  $output .= '<div id="ckeditor-button-description" class="fieldset-description">' . t('Move a button into the <em>Active toolbar</em> to enable it, or into the list of <em>Available buttons</em> to disable it. Buttons may be moved with the mouse or keyboard arrow keys. Create a new toolbar group by placing a button in the placeholder group at the end of a row.') . '</div>';
 
   $output .= '<div class="ckeditor-toolbar-disabled clearfix">';
+  // Available buttons.
+  $output .= '<div class="ckeditor-toolbar-available">';
+  $output .= '<label for="ckeditor-available-buttons">' . t('Available buttons') . '</label>';
+  $output .= '<ul id="ckeditor-available-buttons" class="ckeditor-buttons" role="form" data-drupal-ckeditor-button-sorting="source">';
+  $output .= $print_buttons($disabled_buttons);
+  $output .= '</ul>';
+  $output .= '</div>';
+  // Dividers.
   $output .= '<div class="ckeditor-toolbar-dividers">';
-  $output .= '<label id="ckeditor-multiple-label">' . t('Dividers') . '</label>';
-  $output .= '<ul class="ckeditor-multiple-buttons" role="form" aria-labelledby="ckeditor-multiple-label">';
+  $output .= '<label for="ckeditor-multiple-buttons">' . t('Button divider') . '</label>';
+  $output .= '<ul id="ckeditor-multiple-buttons" class="ckeditor-multiple-buttons" role="form" data-drupal-ckeditor-button-sorting="dividers">';
   $output .= $print_buttons($multiple_buttons);
   $output .= '</ul>';
   $output .= '</div>';
-  $output .= '<label id="ckeditor-available-buttons">' . t('Available buttons') . '</label>';
-  $output .= '<ul class="ckeditor-buttons" role="form" aria-labelledby="ckeditor-available-buttons">';
-  $output .= $print_buttons($disabled_buttons);
-  $output .= '</ul>';
   $output .= '</div>';
-
+  // Active toolbar.
   $output .= '<label id="ckeditor-active-toolbar">' . t('Active toolbar') . '</label>';
-
-  $output .= '<div data-toolbar="active" class="ckeditor-toolbar-active clearfix">';
-  foreach ($active_buttons as $button_row) {
-    $output .= '<ul class="ckeditor-buttons" role="form" aria-labelledby="ckeditor-active-toolbar">';
-    $output .= $print_buttons($button_row);
+  $output .= '<div data-toolbar="active" role="form" class="ckeditor-toolbar ckeditor-toolbar-active clearfix">';
+  $output .= '<ul class="ckeditor-active-toolbar-configuration" role="presentation" aria-label="' . t('CKEditor toolbar and button configuration.') . '">';
+  foreach ($active_buttons as $row_number => $button_row) {
+    $output .= '<li class="ckeditor-row" role="group" aria-labelledby="ckeditor-active-toolbar">';
+    $output .= '<ul class="ckeditor-toolbar-groups clearfix">';
+    foreach ($variables['button_groups'][$row_number] as $group_name) {
+      $buttons = array_filter($button_row, function ($button) use ($group_name) {
+        return $button['group'] === $group_name;
+      });
+      $output .= $print_button_group($buttons, $group_name, $print_buttons);
+    }
     $output .= '</ul>';
+    $output .= '</li>';
   }
   if (empty($active_buttons)) {
     $output .= '<ul class="ckeditor-buttons">';
     $output .= '</ul>';
   }
 
-  $output .= '<div class="ckeditor-row-controls">';
-  $output .= '<a href="#" role="button" aria-label="' . t('Remove last button row') . '" class="ckeditor-row-remove" title="' . t('Remove row') . '">-</a>';
-  $output .= '<a href="#" role="button" aria-label="' . t('Add additional button row') . '" class="ckeditor-row-add" title="' . t('Add row') . '">+</a>';
-  $output .= '</div>';
-
   $output .= '</div>';
 
   $output .= '</div>';
diff --git a/core/modules/ckeditor/ckeditor.module b/core/modules/ckeditor/ckeditor.module
index 39c0707..3170a2d 100644
--- a/core/modules/ckeditor/ckeditor.module
+++ b/core/modules/ckeditor/ckeditor.module
@@ -76,9 +76,10 @@ function ckeditor_library_info() {
       array('system', 'jquery.ui.sortable'),
       array('system', 'jquery.ui.draggable'),
       array('system', 'jquery.ui.touch-punch'),
+      array('system', 'backbone'),
+      array('system', 'drupal.dialog'),
       array('ckeditor', 'ckeditor'),
       array('editor', 'drupal.editor.admin'),
-      array('system', 'underscore')
     ),
   );
   $libraries['drupal.ckeditor.drupalimage.admin'] = array(
diff --git a/core/modules/ckeditor/config/schema/ckeditor.schema.yml b/core/modules/ckeditor/config/schema/ckeditor.schema.yml
index b201870..0ac8c41 100644
--- a/core/modules/ckeditor/config/schema/ckeditor.schema.yml
+++ b/core/modules/ckeditor/config/schema/ckeditor.schema.yml
@@ -8,15 +8,25 @@ editor.settings.ckeditor:
       type: mapping
       label: 'Toolbar configuration'
       mapping:
-        buttons:
+        rows:
           type: sequence
           label: 'Rows'
           sequence:
             - type: sequence
-              label: 'Buttons'
+              label: 'Button groups'
               sequence:
-                - type: string
-                  label: 'Button'
+                - type: mapping
+                  label: 'Button group'
+                  mapping:
+                    name:
+                      type: string
+                      label: 'Button group name'
+                    items:
+                      type: sequence
+                      label: 'Buttons'
+                      sequence:
+                        - type: string
+                          label: 'Button'
     plugins:
       type: sequence
       label: 'Plugins'
diff --git a/core/modules/ckeditor/css/ckeditor.admin.css b/core/modules/ckeditor/css/ckeditor.admin.css
index 5e1b2ab..d9a0339 100644
--- a/core/modules/ckeditor/css/ckeditor.admin.css
+++ b/core/modules/ckeditor/css/ckeditor.admin.css
@@ -6,151 +6,231 @@
  * "moono".
  */
 
-.ckeditor-toolbar-active {
+
+
+.ckeditor-toolbar {
   border: 1px solid #b6b6b6;
-  padding: 6px 8px 2px;
+  padding: 0.1667em 0.1667em 0.08em;
   box-shadow: 0 1px 0 white inset;
   background: #cfd1cf;
-  background-image: -webkit-gradient(linear, left top, left bottom, from(whiteSmoke), to(#cfd1cf));
+  background-image: -webkit-linear-gradient(top, whiteSmoke, #cfd1cf);
   background-image: -moz-linear-gradient(top, whiteSmoke, #cfd1cf);
-  background-image: -o-linear-gradient(top, whiteSmoke, #cfd1cf);
-  background-image: -ms-linear-gradient(top, whiteSmoke, #cfd1cf);
   background-image: linear-gradient(top, whiteSmoke, #cfd1cf);
-  filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#fff5f5f5', endColorstr='#ffcfd1cf');
   margin: 5px 0;
-  overflow: nowrap;
+  /* Disallow any user selections in the drag-and-drop toolbar config UI. */
+  -webkit-user-select: none;
+  -moz-user-select:    none;
+  -ms-user-select:     none;
+  user-select:         none;
+}
+.ckeditor-toolbar-disabled {
+  margin-bottom: 0.5em;
+}
+.ckeditor-toolbar ul,
+.ckeditor-toolbar-disabled ul {
+  list-style: none;
+  margin: 0;
+  padding: 0;
+}
+.ckeditor-row {
+  border: 1px solid whitesmoke;
+  padding: 2px 0px 4px;
+  border-radius: 3px;
 }
-.ckeditor-toolbar-active > ul {
-  clear: left; /* LTR */
+.ckeditor-row + .ckeditor-row {
+  margin-top: 0.25em;
+}
+.ckeditor-toolbar-group,
+.ckeditor-toolbar-group-placeholder {
+  display: inline-block;
   float: left; /* LTR */
 }
-[dir="rtl"] .ckeditor-toolbar-active > ul {
-  clear: right;
+[dir="rtl"].ckeditor-toolbar-group,
+[dir="rtl"].ckeditor-toolbar-group-placeholder {
+  display: inline-block;
   float: right;
 }
-#ckeditor-button-description {
-  margin-bottom: 1em;
+.ckeditor-toolbar-groups {
+  min-height: 2em;
 }
-.ckeditor-toolbar-dividers {
+.ckeditor-toolbar-group {
+  margin: 0 0.3333em;
+  cursor: move;
+}
+.ckeditor-group-names-are-visible .ckeditor-toolbar-group {
+  border: 1px dotted #a6a6a6;
+  border-radius: 3px;
+  padding: 0.2em 0.4em;
+}
+.ckeditor-toolbar-group.placeholder,
+.ckeditor-toolbar-group.placeholder .ckeditor-toolbar-group-name {
+  cursor: not-allowed;
+}
+.ckeditor-toolbar-group.placeholder .ckeditor-toolbar-group-name {
+  font-style: italic;
+}
+.ckeditor-toolbar-group-name {
+  display: none;
+  font-size: 1em;
+  font-weight: normal;
+  margin: 0.25em 0;
+}
+.ckeditor-group-names-are-visible .ckeditor-toolbar-group-name {
+  display: block;
+  cursor: pointer;
+}
+.ckeditor-toolbar-group-buttons {
+  float: left; /* LTR */
+}
+[dir="rtl"] .ckeditor-toolbar-group-buttons {
+  float: right;
+}
+.ckeditor-groupnames-toggle {
   float: right; /* LTR */
 }
-[dir="rtl"] .ckeditor-toolbar-dividers {
+[dir="rtl"] .ckeditor-groupnames-toggle {
   float: left;
 }
-.ckeditor-toolbar-disabled ul.ckeditor-buttons {
-  border: 0;
+.ckeditor-toolbar .ckeditor-toolbar-group > li {
+  border: 1px solid white;
+  border-radius: 5px;
+  background-image: -webkit-linear-gradient(transparent 60%, rgba(0, 0, 0, 0.1));
+  background-image: -moz-linear-gradient(transparent 60%, rgba(0, 0, 0, 0.1));
+  background-image: linear-gradient(transparent 60%, rgba(0, 0, 0, 0.1));
+  margin: 3px 6px;
+  padding: 3px;
 }
-.ckeditor-toolbar-disabled ul.ckeditor-buttons li {
-  margin: 2px;
+#ckeditor-button-description {
+  margin-bottom: 1em;
 }
-.ckeditor-toolbar-disabled ul.ckeditor-buttons li a,
-ul.ckeditor-buttons {
+.ckeditor-toolbar-disabled .ckeditor-toolbar-available,
+.ckeditor-toolbar-disabled .ckeditor-toolbar-dividers {
+  -webkit-box-sizing: border-box;
+  -moz-box-sizing: border-box;
+  box-sizing: border-box;
+}
+.ckeditor-toolbar-disabled .ckeditor-toolbar-available {
+  float: left;
+  width: 80%;
+}
+.ckeditor-toolbar-disabled .ckeditor-toolbar-dividers {
+  float: right;
+  width: 20%;
+}
+.ckeditor-toolbar-disabled .ckeditor-buttons li a,
+.ckeditor-toolbar .ckeditor-buttons {
   border: 1px solid #a6a6a6;
   border-bottom-color: #979797;
   border-radius: 3px;
-  box-shadow: 0 1px 0 rgba(255, 255, 255, .5), 0 0 2px rgba(255, 255, 255, .15) inset, 0 1px 0 rgba(255, 255, 255, .15) inset;
+  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.5), 0 0 2px rgba(255, 255, 255, 0.15) inset, 0 1px 0 rgba(255, 255, 255, 0.15) inset;
 }
-
-ul.ckeditor-buttons {
+.ckeditor-toolbar-disabled .ckeditor-buttons {
+  border: 0;
+}
+.ckeditor-toolbar-disabled .ckeditor-buttons li {
+  margin: 2px;
+}
+.ckeditor-buttons {
   min-height: 26px;
   min-width: 26px;
-  list-style: none;
-  padding: 0;
-  margin: 0 6px 5px 0;
-  border: 1px solid #a6a6a6;
-  border-bottom-color: #979797;
-  border-radius: 3px;
-  box-shadow: 0 1px 0 rgba(255, 255, 255, .5), 0 0 2px rgba(255, 255, 255, .15) inset, 0 1px 0 rgba(255, 255, 255, .15) inset;
 }
-ul.ckeditor-buttons li {
+.ckeditor-buttons li {
   display: inline-block;
   padding: 0;
   margin: 0;
   float: left; /* LTR */
 }
-[dir="rtl"] ul.ckeditor-buttons li {
+[dir="rtl"] .ckeditor-buttons li {
   float: right;
 }
-ul.ckeditor-buttons li a {
-  position: relative;
+.ckeditor-buttons li a {
+  background: #e4e4e4;
+  background-image: -moz-linear-gradient(top, white, #e4e4e4);
+  background-image: -webkit-linear-gradient(top, white, #e4e4e4);
+  background-image: linear-gradient(top, white, #e4e4e4);
+  border: 0;
+  color: #474747;
+  cursor: move;
   display: block;
   height: 18px;
   padding: 4px 6px;
-  cursor: move;
-  border: 0;
-  white-space: nowrap;
+  position: relative;
   text-decoration: none;
   text-shadow: 0 1px 0 rgba(255,255,255,.5);
-  color: #474747;
-  background: #e4e4e4;
-  background-image: -webkit-gradient(linear,left top,left bottom,from(white),to(#e4e4e4));
-  background-image: -moz-linear-gradient(top,white,#e4e4e4);
-  background-image: -webkit-linear-gradient(top,white,#e4e4e4);
-  background-image: -o-linear-gradient(top,white,#e4e4e4);
-  background-image: -ms-linear-gradient(top,white,#e4e4e4);
-  background-image: linear-gradient(top,white,#e4e4e4);
-  filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffffff',endColorstr='#ffe4e4e4');
-}
-ul.ckeditor-buttons li .cke-icon-only {
+  white-space: nowrap;
+}
+.ckeditor-toolbar-dividers {
+  float: right; /* LTR */
+}
+[dir="rtl"] .ckeditor-toolbar-dividers {
+  float: left;
+}
+.ckeditor-buttons li .cke-icon-only {
   text-indent: -9999px;
   width: 16px;
+  /* Firefox includes the offscreen text in the focus indicator, resulting in a
+     far too wide focus indicator. This fixes that. */
+  overflow: hidden;
 }
-ul.ckeditor-buttons li a:focus,
-ul.ckeditor-multiple-buttons li a:focus {
+.ckeditor-buttons li a:focus,
+.ckeditor-buttons li a:active,
+.ckeditor-multiple-buttons li a:focus {
   z-index: 11; /* Ensure focused buttons show their outline on all sides. */
-  outline: 1px dotted #333;
 }
-ul.ckeditor-buttons li:first-child a {
+.ckeditor-buttons li:first-child a {
   border-top-left-radius: 2px; /* LTR */
   border-bottom-left-radius: 2px; /* LTR */
 }
-[dir="rtl"] ul.ckeditor-buttons li:first-child a {
+[dir="rtl"] .ckeditor-buttons li:first-child a {
   border-top-right-radius: 2px;
   border-bottom-right-radius: 2px;
 }
-ul.ckeditor-buttons li:last-child a {
+.ckeditor-buttons li:last-child a {
   border-top-right-radius: 2px; /* LTR */
   border-bottom-right-radius: 2px; /* LTR */
 }
-[dir="rtl"] ul.ckeditor-buttons li:last-child a {
+[dir="rtl"] .ckeditor-buttons li:last-child a {
   border-top-left-radius: 2px;
   border-bottom-left-radius: 2px;
 }
-ul.ckeditor-buttons li.ckeditor-button-placeholder a {
-  background: #333;
-  opacity: 0.3;
+.ckeditor-button-placeholder,
+.ckeditor-toolbar-group-placeholder {
+  background: #9dcae7;
 }
-ul.ckeditor-multiple-buttons {
+.ckeditor-toolbar-group-placeholder {
+  border-radius: 4px;
+}
+.ckeditor-multiple-buttons {
   padding: 1px 2px;
   margin: 5px;
   list-style: none;
   float: left; /* LTR */
 }
-[dir="rtl"] ul.ckeditor-multiple-buttons {
+[dir="rtl"] .ckeditor-multiple-buttons {
   float: right;
 }
-ul.ckeditor-multiple-buttons li {
+.ckeditor-multiple-buttons li {
   display: inline-block;
   float: left; /* LTR */
   margin: 0;
   padding: 0;
 }
-[dir="rtl"] ul.ckeditor-multiple-buttons li {
+[dir="rtl"] .ckeditor-multiple-buttons li {
   float: right;
 }
-ul.ckeditor-multiple-buttons li a {
+.ckeditor-multiple-buttons li a {
   cursor: move;
   display: inline-block;
   height: 18px;
   margin: 0;
   padding: 2px 0;
 }
-ul.ckeditor-buttons li.ckeditor-group-button-separator,
-ul.ckeditor-multiple-buttons li.ckeditor-group-button-separator {
+.ckeditor-buttons .ckeditor-group-button-separator,
+.ckeditor-multiple-buttons .ckeditor-group-button-separator {
   margin: -1px -3px -2px;
 }
-ul.ckeditor-buttons li.ckeditor-group-button-separator a,
-ul.ckeditor-multiple-buttons li.ckeditor-group-button-separator a {
+.ckeditor-buttons .ckeditor-group-button-separator a,
+.ckeditor-multiple-buttons .ckeditor-group-button-separator a {
   background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAdCAMAAABG4xbVAAAAhFBMVEUAAACmpqampqampqb////l5eX////5+fmmpqatra2urq6vr6+1tbW2tra4uLi6urq8vLzb29ve3t7i4uLl5eXn5+fo6Ojp6enq6urr6+vs7Ozt7e3u7u7v7+/w8PDx8fHy8vLz8/P09PT19fX29vb39/f4+Pj5+fn6+vr7+/v8/Pz+/v7qIQO+AAAACHRSTlMATVmAi8XM29MuWToAAABjSURBVBiVrc5BCoAwDETRMKhtRBduev9LKm1xjItWRBBE6Nt9QkIwOTcUzk0Imi8aoMssxbgoTHMtqsFMLta0vPh2N49HyfdelPg6k9uvX/a+Bmggt1qJRNzQFVgjEnkUZDoBmH57VSypjg4AAAAASUVORK5CYII=) no-repeat center center;
   width: 13px;
   padding: 0;
@@ -161,6 +241,7 @@ ul.ckeditor-multiple-buttons li.ckeditor-group-button-separator a {
 ul.ckeditor-buttons li.ckeditor-button-separator a {
   background: #e4e4e4;
   background-image: -webkit-linear-gradient(#e4e4e4, #b4b4b4);
+  background-image: -moz-linear-gradient(#e4e4e4, #b4b4b4);
   background-image: linear-gradient(#e4e4e4, #b4b4b4);
   height: 24px;
   margin: 1px 0 0;
@@ -169,7 +250,7 @@ ul.ckeditor-buttons li.ckeditor-button-separator a {
   width: 1px;
   z-index: 10;
 }
-ul.ckeditor-multiple-buttons li.ckeditor-button-separator a {
+.ckeditor-multiple-buttons .ckeditor-button-separator a {
   width: 2px;
   padding: 0;
   height: 26px;
@@ -177,12 +258,12 @@ ul.ckeditor-multiple-buttons li.ckeditor-button-separator a {
 }
 .ckeditor-separator {
   background-color: silver;
-  background-color: rgba(0, 0, 0, .2);
+  background-color: rgba(0, 0, 0, 0.2);
   margin: 5px 0;
   height: 18px;
   width: 1px;
   display: block;
-  box-shadow: 1px 0 1px rgba(255, 255, 255, .5)
+  box-shadow: 1px 0 1px rgba(255, 255, 255, 0.5)
 }
 .ckeditor-button-arrow {
   width: 0;
@@ -193,7 +274,6 @@ ul.ckeditor-multiple-buttons li.ckeditor-button-separator a {
   display: inline-block;
   margin: 0 4px 2px;
 }
-
 .ckeditor-row-controls {
   float: right; /* LTR */
   font-size: 18px;
diff --git a/core/modules/ckeditor/js/ckeditor.admin.js b/core/modules/ckeditor/js/ckeditor.admin.js
index 338ae6e..95221dc 100644
--- a/core/modules/ckeditor/js/ckeditor.admin.js
+++ b/core/modules/ckeditor/js/ckeditor.admin.js
@@ -1,207 +1,194 @@
-(function ($, Drupal, drupalSettings, CKEDITOR, _) {
+/**
+ * @file
+ * CKEditor button and group configuration user interface.
+ */
+(function ($, Drupal, _, CKEDITOR) {
 
 "use strict";
 
 Drupal.ckeditor = Drupal.ckeditor || {};
 
-// Aria-live element for speaking application state.
-var $messages;
-
 Drupal.behaviors.ckeditorAdmin = {
   attach: function (context) {
-    var $context = $(context);
-    var $ckeditorToolbar = $context.find('.ckeditor-toolbar-configuration').once('ckeditor-toolbar');
-    var featuresMetadata = {};
-    var hiddenCKEditorConfig = drupalSettings.ckeditor.hiddenCKEditorConfig;
+    // Process the CKEditor configuration fragment once.
+    var $configurationForm = $(context).find('.ckeditor-toolbar-configuration');
+    if ($configurationForm.once('ckeditor-configuration').length) {
+      var $textarea = $configurationForm
+        // Hide the textarea that contains the serialized representation of the
+        // CKEditor configuration.
+        .find('.form-item-editor-settings-toolbar-button-groups')
+        .hide()
+        // Return the textarea child node from this expression.
+        .find('textarea');
 
-    /**
-     * Event callback for keypress. Move buttons based on arrow keys.
-     */
-    function adminToolbarMoveButton (event) {
-      var $target = $(event.currentTarget);
-      var $button = $target.parent();
-      var $currentRow = $button.closest('.ckeditor-buttons');
-      var $destinationRow = null;
-      var destinationPosition = $button.index();
-
-      switch (event.keyCode) {
-        case 37: // Left arrow.
-        case 63234: // Safari left arrow.
-          $destinationRow = $currentRow;
-          destinationPosition -= rtl;
-          break;
-
-        case 38: // Up arrow.
-        case 63232: // Safari up arrow.
-          $destinationRow = $($toolbarRows[$toolbarRows.index($currentRow) - 1]);
-          break;
-
-        case 39: // Right arrow.
-        case 63235: // Safari right arrow.
-          $destinationRow = $currentRow;
-          destinationPosition += rtl;
-          break;
-
-        case 40: // Down arrow.
-        case 63233: // Safari down arrow.
-          $destinationRow = $($toolbarRows[$toolbarRows.index($currentRow) + 1]);
-      }
+      // The HTML for the CKEditor configuration is assembled on the server and
+      // and sent to the client as a serialized DOM fragment.
+      $configurationForm.append(drupalSettings.ckeditor.toolbarAdmin);
 
-      if ($destinationRow && $destinationRow.length) {
-        // Detach the button from the DOM so its position doesn't interfere.
-        $button.detach();
-        // Move the button before the button whose position it should occupy.
-        var $targetButton = $destinationRow.children(':eq(' + destinationPosition + ')');
-        if ($targetButton.length) {
-          $targetButton.before($button);
-        }
-        else {
-          $destinationRow.append($button);
-        }
-        // Post the update to the aria-live message element.
-        $messages.text(Drupal.t('moved to @row, position @position of @totalPositions', {
-          '@row': getRowInfo($destinationRow),
-          '@position': (destinationPosition + 1),
-          '@totalPositions': $destinationRow.children().length
-        }));
-        // Update the toolbar value field.
-        adminToolbarValue(event, { item: $button });
-      }
-      event.preventDefault();
+      // Create a configuration model.
+      var model = Drupal.ckeditor.models.configurationModel = new Drupal.ckeditor.ConfigurationModel({
+        $textarea: $textarea,
+        activeEditorConfig: JSON.parse($textarea.val()),
+        hiddenEditorConfig: drupalSettings.ckeditor.hiddenCKEditorConfig
+      });
+
+      // Create the configuration Views.
+      var viewDefaults = {
+        model: model,
+        el: $('.ckeditor-toolbar-configuration')
+      };
+      Drupal.ckeditor.views = {
+        controller: new Drupal.ckeditor.ConfigurationController(viewDefaults),
+        visualView: new Drupal.ckeditor.ConfigurationVisualView(viewDefaults),
+        keyboardView: new Drupal.ckeditor.ConfigurationKeyboardView(viewDefaults),
+        auralView: new Drupal.ckeditor.ConfigurationAuralView(viewDefaults)
+      };
+    }
+  },
+  detach: function (context, settings, trigger) {
+    // Early-return if the trigger for detachment is something else than unload.
+    if (trigger !== 'unload') {
+      return;
     }
 
-    /**
-     * Event callback for keyup. Move a separator into the active toolbar.
-     */
-    function adminToolbarMoveSeparator (event) {
-      switch (event.keyCode) {
-        case 38: // Up arrow.
-        case 63232: // Safari up arrow.
-          var $button = $(event.currentTarget).parent().clone().appendTo($toolbarRows.eq(-2));
-          adminToolbarValue(event, { item: $button });
-          event.preventDefault();
+    // We're detaching because CKEditor as text editor has been disabled; this
+    // really means that all CKEditor toolbar buttons have been removed. Hence,
+    // all editor features will be removed, so any reactions from filters will
+    // be undone.
+    var $configurationForm = $(context).find('.ckeditor-toolbar-configuration.ckeditor-configuration-processed');
+    if ($configurationForm.length && Drupal.ckeditor.models && Drupal.ckeditor.models.configurationModel) {
+      var config = Drupal.ckeditor.models.configurationModel.toJSON().activeEditorConfig;
+      var buttons = Drupal.ckeditor.views.controller.getButtonList(config);
+      var $activeToolbar = $('.ckeditor-toolbar-configuration').find('.ckeditor-toolbar-active');
+      for (var i = 0; i < buttons.length; i++) {
+        $activeToolbar.trigger('CKEditorToolbarChanged', ['removed', buttons[i]]);
       }
     }
+  }
+};
 
-    /**
-     * Provide help when a button is clicked on.
-     */
-    function adminToolbarButtonHelp (event) {
-      var $link = $(event.currentTarget);
-      var $button = $link.parent();
-      var $currentRow = $button.closest('.ckeditor-buttons');
-      var enabled = $button.closest('.ckeditor-toolbar-active').length > 0;
-      var position = $button.index() + 1; // 1-based index for humans.
-      var rowNumber = $toolbarRows.index($currentRow) + 1;
-      var type = event.data.type;
-      var message;
+/**
+ * CKEditor configuration UI methods of Backbone objects.
+ */
+Drupal.ckeditor = {
 
-      if (enabled) {
-        if (type === 'separator') {
-          message = Drupal.t('Separators are used to visually split individual buttons. This @name is currently enabled, in row @row and position @position.', { '@name': $link.attr('aria-label'), '@row': rowNumber, '@position': position }) + "\n\n" + Drupal.t('Drag and drop the separator or use the keyboard arrow keys to change the position of this separator.');
-        }
-        else {
-          message = Drupal.t('The @name button is currently enabled, in row @row and position @position.', { '@name': $link.attr('aria-label'), '@row': rowNumber, '@position': position }) + "\n\n" + Drupal.t('Drag and drop the buttons or use the keyboard arrow keys to change the position of this button.');
-        }
-      }
-      else {
-        if (type === 'separator') {
-          message = Drupal.t('Separators are used to visually split individual buttons. This @name is currently disabled.', { '@name': $link.attr('aria-label') }) + "\n\n" + Drupal.t('Drag the button or use the up arrow key to move this separator into the active toolbar. You may add multiple separators to each row.');
-        }
-        else {
-          message = Drupal.t('The @name button is currently disabled.', { '@name': $link.attr('aria-label') }) + "\n\n" + Drupal.t('Drag the button or use the up arrow key to move this button into the active toolbar.');
-        }
-      }
-      $messages.text(message);
-      $link.focus();
-      event.preventDefault();
-    }
+  // A hash of View instances.
+  views: {},
 
-    /**
-     * Add a new row of buttons.
-     */
-    function adminToolbarAddRow (event) {
-      var $this = $(event.currentTarget);
-      var $rows = $this.closest('.ckeditor-toolbar-active').find('.ckeditor-buttons');
-      var $rowNew = $rows.last().clone().empty().sortable(sortableSettings);
-      $rows.last().after($rowNew);
-      $toolbarRows = $toolbarAdmin.find('.ckeditor-buttons');
-      $this.siblings('a').show();
-      redrawToolbarGradient();
-      // Post the update to the aria-live message element.
-      $messages.text(Drupal.t('row number @count added.', {'@count': ($rows.length + 1)}));
-      event.preventDefault();
-    }
+  // A hash of Model instances.
+  models: {},
 
-    /**
-     * Remove a row of buttons.
-     */
-    function adminToolbarRemoveRow (event) {
-      var $this = $(event.currentTarget);
-      var $rows = $this.closest('.ckeditor-toolbar-active').find('.ckeditor-buttons');
-      if ($rows.length === 2) {
-        $this.hide();
-      }
-      if ($rows.length > 1) {
-        var $lastRow = $rows.last();
-        var $disabledButtons = $ckeditorToolbar.find('.ckeditor-toolbar-disabled .ckeditor-buttons');
-        $lastRow.children(':not(.ckeditor-multiple-button)').prependTo($disabledButtons);
-        $lastRow.sortable('destroy').remove();
-        $toolbarRows = $toolbarAdmin.find('.ckeditor-buttons');
-        redrawToolbarGradient();
-      }
-      // Post the update to the aria-live message element.
-      $messages.text(Drupal.formatPlural($rows.length - 1, 'row removed. 1 row remaining.', 'row removed. @count rows remaining.'));
-      event.preventDefault();
+  /**
+   * Backbone model for the CKEditor toolbar configuration state.
+   */
+  ConfigurationModel: Backbone.Model.extend({
+    defaults: {
+      // The CKEditor configuration that is being manipulated through the UI.
+      activeEditorConfig: null,
+      // The textarea that contains the serialized representation of the active
+      // CKEditor configuration.
+      $textarea: null,
+      // Tracks whether the active toolbar DOM structure has been changed. When
+      // true, activeEditorConfig needs to be updated, and when that is updated,
+      // $textarea will also be updated.
+      isDirty: false,
+      // The configuration for the hidden CKEditor instance that is used to build
+      // the features metadata.
+      hiddenEditorConfig: null,
+      //
+      featuresMetadata: null,
+      // Whether the button group names are currently visible.
+      groupNamesVisible: false,
+    },
+    sync: function () {
+      // Push the settings into the textarea.
+      this.get('$textarea').val(JSON.stringify(this.get('activeEditorConfig')));
     }
+  }),
+
+  /**
+   * Backbone View acting as a controller for CKEditor toolbar configuration.
+   */
+  ConfigurationController: Backbone.View.extend({
+
+    events: {},
 
     /**
-     * Browser quirk work-around to redraw CSS3 gradients.
+     * {@inheritdoc}
      */
-    function redrawToolbarGradient () {
-      $ckeditorToolbar.find('.ckeditor-toolbar-active').css('position', 'relative');
-      window.setTimeout(function () {
-        $ckeditorToolbar.find('.ckeditor-toolbar-active').css('position', '');
-      }, 10);
-    }
+    initialize: function () {
+      this.getCKEditorFeatures(this.model.get('hiddenEditorConfig'), this.disableFeaturesDisallowedByFilters.bind(this));
+
+      // Push the active editor configuration to the textarea.
+      this.model.on('change:activeEditorConfig', this.model.sync, this.model);
+      this.model.on('change:isDirty', this.parseEditorDOM, this);
+    },
 
     /**
-     * jQuery Sortable stop event. Save updated toolbar positions to the
-     * textarea.
+     * Converts the active toolbar DOM structure to an object representation.
+     *
+     * @param Drupal.ckeditor.ConfigurationModel model
+     *   The state model for the CKEditor configuration.
+     * @param Boolean isDirty
+     *   Tracks whether the active toolbar DOM structure has been changed.
+     *   isDirty is toggled back to false in this method.
+     * @param Object options
+     *   An object that includes:
+     *   - Boolean broadcast: (optional) A flag that controls whether a
+     *     CKEditorToolbarChanged event should be fired for configuration
+     *     changes.
      */
-    function adminToolbarValue (event, ui) {
-      var oldToolbarConfig = JSON.parse($textarea.val());
-
-      // Update the toolbar config after updating a sortable.
-      var toolbarConfig = [];
-      var $button = ui.item;
-      $button.find('a').focus();
-      $ckeditorToolbar.find('.ckeditor-toolbar-active ul').each(function () {
-        var $rowButtons = $(this).find('li');
-        var rowConfig = [];
-        if ($rowButtons.length) {
-          $rowButtons.each(function () {
-            rowConfig.push(this.getAttribute('data-button-name'));
+    parseEditorDOM: function (model, isDirty, options) {
+      if (isDirty) {
+        var currentConfig = this.model.get('activeEditorConfig');
+
+        // Process the rows
+        var rows = [];
+        this.$el
+          .find('.ckeditor-active-toolbar-configuration')
+          .children('.ckeditor-row').each(function () {
+            var groups = [];
+            // Process the button groups.
+            $(this).find('.ckeditor-toolbar-group').each(function () {
+              var $group = $(this);
+              var $buttons = $group.find('.ckeditor-button');
+              if ($buttons.length) {
+                var group = {
+                  name: $group.attr('data-drupal-ckeditor-toolbar-group-name'),
+                  items: []
+                };
+                $group.find('.ckeditor-button, .ckeditor-multiple-button').each(function () {
+                  group.items.push($(this).attr('data-drupal-ckeditor-button-name'));
+                });
+                groups.push(group);
+              }
+            });
+            if (groups.length) {
+              rows.push(groups);
+            }
           });
-          toolbarConfig.push(rowConfig);
-        }
-      });
-      $textarea.val(JSON.stringify(toolbarConfig, null, '  '));
+        this.model.set('activeEditorConfig', rows);
+        // Mark the model as clean. Whether or not the sync to the textfield
+        // occurs depends on the activeEditorConfig attribute firing a change
+        // event. The DOM has at least been processed and posted, so as far as
+        // the model is concerned, it is clean.
+        this.model.set('isDirty', false);
 
-      if (!ui.silent) {
         // Determine whether we should trigger an event.
-        var prev = _.flatten(oldToolbarConfig);
-        var next = _.flatten(toolbarConfig);
-        if (prev.length !== next.length) {
-          $ckeditorToolbar
-          .find('.ckeditor-toolbar-active')
-          .trigger('CKEditorToolbarChanged', [
-            (prev.length < next.length) ? 'added' : 'removed',
-            _.difference(_.union(prev, next), _.intersection(prev, next))[0]
-          ]);
+        if (options.broadcast !== false) {
+          var prev = this.getButtonList(currentConfig);
+          var next = this.getButtonList(rows);
+          if (prev.length !== next.length) {
+            this.$el
+            .find('.ckeditor-toolbar-active')
+            .trigger('CKEditorToolbarChanged', [
+              (prev.length < next.length) ? 'added' : 'removed',
+              _.difference(_.union(prev, next), _.intersection(prev, next))[0]
+            ]);
+          }
         }
       }
-    }
+    },
 
     /**
      * Asynchronously retrieve the metadata for all available CKEditor features.
@@ -211,8 +198,15 @@ Drupal.behaviors.ckeditorAdmin = {
      * filter settings. Because creating an instance is expensive, a callback
      * must be provided that will receive a hash of Drupal.EditorFeature
      * features keyed by feature (button) name.
+     *
+     * @param Object CKEditorConfig
+     *   An object that represents the configuration settings for a CKEditor
+     *   editor component.
+     * @param Function callback
+     *   A function to invoke when the instanceReady event is fired by the
+     *   CKEditor object.
      */
-    function getCKEditorFeatures(CKEditorConfig, callback) {
+    getCKEditorFeatures: function (CKEditorConfig, callback) {
       var getProperties = function (CKEPropertiesList) {
         return (_.isObject(CKEPropertiesList)) ? _.keys(CKEPropertiesList) : [];
       };
@@ -243,14 +237,15 @@ Drupal.behaviors.ckeditorAdmin = {
       };
 
       // Create hidden CKEditor with all features enabled, retrieve metadata.
-      // @see \Drupal\ckeditor\Plugin\editor\editor\CKEditor::settingsForm.
+      // @see \Drupal\ckeditor\Plugin\Editor\CKEditor::settingsForm.
       var hiddenCKEditorID = 'ckeditor-hidden';
       if (CKEDITOR.instances[hiddenCKEditorID]) {
         CKEDITOR.instances[hiddenCKEditorID].destroy(true);
       }
       // Load external plugins, if any.
-      if (hiddenCKEditorConfig.drupalExternalPlugins) {
-        var externalPlugins = hiddenCKEditorConfig.drupalExternalPlugins;
+      var hiddenEditorConfig = this.model.get('hiddenEditorConfig');
+      if (hiddenEditorConfig.drupalExternalPlugins) {
+        var externalPlugins = hiddenEditorConfig.drupalExternalPlugins;
         for (var pluginName in externalPlugins) {
           if (externalPlugins.hasOwnProperty(pluginName)) {
             CKEDITOR.plugins.addExternal(pluginName, externalPlugins[pluginName], '');
@@ -289,15 +284,20 @@ Drupal.behaviors.ckeditorAdmin = {
           callback(features);
         }
       });
-    }
+    },
 
     /**
      * Retrieves the feature for a given button from featuresMetadata. Returns
      * false if the given button is in fact a divider.
+     *
+     * @param String button
+     *   The name of a CKEditor button.
+     * @return Object
+     *   The feature metadata object for a button.
      */
-    function getFeatureForButton (button) {
+    getFeatureForButton: function (button) {
       // Return false if the button being added is a divider.
-      if (button === '|' || button === '-') {
+      if (button === '-') {
         return false;
       }
 
@@ -305,21 +305,83 @@ Drupal.behaviors.ckeditorAdmin = {
       // the feature that was just added or removed. Not every feature has
       // such metadata.
       var featureName = button.toLowerCase();
+      var featuresMetadata = this.model.get('featuresMetadata');
       if (!featuresMetadata[featureName]) {
         featuresMetadata[featureName] = new Drupal.EditorFeature(featureName);
+        this.model.set('featuresMetadata', featuresMetadata);
       }
       return featuresMetadata[featureName];
-    }
+    },
+
+    /**
+     * Checks buttons against filter settings; disables disallowed buttons.
+     *
+     * @param Object features
+     *   A map of Drupal.EditorFeature objects.
+     */
+    disableFeaturesDisallowedByFilters: function (features) {
+      this.model.set('featuresMetadata', features);
+
+      // Ensure that toolbar configuration changes are broadcast.
+      this.broadcastConfigurationChanges(this.$el);
+
+      // Initialization: not all of the default toolbar buttons may be allowed
+      // by the current filter settings. Remove any of the default toolbar
+      // buttons that require more permissive filter settings. The remaining
+      // default toolbar buttons are marked as "added".
+      var existingButtons = [];
+      // Loop through each button group after flattening the groups from the
+      // toolbar row arrays.
+      for (var i = 0, buttonGroups = _.flatten(this.model.get('activeEditorConfig')); i < buttonGroups.length; i++) {
+        // Pull the button names from each toolbar button group.
+        for (var k = 0, buttons = buttonGroups[i].items; k < buttons.length; k++) {
+          existingButtons.push(buttons[k]);
+        }
+      }
+      // Remove duplicate buttons.
+      existingButtons = _.unique(existingButtons);
+      // Prepare the active toolbar and available-button toolbars.
+      for (i = 0; i < existingButtons.length; i++) {
+        var button = existingButtons[i];
+        var feature = this.getFeatureForButton(button);
+        // Skip dividers.
+        if (feature === false) {
+          continue;
+        }
+
+        if (Drupal.editorConfiguration.featureIsAllowedByFilters(feature)) {
+          // Default toolbar buttons are in fact "added features".
+          this.$el.find('.ckeditor-toolbar-active').trigger('CKEditorToolbarChanged', ['added', existingButtons[i]]);
+        }
+        else {
+          // Move the button element from the active the active toolbar to the
+          // list of available buttons.
+          $('.ckeditor-toolbar-active li[data-drupal-ckeditor-button-name="' + button + '"]')
+            .detach()
+            .appendTo('.ckeditor-toolbar-disabled > .ckeditor-toolbar-available > ul');
+          // Update the toolbar value field.
+          this.model.set({'isDirty': true}, {broadcast: false});
+        }
+      }
+    },
 
     /**
      * Sets up broadcasting of CKEditor toolbar configuration changes.
+     *
+     * @param jQuery $ckeditorToolbar
+     *   The active toolbar DOM element wrapped in jQuery.
      */
-    function broadcastConfigurationChanges ($ckeditorToolbar) {
+    broadcastConfigurationChanges: function ($ckeditorToolbar) {
+      var view = this;
+      var hiddenEditorConfig = this.model.get('hiddenEditorConfig');
+      var featuresMetadata = this.model.get('featuresMetadata');
+      var getFeatureForButton = this.getFeatureForButton.bind(this);
+      var getCKEditorFeatures = this.getCKEditorFeatures.bind(this);
       $ckeditorToolbar
         .find('.ckeditor-toolbar-active')
         // Listen for CKEditor toolbar configuration changes. When a button is
         // added/removed, call an appropriate Drupal.editorConfiguration method.
-        .on('CKEditorToolbarChanged.ckeditorAdmin', function (e, action, button) {
+        .on('CKEditorToolbarChanged.ckeditorAdmin', function (event, action, button) {
           var feature = getFeatureForButton(button);
 
           // Early-return if the button being added is a divider.
@@ -329,21 +391,22 @@ Drupal.behaviors.ckeditorAdmin = {
 
           // Trigger a standardized text editor configuration event to indicate
           // whether a feature was added or removed, so that filters can react.
-          var event = (action === 'added') ? 'addedFeature' : 'removedFeature';
-          Drupal.editorConfiguration[event](feature);
+          var configEvent = (action === 'added') ? 'addedFeature' : 'removedFeature';
+          Drupal.editorConfiguration[configEvent](feature);
         })
         // Listen for CKEditor plugin settings changes. When a plugin setting is
         // changed, rebuild the CKEditor features metadata.
-        .on('CKEditorPluginSettingsChanged.ckeditorAdmin', function (e, settingsChanges) {
+        // @todo I think this is a dead code path.
+        .on('CKEditorPluginSettingsChanged.ckeditorAdmin', function (event, settingsChanges) {
           // Update hidden CKEditor configuration.
           for (var key in settingsChanges) {
             if (settingsChanges.hasOwnProperty(key)) {
-              hiddenCKEditorConfig[key] = settingsChanges[key];
+              hiddenEditorConfig[key] = settingsChanges[key];
             }
           }
 
           // Retrieve features for the updated hidden CKEditor configuration.
-          getCKEditorFeatures(hiddenCKEditorConfig, function (features) {
+          getCKEditorFeatures(hiddenEditorConfig, function (features) {
             // Trigger a standardized text editor configuration event for each
             // feature that was modified by the configuration changes.
             for (var name in features) {
@@ -355,165 +418,988 @@ Drupal.behaviors.ckeditorAdmin = {
               }
             }
             // Update the CKEditor features metadata.
-            featuresMetadata = features;
+            view.model.set('featuresMetadata', features);
           });
         });
+    },
+
+    /**
+     * Returns the list of buttons from an editor configuration.
+     *
+     * @param Object config
+     *   A CKEditor configuration object.
+     * @return Array
+     *   A list of buttons in the CKEditor configuration.
+     */
+    getButtonList: function (config) {
+      var buttons = [];
+      // Remove the rows
+      config = _.flatten(config);
+
+      // Loop through the button groups and pull out the buttons.
+      config.forEach(function (group) {
+        group.items.forEach(function (button) {
+          buttons.push(button);
+        });
+      });
+
+      // Remove the dividing elements if any.
+      return _.without(buttons, '-');
     }
+  }),
+
+  /**
+   * Backbone View for CKEditor toolbar configuration; visual UX.
+   */
+  ConfigurationVisualView: Backbone.View.extend({
+
+    events: {
+      'click .ckeditor-toolbar-group-name': 'onGroupNameClick',
+      'click .ckeditor-groupnames-toggle': 'onGroupNamesToggleClick'
+    },
+
+    /**
+     * {@inheritdoc}
+     */
+    initialize: function () {
+      this.model.on('change:isDirty change:groupNamesVisible', this.render, this);
+
+      // Add a toggle for the button group names.
+      $(Drupal.theme('ckeditorButtonGroupNamesToggle'))
+        .insertBefore(this.$el.find('.ckeditor-toolbar-active').prev('label'));
+
+      this.render();
+    },
+
+    /**
+     * {@inheritdoc}
+     */
+    render: function (model, value, changedAttributes) {
+      this.insertPlaceholders();
+      this.applySorting();
+
+      // Toggle button group names.
+      var groupNamesVisible = this.model.get('groupNamesVisible');
+      // If a button was just placed in the active toolbar, ensure that the
+      // button group names are visible.
+      if (changedAttributes && changedAttributes.changes && changedAttributes.changes.isDirty) {
+        this.model.set({groupNamesVisible: true}, {silent: true});
+        groupNamesVisible = true;
+      }
+      this.$el.find('[data-toolbar="active"]').toggleClass('ckeditor-group-names-are-visible', groupNamesVisible);
+      this.$el.find('.ckeditor-groupnames-toggle')
+        .text((groupNamesVisible) ? Drupal.t('Hide group names') : Drupal.t('Show group names'))
+        .attr('aria-pressed', groupNamesVisible);
+
+      return this;
+    },
+
+    /**
+     * Handles clicks to a button group name.
+     *
+     * @param jQuery.Event event
+     */
+    onGroupNameClick: function (event) {
+      var $group = $(event.currentTarget).closest('.ckeditor-toolbar-group');
+      openGroupNameDialog(this, $group);
+
+      event.stopPropagation();
+      event.preventDefault();
+    },
+
+    /**
+     * Handles clicks on the button group names toggle button.
+     */
+    onGroupNamesToggleClick: function (event) {
+      event.preventDefault();
+      this.model.set('groupNamesVisible', !this.model.get('groupNamesVisible'));
+    },
+
+    /**
+     * Handles jQuery Sortable stop sort of a button group.
+     *
+     * @param jQuery.Event event
+     * @param Object ui
+     *   A jQuery.ui.sortable argument that contains information about the
+     *   elements involved in the sort action.
+     */
+    endGroupDrag: function (event, ui) {
+      var view = this;
+      registerGroupMove(this, ui.item, function (success) {
+        if (!success) {
+          // Cancel any sorting in the configuration area.
+          view.$el.find('.ckeditor-toolbar-configuration').find('.ui-sortable').sortable('cancel');
+        }
+      });
+    },
+
+    /**
+     * Handles jQuery Sortable start sort of a button.
+     *
+     * @param jQuery.Event event
+     * @param Object ui
+     *   A jQuery.ui.sortable argument that contains information about the
+     *   elements involved in the sort action.
+     */
+    startButtonDrag: function (event, ui) {
+      this.$el.find('a:focus').blur();
+
+      // Show the button group names as soon as the user starts dragging.
+      this.model.set('groupNamesVisible', true);
+    },
+
+    /**
+     * Handles jQuery Sortable stop sort of a button.
+     *
+     * @param jQuery.Event event
+     * @param Object ui
+     *   A jQuery.ui.sortable argument that contains information about the
+     *   elements involved in the sort action.
+     */
+    endButtonDrag: function (event, ui) {
+      var view = this;
+      registerButtonMove(this, ui.item, function (success) {
+        if (!success) {
+          // Cancel any sorting in the configuration area.
+          view.$el.find('.ui-sortable').sortable('cancel');
+        }
+        // Refocus the target button so that the user can continue from a known
+        // place.
+        ui.item.find('a').focus();
+      });
+    },
 
-    if ($ckeditorToolbar.length) {
-      var $textareaWrapper = $ckeditorToolbar.find('.form-item-editor-settings-toolbar-buttons').hide();
-      var $textarea = $textareaWrapper.find('textarea');
-      var $toolbarAdmin = $(drupalSettings.ckeditor.toolbarAdmin);
-      var sortableSettings = {
+    /**
+     * Invokes jQuery.sortable() on new buttons and groups in a CKEditor config.
+     */
+    applySorting: function () {
+      // Make the buttons sortable.
+      this.$el.find('.ckeditor-buttons').not('.ui-sortable').sortable({
+        // Change this to .ckeditor-toolbar-group-buttons.
         connectWith: '.ckeditor-buttons',
         placeholder: 'ckeditor-button-placeholder',
         forcePlaceholderSize: true,
         tolerance: 'pointer',
         cursor: 'move',
-        stop: adminToolbarValue
-      };
-      // Add the toolbar to the page.
-      $toolbarAdmin.insertAfter($textareaWrapper);
+        start: this.startButtonDrag.bind(this),
+        // Sorting within a sortable.
+        stop: this.endButtonDrag.bind(this)
+      }).disableSelection();
 
-      // Then determine if this is RTL or not.
-      var rtl = $toolbarAdmin.css('direction') === 'rtl' ? -1 : 1;
-      var $toolbarRows = $toolbarAdmin.find('.ckeditor-buttons');
+      // Add the drag and drop functionality to button groups.
+      this.$el.find('.ckeditor-toolbar-groups').not('.ui-sortable').sortable({
+        connectWith: '.ckeditor-toolbar-groups',
+        cancel: '.ckeditor-toolbar-group.placeholder',
+        placeholder: 'ckeditor-toolbar-group-placeholder',
+        forcePlaceholderSize: true,
+        cursor: 'move',
+        stop: this.endGroupDrag.bind(this)
+      });
 
-      // Add the drag and drop functionality.
-      $toolbarRows.sortable(sortableSettings);
-      $toolbarAdmin.find('.ckeditor-multiple-buttons li').draggable({
+      // Add the drag and drop functionality to buttons.
+      this.$el.find('.ckeditor-multiple-buttons li').draggable({
         connectToSortable: '.ckeditor-toolbar-active .ckeditor-buttons',
         helper: 'clone'
       });
+    },
+
+    /**
+     * Wraps the invocation of methods to insert blank groups and rows.
+     */
+    insertPlaceholders: function () {
+      this.insertPlaceholderRow();
+      this.insertPlaceholderGroup();
+    },
+
+    /**
+     * Inserts a blank row at the bottom of the CKEditor configuration.
+     */
+    insertPlaceholderRow: function () {
+      var $rows = this.$el.find('.ckeditor-row');
+      // Add a placeholder row. to the end of the list if one does not exist.
+      if (!$rows.eq(-1).hasClass('placeholder')) {
+         this.$el
+          .find('.ckeditor-toolbar-active')
+          .children('.ckeditor-active-toolbar-configuration')
+          .append(Drupal.theme('ckeditorRow'));
+      }
+      // Update the $rows variable to include the new row.
+      $rows = this.$el.find('.ckeditor-row');
+      // Remove blank rows except the last one.
+      var len = $rows.length;
+      $rows.filter(function (index, row) {
+          // Do not remove the last row.
+          if (index + 1 === len) {
+            return false;
+          }
+          return $(row).find('.ckeditor-toolbar-group').not('.placeholder').length === 0;
+        })
+      // Then get all rows that are placeholders and remove them.
+      .remove();
+    },
+
+    /**
+     * Inserts a blank group at the end of a row CKEditor configuration.
+     */
+    insertPlaceholderGroup: function () {
+      // Add placeholder groups.
+      this.$el.find('.ckeditor-row').each(function () {
+        var $row = $(this);
+        var $groups = $row.find('.ckeditor-toolbar-group');
+        var $placeholder = $groups.filter('.placeholder');
+        if ($placeholder.length === 0) {
+          $row.children('.ckeditor-toolbar-groups').append(Drupal.theme('ckeditorToolbarGroup'));
+        }
+        // If a placeholder group exists, make sure it's at the end of the row.
+        else if (!$groups.eq(-1).hasClass('placeholder')) {
+          $placeholder.appendTo($row.children('.ckeditor-toolbar-groups'));
+        }
+      });
+    }
+  }),
+
+  /**
+   * Backbone View for CKEditor toolbar configuration; keyboard UX.
+   */
+  ConfigurationKeyboardView: Backbone.View.extend({
 
+    /**
+     * {@inheritdoc}
+     */
+    initialize: function () {
       // Add keyboard arrow support.
-      $toolbarAdmin.on('keyup.ckeditorMoveButton', '.ckeditor-buttons a', adminToolbarMoveButton);
-      $toolbarAdmin.on('keyup.ckeditorMoveSeparator', '.ckeditor-multiple-buttons a', adminToolbarMoveSeparator);
-
-      // Add click for help.
-      $toolbarAdmin.on('click.ckeditorClickButton', '.ckeditor-buttons a', { type: 'button' }, adminToolbarButtonHelp);
-      $toolbarAdmin.on('click.ckeditorClickSeparator', '.ckeditor-multiple-buttons a', { type: 'separator' }, adminToolbarButtonHelp);
-
-      // Add/remove row button functionality.
-      $toolbarAdmin.on('click.ckeditorAddRow', 'a.ckeditor-row-add', adminToolbarAddRow);
-      $toolbarAdmin.on('click.ckeditorAddRow', 'a.ckeditor-row-remove', adminToolbarRemoveRow);
-      if ($toolbarAdmin.find('.ckeditor-toolbar-active ul').length > 1) {
-        $toolbarAdmin.find('a.ckeditor-row-remove').hide();
+      this.$el.on('keyup.ckeditor', '.ckeditor-buttons a, .ckeditor-multiple-buttons a', this.onPressButton.bind(this));
+      this.$el.on('keyup.ckeditor', '[data-drupal-ckeditor-type="group"]', this.onPressGroup.bind(this));
+    },
+
+    /**
+     * {@inheritdoc}
+     */
+    render: function () {},
+
+    /**
+     * Handles keypresses on a CKEditor configuration button.
+     *
+     * @param jQuery.Event event
+     */
+    onPressButton: function (event) {
+      var upDownKeys = [
+        38, // Up arrow.
+        63232, // Safari up arrow.
+        40, // Down arrow.
+        63233 // Safari down arrow.
+      ];
+      var leftRightKeys = [
+        37, // Left arrow.
+        63234, // Safari left arrow.
+        39, // Right arrow.
+        63235 // Safari right arrow.
+      ];
+
+      // Respond to an enter key press. Prevent the bubbling of the enter key
+      // press to the button group parent element.
+      if (event.keyCode === 13) {
+        event.stopPropagation();
       }
 
-      // Add aural UI focus updates when for individual toolbars.
-      $toolbarAdmin.on('focus.ckeditor', '.ckeditor-buttons', grantRowFocus);
-      // Identify the aria-live element for interaction updates for screen
-      // readers.
-      $messages = $('#ckeditor-button-configuration-aria-live');
-
-      getCKEditorFeatures(hiddenCKEditorConfig, function (features) {
-        featuresMetadata = features;
-
-        // Ensure that toolbar configuration changes are broadcast.
-        broadcastConfigurationChanges($ckeditorToolbar);
-
-        // Initialization: not all of the default toolbar buttons may be allowed
-        // by the current filter settings. Remove any of the default toolbar
-        // buttons that require more permissive filter settings. The remaining
-        // default toolbar buttons are marked as "added".
-        var $activeToolbar = $ckeditorToolbar.find('.ckeditor-toolbar-active');
-        var existingButtons = _.unique(_.flatten(JSON.parse($textarea.val())));
-        for (var i = 0; i < existingButtons.length; i++) {
-          var button = existingButtons[i];
-          var feature = getFeatureForButton(button);
+      // Only take action when a direction key is pressed.
+      if (_.indexOf(_.union(upDownKeys, leftRightKeys), event.keyCode) > -1) {
+        var view = this;
+        var $target = $(event.currentTarget);
+        var $button = $target.parent();
+        var $container = $button.parent();
+        var $group = $button.closest('.ckeditor-toolbar-group');
+        var $row = $button.closest('.ckeditor-row');
+        var containerType = $container.data('drupal-ckeditor-button-sorting');
+        var $availableButtons = this.$el.find('[data-drupal-ckeditor-button-sorting="source"]');
+        var $activeButtons = this.$el.find('.ckeditor-toolbar-active');
+        // The current location of the button, just in case it needs to be put
+        // back.
+        var $originalGroup = $group;
+        var dir;
 
-          // Skip dividers.
-          if (feature === false) {
-            continue;
+        // Move available buttons between their container and the active toolbar.
+        if (containerType === 'source') {
+          // Move the button to the active toolbar configuration when the down or
+          // up keys are pressed.
+          if (_.indexOf([40, 63233], event.keyCode) > -1) {
+            // Move the button to the first row, first button group index
+            // position.
+            $activeButtons.find('.ckeditor-toolbar-group-buttons').eq(0).prepend($button);
+          }
+        }
+        else if (containerType === 'target') {
+          // Move buttons between sibling buttons in a group and between groups.
+          if (_.indexOf(leftRightKeys, event.keyCode) > -1) {
+            // Move left.
+            var $siblings = $container.children();
+            var index = $siblings.index($button);
+            if (_.indexOf([37, 63234], event.keyCode) > -1) {
+              // Move between sibling buttons.
+              if (index > 0) {
+                $button.insertBefore($container.children().eq(index - 1));
+              }
+              // Move between button groups and rows.
+              else {
+                // Move between button groups.
+                $group = $container.parent().prev();
+                if ($group.length > 0) {
+                  $group.find('.ckeditor-toolbar-group-buttons').append($button);
+                }
+                // Wrap between rows.
+                else {
+                  $container.closest('.ckeditor-row').prev().find('.ckeditor-toolbar-group').not('.placeholder').find('.ckeditor-toolbar-group-buttons').eq(-1).append($button);
+                }
+              }
+            }
+            // Move right.
+            else if (_.indexOf([39, 63235], event.keyCode) > -1) {
+              // Move between sibling buttons.
+              if (index < ($siblings.length - 1)) {
+                $button.insertAfter($container.children().eq(index + 1));
+              }
+              // Move between button groups. Moving right at the end of a row
+              // will create a new group.
+              else {
+                $container.parent().next().find('.ckeditor-toolbar-group-buttons').prepend($button);
+              }
+            }
+          }
+          // Move buttons between rows and the available button set.
+          else if (_.indexOf(upDownKeys, event.keyCode) > -1) {
+            dir = (_.indexOf([38, 63232], event.keyCode) > -1) ? 'prev' : 'next';
+            $row = $container.closest('.ckeditor-row')[dir]();
+            // Move the button back into the available button set.
+            if (dir === 'prev' && $row.length === 0) {
+              // If this is a divider, just destroy it.
+              if ($button.data('drupal-ckeditor-type') === 'separator') {
+                $button
+                  .off()
+                  .remove();
+                // Focus on the first button in the active toolbar.
+                $activeButtons.find('.ckeditor-toolbar-group-buttons').eq(0).children().eq(0).children().focus();
+              }
+              // Otherwise, move it.
+              else {
+                $availableButtons.prepend($button);
+              }
+            }
+            else {
+              $row.find('.ckeditor-toolbar-group-buttons').eq(0).prepend($button);
+            }
+          }
+        }
+        // Move dividers between their container and the active toolbar.
+        else if (containerType === 'dividers') {
+          // Move the button to the active toolbar configuration when the down or
+          // up keys are pressed.
+          if (_.indexOf([40, 63233], event.keyCode) > -1) {
+            // Move the button to the first row, first button group index
+            // position.
+            $button = $button.clone(true);
+            $activeButtons.find('.ckeditor-toolbar-group-buttons').eq(0).prepend($button);
+            $target = $button.children();
           }
+        }
+
+        view = this;
+        // Attempt to move the button to the new toolbar position.
+        registerButtonMove(this, $button, function (result) {
 
-          if (Drupal.editorConfiguration.featureIsAllowedByFilters(feature)) {
-            // Default toolbar buttons are in fact "added features".
-            $activeToolbar.trigger('CKEditorToolbarChanged', ['added', existingButtons[i]]);
+          // Put the button back if the registration failed.
+          // If the button was in a row, then it was in the active toolbar
+          // configuration. The button was probably placed in a new group, but
+          // that action was canceled.
+          if (!result && $originalGroup) {
+            $originalGroup.find('.ckeditor-buttons').append($button);
           }
+          // Otherwise refresh the sortables to acknowledge the new button
+          // positions.
           else {
-            // Move the button element from the active the active toolbar to the
-            // list of available buttons.
-            var $button = $('.ckeditor-toolbar-active > ul > li[data-button-name="' + button + '"]')
-              .detach()
-              .appendTo('.ckeditor-toolbar-disabled > ul');
-            // Update the toolbar value field.
-            adminToolbarValue({}, { silent: true, item: $button});
+            view.$el.find('.ui-sortable').sortable('refresh');
+          }
+          // Refocus the target button so that the user can continue from a known
+          // place.
+          $target.focus();
+        });
+
+        event.preventDefault();
+        event.stopPropagation();
+      }
+    },
+
+    /**
+     * Handles keypresses on a CKEditor configuration group.
+     *
+     * @param jQuery.Event event
+     */
+    onPressGroup: function (event) {
+      var upDownKeys = [
+        38, // Up arrow.
+        63232, // Safari up arrow.
+        40, // Down arrow.
+        63233 // Safari down arrow.
+      ];
+      var leftRightKeys = [
+        37, // Left arrow.
+        63234, // Safari left arrow.
+        39, // Right arrow.
+        63235 // Safari right arrow.
+      ];
+
+      // Respond to an enter key press.
+      if (event.keyCode === 13) {
+        openGroupNameDialog(this, $(event.currentTarget));
+        event.preventDefault();
+        event.stopPropagation();
+      }
+
+      // Respond to direction key presses.
+      if (_.indexOf(_.union(upDownKeys, leftRightKeys), event.keyCode) > -1) {
+        var $group = $(event.currentTarget);
+        var $container = $group.parent();
+        var $siblings = $container.children();
+        var index, dir;
+        // Move groups between sibling groups.
+        if (_.indexOf(leftRightKeys, event.keyCode) > -1) {
+          index = $siblings.index($group);
+          // Move left between sibling groups.
+          if ((_.indexOf([37, 63234], event.keyCode) > -1)) {
+            if (index > 0) {
+              $group.insertBefore($siblings.eq(index - 1));
+            }
+            // Wrap between rows. Insert the group before the placeholder group
+            // at the end of the previous row.
+            else {
+              $group.insertBefore($container.closest('.ckeditor-row').prev().find('.ckeditor-toolbar-groups').children().eq(-1));
+            }
           }
+          // Move right between sibling groups.
+          else if (_.indexOf([39, 63235], event.keyCode) > -1) {
+            // Move to the right if the next group is not a placeholder.
+            if (!$siblings.eq(index + 1).hasClass('placeholder')) {
+              $group.insertAfter($container.children().eq(index + 1));
+            }
+            // Wrap group between rows.
+            else {
+              $container.closest('.ckeditor-row').next().find('.ckeditor-toolbar-groups').prepend($group);
+            }
+          }
+
         }
-      });
-    }
-  },
-  detach: function (context, settings, trigger) {
-    // Early-return if the trigger for detachment is something else than unload.
-    if (trigger !== 'unload') {
-      return;
+        // Move groups between rows.
+        else if (_.indexOf(upDownKeys, event.keyCode) > -1) {
+          dir = (_.indexOf([38, 63232], event.keyCode) > -1) ? 'prev' : 'next';
+          $group.closest('.ckeditor-row')[dir]().find('.ckeditor-toolbar-groups').eq(0).prepend($group);
+        }
+
+        registerGroupMove(this, $group);
+        $group.focus();
+        event.preventDefault();
+        event.stopPropagation();
+      }
     }
+  }),
 
-    // We're detaching because CKEditor as text editor has been disabled; this
-    // really means that all CKEditor toolbar buttons have been removed. Hence,
-    // all editor features will be removed, so any reactions from filters will
-    // be undone.
-    var $ckeditorToolbar = $(context).find('.ckeditor-toolbar-configuration.ckeditor-toolbar-processed');
-    if ($ckeditorToolbar.length) {
-      var value = $ckeditorToolbar
-        .find('.form-item-editor-settings-toolbar-buttons')
-        .find('textarea')
-        .val();
-      var $activeToolbar = $ckeditorToolbar.find('.ckeditor-toolbar-active');
-      var buttons = _.unique(_.flatten(JSON.parse(value)));
-      for (var i = 0; i < buttons.length; i++) {
-        $activeToolbar.trigger('CKEditorToolbarChanged', ['removed', buttons[i]]);
+  /**
+   * Backbone View for CKEditor toolbar configuration; aural UX (output only).
+   */
+  ConfigurationAuralView: Backbone.View.extend({
+
+    events: {
+      'click .ckeditor-buttons a': 'announceButtonHelp',
+      'click .ckeditor-multiple-buttons a': 'announceSeparatorHelp',
+      'focus .ckeditor-button a': 'onFocus',
+      'focus .ckeditor-button-separator a': 'onFocus',
+      'focus .ckeditor-toolbar-group': 'onFocus'
+    },
+
+    /**
+     * {@inheritdoc}
+     */
+    initialize: function () {
+      // Announce the button and group positions when the model is no longer
+      // dirty.
+      this.model.on('change:isDirty', this.announceMove, this);
+    },
+
+    /**
+     * Calls announce on buttons and groups when their position is changed.
+     *
+     * @param Drupal.ckeditor.ConfigurationModel model
+     * @param Boolean isDirty
+     *   A model attribute that indicates if the changed toolbar configuration
+     *   has been stored or not.
+     */
+    announceMove: function (model, isDirty) {
+      // Announce the position of a button or group after the model has been
+      // updated.
+      if (!isDirty) {
+        var item = document.activeElement || null;
+        if (item) {
+          var $item = $(item);
+          if ($item.hasClass('ckeditor-toolbar-group')) {
+            this.announceButtonGroupPosition($item);
+          }
+          else if ($item.parent().hasClass('ckeditor-button')) {
+            this.announceButtonPosition($item.parent());
+          }
+        }
+      }
+    },
+
+    /**
+     * Handles the focus event of elements in the active and available toolbars.
+     *
+     * @param jQuery.Event event
+     */
+    onFocus: function (event) {
+      event.stopPropagation();
+
+      var $originalTarget = $(event.target);
+      var $currentTarget = $(event.currentTarget);
+      var $parent = $currentTarget.parent();
+      if ($parent.hasClass('ckeditor-button') || $parent.hasClass('ckeditor-button-separator')) {
+        this.announceButtonPosition($currentTarget.parent());
+      }
+      else if ($originalTarget.attr('role') !== 'button' && $currentTarget.hasClass('ckeditor-toolbar-group')) {
+        this.announceButtonGroupPosition($currentTarget);
+      }
+    },
+
+    /**
+     * Announces the current position of a button group.
+     *
+     * @param jQuery $group
+     *   A jQuery set that contains an li element that wraps a group of buttons.
+     */
+    announceButtonGroupPosition: function ($group) {
+      var $groups = $group.parent().children();
+      var $row = $group.closest('.ckeditor-row');
+      var $rows = $row.parent().children();
+      var text = Drupal.t('"@groupName" button group in position @position of @positionCount in row @row of @rowCount', {
+        '@groupName': $group.attr('data-drupal-ckeditor-toolbar-group-name'),
+        '@position': $groups.index($group) + 1,
+        '@positionCount': $groups.not('.placeholder').length,
+        '@row': $rows.index($row) + 1,
+        '@rowCount': $rows.not('.placeholder').length
+      });
+      Drupal.announce(text, 'assertive');
+    },
+
+
+    /**
+     * Announces current button position.
+     *
+     * @param jQuery $button
+     *   A jQuery set that contains an li element that wraps a button.
+     */
+    announceButtonPosition: function ($button) {
+      var $row = $button.closest('.ckeditor-row');
+      var $rows = $row.parent().children();
+      var $buttons = $button.closest('.ckeditor-buttons').children();
+      var $group = $button.closest('.ckeditor-toolbar-group');
+      // The name of the button separator is 'button separator' and its type
+      // is 'separator', so we do not want to print the type of this item,
+      // otherwise the UA will speak 'button separator separator'.
+      var type = ($button.attr('data-drupal-ckeditor-type') === 'separator') ? '' : Drupal.t('button');
+      var text;
+      // The button is located in the available button set.
+      if ($button.closest('.ckeditor-toolbar-disabled').length > 0) {
+        text = Drupal.t('@name @type.', {
+          '@name': $button.children().attr('aria-label'),
+          '@type': type
+        });
+        text += "\n" + Drupal.t('Press the down arrow key to activate.');
+
+        Drupal.announce(text, 'assertive');
+      }
+      // The button is in the active toolbar.
+      else if ($group.not('.placeholder').length === 1) {
+        text = Drupal.t('@name @type in position @position of @positionCount in "@groupName" button group in row @row of @rowCount.', {
+          '@name': $button.children().attr('aria-label'),
+          '@type': type,
+          '@position': $buttons.index($button) + 1,
+          '@positionCount': $buttons.length,
+          '@groupName': $group.attr('data-drupal-ckeditor-toolbar-group-name'),
+          '@row': $rows.index($row) + 1,
+          '@rowCount': $rows.not('.placeholder').length
+        });
+        Drupal.announce(text, 'assertive');
       }
+    },
+
+    /**
+     * Provides help information when a button is clicked.
+     *
+     * @param jQuery.Event event
+     */
+    announceButtonHelp: function (event) {
+      var $link = $(event.currentTarget);
+      var $button = $link.parent();
+      var enabled = $button.closest('.ckeditor-toolbar-active').length > 0;
+      var message;
+
+      if (enabled) {
+        message = Drupal.t('The "@name" button is currently enabled.', {
+          '@name': $link.attr('aria-label')
+        });
+        message += "\n" + Drupal.t('Use the keyboard arrow keys to change the position of this button.');
+        message += "\n" + Drupal.t('Press the up arrow key on the top row to disable the button.');
+      }
+      else {
+        message = Drupal.t('The "@name" button is currently disabled.', {
+          '@name': $link.attr('aria-label')
+        });
+        message += "\n" + Drupal.t('Use the down arrow key to move this button into the active toolbar.');
+      }
+      Drupal.announce(message);
+      event.preventDefault();
+    },
+
+    /**
+     * Provides help information when a separator is clicked.
+     *
+     * @param jQuery.Event event
+     */
+    announceSeparatorHelp: function (event) {
+      var $link = $(event.currentTarget);
+      var $button = $link.parent();
+      var enabled = $button.closest('.ckeditor-toolbar-active').length > 0;
+      var message;
+
+      if (enabled) {
+        message = Drupal.t('This @name is currently enabled.', {
+          '@name': $link.attr('aria-label')
+        });
+        message += "\n" + Drupal.t('Use the keyboard arrow keys to change the position of this separator.');
+      }
+      else {
+        message = Drupal.t('Separators are used to visually split individual buttons.');
+        message += "\n" + Drupal.t('This @name is currently disabled.', {
+          '@name': $link.attr('aria-label')
+        });
+        message += "\n" + Drupal.t('Use the down arrow key to move this separator into the active toolbar.');
+        message += "\n" + Drupal.t('You may add multiple separators to each button group.');
+      }
+      Drupal.announce(message);
+      event.preventDefault();
     }
-  }
+  })
 };
 
 /**
- * Returns a string describing the type and index of a toolbar row.
+ * Translates a change in CKEditor config DOM structure into the config model.
  *
- * @param {jQuery} $row
- *   A jQuery object containing a .ckeditor-button row.
+ * If the button is moved within an existing group, the DOM structure is simply
+ * translated to a configuration model. If the button is moved into a new group
+ * placeholder, then a process is launched to name that group before the button
+ * move is translated into configuration.
  *
- * @return {String}
- *   A string describing the type and index of a toolbar row.
+ * @param Backbone.View view
+ *   The Backbone View that invoked this function.
+ * @param jQuery $button
+ *   A jQuery set that contains an li element that wraps a button element.
+ * @param function callback
+ *   A callback to invoke after the button group naming modal dialog has been
+ *   closed.
  */
-function getRowInfo ($row) {
-  var output = '';
-  var row;
-  // Determine if this is an active row or an available row.
-  if ($row.closest('.ckeditor-toolbar-disabled').length > 0) {
-    row = $('.ckeditor-toolbar-disabled').find('.ckeditor-buttons').index($row) + 1;
-    output += Drupal.t('available button row @row', {'@row': row});
+function registerButtonMove (view, $button, callback) {
+  var $group = $button.closest('.ckeditor-toolbar-group');
+
+  // If dropped in a placeholder button group, the user must name it.
+  if ($group.hasClass('placeholder')) {
+
+    if (view.isProcessing) {
+      event.stopPropagation();
+      return;
+    }
+    view.isProcessing = true;
+
+    openGroupNameDialog(view, $group, callback);
   }
   else {
-    row = $('.ckeditor-toolbar-active').find('.ckeditor-buttons').index($row) + 1;
-    output += Drupal.t('active button row @row', {'@row': row});
+    view.model.set('isDirty', true);
+    callback(true);
   }
-  return output;
 }
 
 /**
- * Applies or removes the focused class to a toolbar row.
+ * Translates a change in CKEditor config DOM structure into the config model.
+ *
+ * Each row has a placeholder group at the end of the row. A user may not move
+ * an existing button group past the placeholder group at the end of a row.
  *
- * When a button in a toolbar is focused, focus is triggered on the containing
- * toolbar row. When a row is focused, the state change is announced through
- * the aria-live message area.
+ * @param Backbone.View view
+ *   The Backbone View that invoked this function.
+ * @param jQuery $group
+ *   A jQuery set that contains an li element that wraps a group of buttons.
+ */
+function registerGroupMove (view, $group) {
+  // Remove placeholder classes if necessary.
+  var $row = $group.closest('.ckeditor-row');
+  if ($row.hasClass('placeholder')) {
+    $row.removeClass('placeholder');
+  }
+  // If there are any rows with just a placeholder group, mark the row as a
+  // placeholder.
+  $row.parent().children().each(function () {
+    var $row = $(this);
+    if ($row.find('.ckeditor-toolbar-group').not('.placeholder').length === 0) {
+      $row.addClass('placeholder');
+    }
+  });
+  view.model.set('isDirty', true);
+}
+
+/**
+ * Opens a Drupal dialog with a form for changing the title of a button group.
  *
- * @param {jQuery} event
- *   A jQuery event.
+ * @param Backbone.View view
+ *   The Backbone View that invoked this function.
+ * @param jQuery $group
+ *   A jQuery set that contains an li element that wraps a group of buttons.
+ * @param function callback
+ *   A callback to invoke after the button group naming modal dialog has been
+ *   closed.
  */
-function grantRowFocus (event) {
-  var $row = $(event.currentTarget);
-  // Remove the focused class from all other toolbars.
-  $('.ckeditor-buttons.focused').not($row).removeClass('focused');
-  // Post the update to the aria-live message element.
-  if (!$row.hasClass('focused')) {
-    // Indicate that the current row has focus.
-    $row.addClass('focused');
-    $messages.text(Drupal.t('@row', {'@row': getRowInfo($row)}));
+function openGroupNameDialog (view, $group, callback) {
+  callback = callback || function () {};
+
+  /**
+   * Validates the string provided as a button group title.
+   *
+   * @param DOM form
+   *   The form DOM element that contains the input with the new button group
+   *   title string.
+   * @return Boolean
+   *   Returns true when an error exists, otherwise returns false.
+   */
+  function validateForm (form) {
+    if (form.elements[0].value.length === 0) {
+      var $form = $(form);
+      if (!$form.hasClass('errors')) {
+        $form
+          .addClass('errors')
+          .find('input')
+          .addClass('error')
+          .attr('aria-invalid', 'true');
+        $('<div class=\"description\" >' + Drupal.t('Please provide a name for the button group.') + '</div>').insertAfter(form.elements[0]);
+      }
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Attempts to close the dialog; Validates user input.
+   *
+   * @param String action
+   *   The dialog action chosen by the user: 'apply' or 'cancel'.
+   * @param DOM form
+   *   The form DOM element that contains the input with the new button group
+   *   title string.
+   */
+  function closeDialog (action, form) {
+
+    /**
+     * Closes the dialog when the user cancels or supplies valid data.
+     */
+    function shutdown () {
+      dialog.close(action);
+
+      // The processing marker can be deleted since the dialog has been closed.
+      delete view.isProcessing;
+    }
+
+    /**
+     * Applies a string as the name of a CKEditor button group.
+     *
+     * @param jQuery $group
+     *   A jQuery set that contains an li element that wraps a group of buttons.
+     * @param String name
+     *   The new name of the CKEditor button group.
+     */
+    function namePlaceholderGroup ($group, name) {
+      // If it's currently still a placeholder, then that means we're creating
+      // a new group, and we must do some extra work.
+      if ($group.hasClass('placeholder')) {
+        // Remove all whitespace from the name, lowercase it and ensure
+        // HTML-safe encoding, then use this as the group ID for CKEditor
+        // configuration UI accessibility purposes only.
+        var groupID = 'ckeditor-toolbar-group-aria-label-for-' + Drupal.checkPlain(name.toLowerCase().replace(/ /g,'-'));
+        $group
+          // Update the group container.
+          .removeAttr('aria-label')
+          .attr('data-drupal-ckeditor-type', 'group')
+          .attr('tabindex', 0)
+          // Update the group heading.
+          .children('.ckeditor-toolbar-group-name')
+          .attr('id', groupID)
+          .end()
+          // Update the group items.
+          .children('.ckeditor-toolbar-group-buttons')
+          .attr('aria-labelledby', groupID);
+      }
+
+      $group
+        .attr('data-drupal-ckeditor-toolbar-group-name', name)
+        .children('.ckeditor-toolbar-group-name')
+        .text(name);
+    }
+
+    // Invoke a user-provided callback and indicate failure.
+    if (action === 'cancel') {
+      shutdown();
+      callback(false);
+      return;
+    }
+
+    // Validate that a group name was provided.
+    if (form && validateForm(form)) {
+      return;
+    }
+
+    // React to application of a valid group name.
+    if (action === 'apply') {
+      shutdown();
+      // Apply the provided name to the button group label.
+      namePlaceholderGroup($group, Drupal.checkPlain(form.elements[0].value));
+      // Remove placeholder classes so that new placeholders will be
+      // inserted.
+      $group.closest('.ckeditor-row.placeholder').addBack().removeClass('placeholder');
+
+      // Invoke a user-provided callback and indicate success.
+      callback(true);
+
+      // Signal that the active toolbar DOM structure has changed.
+      view.model.set('isDirty', true);
+    }
   }
+
+  // Create a Drupal dialog that will get a button group name from the user.
+  var dialog = Drupal.dialog(Drupal.theme('ckeditorButtonGroupNameForm'), {
+    title: Drupal.t('Button group name'),
+    dialogClass: 'ckeditor-name-toolbar-group',
+    resizable: false,
+    buttons: [
+      {
+        text: Drupal.t('Apply'),
+        click: function () {
+          closeDialog('apply', this);
+        }
+      },
+      {
+        text: Drupal.t('Cancel'),
+        click: function () {
+          closeDialog('cancel');
+        }
+      }
+    ],
+    // Disable jQuery UI Dialog's escape handling so we can handle this
+    // ourselves, we want the 'cancel' button to be pressed.
+    closeOnEscape: false,
+    open: function () {
+      var form = this;
+      var $form = $(this);
+      var $widget = $form.parent();
+      $widget.find('.ui-dialog-titlebar-close').remove();
+      // Set a click handler on the input and button in the form.
+      $widget.on('keyup.ckeditor', 'input, button', function (event) {
+        // React to enter key press.
+        if (event.keyCode === 13) {
+          var $target = $(event.currentTarget);
+          var data = $target.data('ui-button');
+          var action = 'apply';
+          // Assume 'apply', but take into account that the user might have
+          // pressed the enter key on the dialog buttons.
+          if (data && data.options && data.options.label) {
+            action = data.options.label.toLowerCase();
+          }
+          closeDialog(action, form);
+          event.stopPropagation();
+          event.stopImmediatePropagation();
+          event.preventDefault();
+        }
+        // React to ESC key press.
+        else if (event.keyCode === 27) {
+          closeDialog('cancel', form);
+          event.stopPropagation();
+          event.stopImmediatePropagation();
+          event.preventDefault();
+        }
+      });
+      // Prevent the form from submitting.
+      $widget.on('keydown keypress', function () {
+        if (event.keyCode === 13) {
+          return false;
+        }
+      });
+      // Announce to the user that a modal dialog is open.
+      var text = Drupal.t('Editing the name of the new button group in a dialog.');
+      if ($group.attr('data-drupal-ckeditor-toolbar-group-name') !== undefined) {
+        text = Drupal.t('Editing the name of the "@groupName" button group in a dialog.', {
+          '@groupName': $group.attr('data-drupal-ckeditor-toolbar-group-name')
+        });
+      }
+      Drupal.announce(text);
+    },
+    beforeClose: false,
+    close: function (event) {
+      // Automatically destroy the DOM element that was used for the dialog.
+      $(event.target).remove();
+    }
+  });
+  // A modal dialog is used because the user must provide a button group name
+  // or cancel the button placement before taking any other action.
+  dialog.showModal();
+
+  $(document.querySelector('.ckeditor-name-toolbar-group').querySelector('input'))
+    // When editing, set the "group name" input in the form to the current value.
+    .attr('value', $group.attr('data-drupal-ckeditor-toolbar-group-name'))
+    // Focus on the "group name" input in the form.
+    .focus();
 }
 
-})(jQuery, Drupal, drupalSettings, CKEDITOR, _);
+/**
+ * Themes a blank CKEditor row.
+ *
+ * @return String
+ */
+Drupal.theme.ckeditorRow = function () {
+  return '<li class="ckeditor-row placeholder" role="group"><ul class="ckeditor-toolbar-groups clearfix"></ul></li>';
+};
+
+/**
+ * Themes a blank CKEditor button group.
+ *
+ * @return String
+ */
+Drupal.theme.ckeditorToolbarGroup = function () {
+  var group = '';
+  group += '<li class="ckeditor-toolbar-group placeholder" role="presentation" aria-label="' + Drupal.t('Place a button to create a new button group.') + '">';
+  group += '<h3 class="ckeditor-toolbar-group-name">' + Drupal.t('New group') + '</h3>';
+  group += '<ul class="ckeditor-buttons ckeditor-toolbar-group-buttons" role="toolbar" data-drupal-ckeditor-button-sorting="target"></ul>';
+  group += '</li>';
+  return group;
+};
+
+/**
+ * Themes a form for changing the title of a CKEditor button group.
+ *
+ * @return String
+ */
+Drupal.theme.ckeditorButtonGroupNameForm = function () {
+  return '<form><input name="group-name" required="required"></form>';
+};
+
+/**
+ * Themes a button that will toggle the button group names in active config.
+ *
+ * @return String
+ */
+Drupal.theme.ckeditorButtonGroupNamesToggle = function () {
+  return '<button class="ckeditor-groupnames-toggle" role="button" aria-pressed="false"></button>';
+};
+
+})(jQuery, Drupal, _, CKEDITOR);
diff --git a/core/modules/ckeditor/js/ckeditor.drupalimage.admin.js b/core/modules/ckeditor/js/ckeditor.drupalimage.admin.js
index ec093ab..c65cacc 100644
--- a/core/modules/ckeditor/js/ckeditor.drupalimage.admin.js
+++ b/core/modules/ckeditor/js/ckeditor.drupalimage.admin.js
@@ -19,7 +19,7 @@ Drupal.behaviors.ckeditorDrupalImageSettings = {
     $context
       .find('.ckeditor-toolbar-active')
       .on('CKEditorToolbarChanged.ckeditorDrupalImageSettings', function (e, action, button) {
-        if (button === 'DrupalImage') {
+        if (button === 'DrupalImage' && $drupalImageVerticalTab) {
           if (action === 'added') {
             $drupalImageVerticalTab.tabShow();
           }
diff --git a/core/modules/ckeditor/js/ckeditor.stylescombo.admin.js b/core/modules/ckeditor/js/ckeditor.stylescombo.admin.js
index 84e6e0a..441ae03 100644
--- a/core/modules/ckeditor/js/ckeditor.stylescombo.admin.js
+++ b/core/modules/ckeditor/js/ckeditor.stylescombo.admin.js
@@ -19,7 +19,7 @@ Drupal.behaviors.ckeditorStylesComboSettings = {
     $context
       .find('.ckeditor-toolbar-active')
       .on('CKEditorToolbarChanged.ckeditorStylesComboSettings', function (e, action, button) {
-        if (button === 'Styles') {
+        if (button === 'Styles' && $stylesComboVerticalTab) {
           if (action === 'added') {
             $stylesComboVerticalTab.tabShow();
           }
diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginManager.php b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginManager.php
index 636d82b..fb97d9b 100644
--- a/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginManager.php
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginManager.php
@@ -65,7 +65,14 @@ public function __construct(\Traversable $namespaces, CacheBackendInterface $cac
    */
   public function getEnabledPluginFiles(Editor $editor, $include_internal_plugins = FALSE) {
     $plugins = array_keys($this->getDefinitions());
-    $toolbar_buttons = array_unique(NestedArray::mergeDeepArray($editor->settings['toolbar']['buttons']));
+    // Flatten each row.
+    $toolbar_rows = array();
+    foreach ($editor->settings['toolbar']['rows'] as $row_number => $row) {
+      $toolbar_rows[] = array_reduce($editor->settings['toolbar']['rows'][$row_number], function (&$result, $button_group) {
+        return array_merge($result, $button_group['items']);
+      }, array());
+    }
+    $toolbar_buttons = array_unique(NestedArray::mergeDeepArray($toolbar_rows));
     $enabled_plugins = array();
     $additional_plugins = array();
 
diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalImageCaption.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalImageCaption.php
index 8fb20fe..3236690 100644
--- a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalImageCaption.php
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalImageCaption.php
@@ -76,12 +76,21 @@ function isEnabled(Editor $editor) {
     // Automatically enable this plugin if the text format associated with this
     // text editor uses the filter_caption filter and the DrupalImage button is
     // enabled.
+    // @todo, This parsing of the settings structure should be provided by a
+    // method on the CKEditor, but $editor here is not a CKEditor, it's a
+    // generic editor. I'm not sure how to get a reference to the CKEditor.
     if (isset($filters['filter_caption']) && $filters['filter_caption']->status) {
-      foreach ($editor->settings['toolbar']['buttons'] as $row) {
-        if (in_array('DrupalImage', $row)) {
-          return TRUE;
+      $enabled = FALSE;
+      foreach ($editor->settings['toolbar']['rows'] as $row) {
+        foreach ($row as $group) {
+          foreach ($group['items'] as $button) {
+            if ($button === 'DrupalImage') {
+              $enabled = TRUE;
+            }
+          }
         }
       }
+      return $enabled;
     }
 
     return FALSE;
diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/Internal.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/Internal.php
index 75a97bf..3f86178 100644
--- a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/Internal.php
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/Internal.php
@@ -55,7 +55,13 @@ public function getConfig(Editor $editor) {
     $config['allowedContent'] = $this->generateAllowedContentSetting($editor);
 
     // Add the format_tags setting, if its button is enabled.
-    $toolbar_buttons = array_unique(NestedArray::mergeDeepArray($editor->settings['toolbar']['buttons']));
+    $toolbar_rows = array();
+    foreach ($editor->settings['toolbar']['rows'] as $row_number => $row) {
+      $toolbar_rows[] = array_reduce($editor->settings['toolbar']['rows'][$row_number], function (&$result, $button_group) {
+        return array_merge($result, $button_group['items']);
+      }, array());
+    }
+    $toolbar_buttons = array_unique(NestedArray::mergeDeepArray($toolbar_rows));
     if (in_array('Format', $toolbar_buttons)) {
       $config['format_tags'] = $this->generateFormatTagsSetting($editor);
     }
@@ -220,17 +226,14 @@ public function getButtons() {
         'label' => t('Maximize'),
         'image_alternative' => $button('maximize'),
       ),
-      // No plugin, separator "buttons" for toolbar builder UI use only.
-      '|' => array(
-        'label' => t('Group separator'),
-        'image_alternative' => '<a href="#" role="button" aria-label="' . t('Button group separator') . '" class="ckeditor-group-separator"></a>',
-        'attributes' => array('class' => array('ckeditor-group-button-separator')),
-        'multiple' => TRUE,
-      ),
+      // No plugin, separator "button" for toolbar builder UI use only.
       '-' => array(
         'label' => t('Separator'),
         'image_alternative' => '<a href="#" role="button" aria-label="' . t('Button separator') . '" class="ckeditor-separator"></a>',
-        'attributes' => array('class' => array('ckeditor-button-separator')),
+        'attributes' => array(
+          'class' => array('ckeditor-button-separator'),
+          'data-drupal-ckeditor-type' => 'separator',
+        ),
         'multiple' => TRUE,
       ),
     );
diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php
index 44bdd5d..0b40886 100644
--- a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php
@@ -94,13 +94,29 @@ public static function create(ContainerInterface $container, array $configuratio
   public function getDefaultSettings() {
     return array(
       'toolbar' => array(
-        'buttons' => array(
+        'rows' => array(
+          // Button groups
           array(
-            'Bold', 'Italic',
-            '|', 'DrupalLink', 'DrupalUnlink',
-            '|', 'BulletedList', 'NumberedList',
-            '|', 'Blockquote', 'DrupalImage',
-            '|', 'Source',
+            array(
+              'name' => t('Formatting'),
+              'items' => array('Bold', 'Italic',),
+            ),
+            array(
+              'name' => t('Links'),
+              'items' => array('DrupalLink', 'DrupalUnlink',),
+            ),
+            array(
+              'name' => t('Lists'),
+              'items' => array('BulletedList', 'NumberedList',),
+            ),
+            array(
+              'name' => t('Media'),
+              'items' => array('Blockquote', 'DrupalImage',),
+            ),
+            array(
+              'name' => t('Tools'),
+              'items' => array('Source',),
+            ),
           ),
         ),
       ),
@@ -132,10 +148,11 @@ public function settingsForm(array $form, array &$form_state, EditorEntity $edit
       ),
       '#attributes' => array('class' => array('ckeditor-toolbar-configuration')),
     );
-    $form['toolbar']['buttons'] = array(
+
+    $form['toolbar']['button_groups'] = array(
       '#type' => 'textarea',
       '#title' => t('Toolbar buttons'),
-      '#default_value' => json_encode($editor->settings['toolbar']['buttons']),
+      '#default_value' => json_encode($editor->settings['toolbar']['rows']),
       '#attributes' => array('class' => array('ckeditor-toolbar-textarea')),
     );
 
@@ -174,8 +191,17 @@ public function settingsForm(array $form, array &$form_state, EditorEntity $edit
       'format' => $editor->id(),
       'editor' => 'ckeditor',
       'settings' => array(
-        // Single toolbar row that contains all existing buttons.
-        'toolbar' => array('buttons' => array(0 => $all_buttons)),
+        // Single toolbar row, single button group, all existing buttons.
+        'toolbar' => array(
+          'rows' => array(
+            0 => array(
+              0 => array(
+                'name' => 'All existing buttons',
+                'items' => $all_buttons,
+              )
+            )
+          ),
+        ),
         'plugins' => $editor->settings['plugins'],
       ),
     ));
@@ -209,7 +235,10 @@ public function settingsFormSubmit(array $form, array &$form_state) {
     // editor_form_filter_admin_format_submit().
     $toolbar_settings = &$form_state['values']['editor']['settings']['toolbar'];
 
-    $toolbar_settings['buttons'] = json_decode($toolbar_settings['buttons'], FALSE);
+    // The rows key is not built into the form structure, so decode the button
+    // groups data into this new key and remove the button_groups key.
+    $toolbar_settings['rows'] = json_decode($toolbar_settings['button_groups'], TRUE);
+    unset($toolbar_settings['button_groups']);
 
     // Remove the plugin settings' vertical tabs state; no need to save that.
     if (isset($form_state['values']['editor']['settings']['plugins'])) {
@@ -349,22 +378,12 @@ public function getLibraries(EditorEntity $editor) {
    */
   public function buildToolbarJSSetting(EditorEntity $editor) {
     $toolbar = array();
-    foreach ($editor->settings['toolbar']['buttons'] as $row) {
-      $button_group = array();
-      foreach ($row as $button_name) {
-        // Change the toolbar separators into groups.
-        if ($button_name === '|') {
-          $toolbar[] = $button_group;
-          $button_group = array();
-        }
-        else {
-          $button_group['items'][] = $button_name;
-        }
+    foreach ($editor->settings['toolbar']['rows'] as $row) {
+      foreach ($row as $group) {
+        $toolbar[] = $group;
       }
-      $toolbar[] = $button_group;
       $toolbar[] = '/';
     }
-
     return $toolbar;
   }
 
diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorAdminTest.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorAdminTest.php
index ce2e0e9..f984160 100644
--- a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorAdminTest.php
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorAdminTest.php
@@ -77,13 +77,29 @@ function testAdmin() {
     // Ensure the CKEditor editor returns the expected default settings.
     $expected_default_settings = array(
       'toolbar' => array(
-        'buttons' => array(
+        'rows' => array(
+          // Button groups
           array(
-            'Bold', 'Italic',
-            '|', 'DrupalLink', 'DrupalUnlink',
-            '|', 'BulletedList', 'NumberedList',
-            '|', 'Blockquote', 'DrupalImage',
-            '|', 'Source',
+            array(
+              'name' => t('Formatting'),
+              'items' => array('Bold', 'Italic',),
+            ),
+            array(
+              'name' => t('Links'),
+              'items' => array('DrupalLink', 'DrupalUnlink',),
+            ),
+            array(
+              'name' => t('Lists'),
+              'items' => array('BulletedList', 'NumberedList',),
+            ),
+            array(
+              'name' => t('Media'),
+              'items' => array('Blockquote', 'DrupalImage',),
+            ),
+            array(
+              'name' => t('Tools'),
+              'items' => array('Source',),
+            ),
           ),
         ),
       ),
@@ -98,8 +114,8 @@ function testAdmin() {
 
     // Ensure the toolbar buttons configuration value is initialized to the
     // expected default value.
-    $expected_buttons_value = json_encode($expected_default_settings['toolbar']['buttons']);
-    $this->assertFieldByName('editor[settings][toolbar][buttons]', $expected_buttons_value);
+    $expected_buttons_value = json_encode($expected_default_settings['toolbar']['rows']);
+    $this->assertFieldByName('editor[settings][toolbar][button_groups]', $expected_buttons_value);
 
     // Ensure the styles textarea exists and is initialized empty.
     $styles_textarea = $this->xpath('//textarea[@name="editor[settings][plugins][stylescombo][styles]"]');
@@ -131,12 +147,13 @@ function testAdmin() {
     // done via drag and drop, but here we can only emulate the end result of
     // that interaction). Test multiple toolbar rows and a divider within a row.
     $this->drupalGet('admin/config/content/formats/manage/filtered_html');
-    $expected_settings['toolbar']['buttons'] = array(
-      array('Undo', '|', 'Redo'),
-      array('JustifyCenter'),
+    $expected_settings['toolbar']['rows'][0][] = array(
+      'name' => 'Action history',
+      'items' => array('Undo', '|', 'Redo'),
+      array('JustifyCenter')
     );
     $edit = array(
-      'editor[settings][toolbar][buttons]' => json_encode($expected_settings['toolbar']['buttons']),
+      'editor[settings][toolbar][button_groups]' => json_encode($expected_settings['toolbar']['rows']),
     );
     $this->drupalPostForm(NULL, $edit, t('Save configuration'));
     $editor = entity_load('editor', 'filtered_html');
diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorPluginManagerTest.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorPluginManagerTest.php
index e376df8..db1c6a4 100644
--- a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorPluginManagerTest.php
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorPluginManagerTest.php
@@ -103,8 +103,8 @@ function testEnabledPlugins() {
     // cause the LlamaContextual and LlamaContextualAndButton plugins to be
     // enabled. Finally, we will add the "Strike" button back again, which would
     // cause all three plugins to be enabled.
-    $original_toolbar = $editor->settings['toolbar']['buttons'][0];
-    $editor->settings['toolbar']['buttons'][0][] = 'Llama';
+    $original_toolbar = $editor->settings['toolbar'];
+    $editor->settings['toolbar']['rows'][0][0]['items'][] = 'Llama';
     $editor->save();
     $file = array();
     $file['b'] = 'core/modules/ckeditor/tests/modules/js/llama_button.js';
@@ -113,13 +113,13 @@ function testEnabledPlugins() {
     $expected = $enabled_plugins + array('llama_button' => $file['b'], 'llama_contextual_and_button' => $file['cb']);
     $this->assertIdentical($expected, $this->manager->getEnabledPluginFiles($editor), 'The LlamaButton and LlamaContextualAndButton plugins are enabled.');
     $this->assertIdentical(array('internal' => NULL) + $expected, $this->manager->getEnabledPluginFiles($editor, TRUE), 'The LlamaButton and LlamaContextualAndButton plugins are enabled.');
-    $editor->settings['toolbar']['buttons'][0] = $original_toolbar;
-    $editor->settings['toolbar']['buttons'][0][] = 'Strike';
+    $editor->settings['toolbar'] = $original_toolbar;
+    $editor->settings['toolbar']['rows'][0][0]['items'][] = 'Strike';
     $editor->save();
     $expected = $enabled_plugins + array('llama_contextual' => $file['c'], 'llama_contextual_and_button' => $file['cb']);
     $this->assertIdentical($expected, $this->manager->getEnabledPluginFiles($editor), 'The  LLamaContextual and LlamaContextualAndButton plugins are enabled.');
     $this->assertIdentical(array('internal' => NULL) + $expected, $this->manager->getEnabledPluginFiles($editor, TRUE), 'The LlamaContextual and LlamaContextualAndButton plugins are enabled.');
-    $editor->settings['toolbar']['buttons'][0][] = 'Llama';
+    $editor->settings['toolbar']['rows'][0][0]['items'][] = 'Llama';
     $editor->save();
     $expected = $enabled_plugins + array('llama_button' => $file['b'], 'llama_contextual' => $file['c'], 'llama_contextual_and_button' => $file['cb']);
     $this->assertIdentical($expected, $this->manager->getEnabledPluginFiles($editor), 'The LlamaButton, LlamaContextual and LlamaContextualAndButton plugins are enabled.');
diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php
index 782d590..e4e821f 100644
--- a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php
@@ -109,12 +109,11 @@ function testGetJSSettings() {
     $this->container->get('plugin.manager.editor')->clearCachedDefinitions();
     $this->ckeditor = $this->container->get('plugin.manager.editor')->createInstance('ckeditor');
     $this->container->get('plugin.manager.ckeditor.plugin')->clearCachedDefinitions();
-    $editor->settings['toolbar']['buttons'][0][] = 'Strike';
-    $editor->settings['toolbar']['buttons'][1][] = 'Format';
+    $editor->settings['toolbar']['rows'][0][0]['items'][] = 'Strike';
+    $editor->settings['toolbar']['rows'][0][0]['items'][] = 'Format';
     $editor->save();
-    $expected_config['toolbar'][count($expected_config['toolbar'])-2]['items'][] = 'Strike';
-    $expected_config['toolbar'][]['items'][] = 'Format';
-    $expected_config['toolbar'][] = '/';
+    $expected_config['toolbar'][0]['items'][] = 'Strike';
+    $expected_config['toolbar'][0]['items'][] = 'Format';
     $expected_config['format_tags'] = 'p;h4;h5;h6';
     $expected_config['extraPlugins'] .= ',llama_contextual,llama_contextual_and_button';
     $expected_config['drupalExternalPlugins']['llama_contextual'] = file_create_url('core/modules/ckeditor/tests/modules/js/llama_contextual.js');
@@ -208,17 +207,20 @@ function testBuildToolbarJSSetting() {
     $this->assertIdentical($expected, $this->ckeditor->buildToolbarJSSetting($editor), '"toolbar" configuration part of JS settings built correctly for default toolbar.');
 
     // Customize the configuration.
-    $editor->settings['toolbar']['buttons'][0][] = 'Strike';
+    $editor->settings['toolbar']['rows'][0][0]['items'][] = 'Strike';
     $editor->save();
-    $expected[count($expected)-2]['items'][] = 'Strike';
+    $expected[0]['items'][] = 'Strike';
     $this->assertIdentical($expected, $this->ckeditor->buildToolbarJSSetting($editor), '"toolbar" configuration part of JS settings built correctly for customized toolbar.');
 
     // Enable the editor_test module, customize further.
     $this->enableModules(array('ckeditor_test'));
     $this->container->get('plugin.manager.ckeditor.plugin')->clearCachedDefinitions();
-    $editor->settings['toolbar']['buttons'][0][] = 'Llama';
+    // Override the label of a toolbar component.
+    $editor->settings['toolbar']['rows'][0][0]['name'] = 'JunkScience';
+    $editor->settings['toolbar']['rows'][0][0]['items'][] = 'Llama';
     $editor->save();
-    $expected[count($expected)-2]['items'][] = 'Llama';
+    $expected[0]['name'] = 'JunkScience';
+    $expected[0]['items'][] = 'Llama';
     $this->assertIdentical($expected, $this->ckeditor->buildToolbarJSSetting($editor), '"toolbar" configuration part of JS settings built correctly for customized toolbar with contrib module-provided CKEditor plugin.');
   }
 
@@ -253,7 +255,7 @@ function testInternalGetConfig() {
     $this->assertIdentical($expected, $internal_plugin->getConfig($editor), '"Internal" plugin configuration built correctly for default toolbar.');
 
     // Format dropdown/button enabled: new setting should be present.
-    $editor->settings['toolbar']['buttons'][0][] = 'Format';
+    $editor->settings['toolbar']['rows'][0][0]['items'][] = 'Format';
     $expected['format_tags'] = 'p;h4;h5;h6';
     $this->assertIdentical($expected, $internal_plugin->getConfig($editor), '"Internal" plugin configuration built correctly for customized toolbar.');
   }
@@ -266,7 +268,7 @@ function testStylesComboGetConfig() {
     $stylescombo_plugin = $this->container->get('plugin.manager.ckeditor.plugin')->createInstance('stylescombo');
 
     // Styles dropdown/button enabled: new setting should be present.
-    $editor->settings['toolbar']['buttons'][0][] = 'Styles';
+    $editor->settings['toolbar']['rows'][0][0]['items'][] = 'Styles';
     $editor->settings['plugins']['stylescombo']['styles'] = '';
     $editor->save();
     $expected['stylesSet'] = array();
@@ -367,12 +369,27 @@ protected function getDefaultAllowedContentConfig() {
 
   protected function getDefaultToolbarConfig() {
     return array(
-      0 => array('items' => array('Bold', 'Italic')),
-      1 => array('items' => array('DrupalLink', 'DrupalUnlink')),
-      2 => array('items' => array('BulletedList', 'NumberedList')),
-      3 => array('items' => array('Blockquote', 'DrupalImage')),
-      4 => array('items' => array('Source')),
-      5 => '/'
+      array(
+        'name' => t('Formatting'),
+        'items' => array('Bold', 'Italic',),
+      ),
+      array(
+        'name' => t('Links'),
+        'items' => array('DrupalLink', 'DrupalUnlink',),
+      ),
+      array(
+        'name' => t('Lists'),
+        'items' => array('BulletedList', 'NumberedList',),
+      ),
+      array(
+        'name' => t('Media'),
+        'items' => array('Blockquote', 'DrupalImage',),
+      ),
+      array(
+        'name' => t('Tools'),
+        'items' => array('Source',),
+      ),
+      '/',
     );
   }
 
diff --git a/core/modules/ckeditor/tests/modules/lib/Drupal/ckeditor_test/Plugin/CKEditorPlugin/LlamaContextual.php b/core/modules/ckeditor/tests/modules/lib/Drupal/ckeditor_test/Plugin/CKEditorPlugin/LlamaContextual.php
index e2fb4b9..d73f910 100644
--- a/core/modules/ckeditor/tests/modules/lib/Drupal/ckeditor_test/Plugin/CKEditorPlugin/LlamaContextual.php
+++ b/core/modules/ckeditor/tests/modules/lib/Drupal/ckeditor_test/Plugin/CKEditorPlugin/LlamaContextual.php
@@ -28,9 +28,11 @@ class LlamaContextual extends Llama implements CKEditorPluginContextualInterface
    */
   function isEnabled(Editor $editor) {
     // Automatically enable this plugin if the Underline button is enabled.
-    foreach ($editor->settings['toolbar']['buttons'] as $row) {
-      if (in_array('Strike', $row)) {
-        return TRUE;
+    foreach ($editor->settings['toolbar']['rows'] as $row) {
+      foreach ($row as $group) {
+        if (in_array('Strike', $group['items'])) {
+          return TRUE;
+        }
       }
     }
     return FALSE;
diff --git a/core/modules/ckeditor/tests/modules/lib/Drupal/ckeditor_test/Plugin/CKEditorPlugin/LlamaContextualAndButton.php b/core/modules/ckeditor/tests/modules/lib/Drupal/ckeditor_test/Plugin/CKEditorPlugin/LlamaContextualAndButton.php
index 87ec5a7..db6229e 100644
--- a/core/modules/ckeditor/tests/modules/lib/Drupal/ckeditor_test/Plugin/CKEditorPlugin/LlamaContextualAndButton.php
+++ b/core/modules/ckeditor/tests/modules/lib/Drupal/ckeditor_test/Plugin/CKEditorPlugin/LlamaContextualAndButton.php
@@ -31,9 +31,11 @@ class LlamaContextualAndButton extends Llama implements CKEditorPluginContextual
    */
   function isEnabled(Editor $editor) {
     // Automatically enable this plugin if the Strike button is enabled.
-    foreach ($editor->settings['toolbar']['buttons'] as $row) {
-      if (in_array('Strike', $row)) {
-        return TRUE;
+    foreach ($editor->settings['toolbar']['rows'] as $row) {
+      foreach ($row as $group) {
+        if (in_array('Strike', $group['items'])) {
+          return TRUE;
+        }
       }
     }
     return FALSE;
diff --git a/core/profiles/standard/config/editor.editor.basic_html.yml b/core/profiles/standard/config/editor.editor.basic_html.yml
index e340685..e321d30 100644
--- a/core/profiles/standard/config/editor.editor.basic_html.yml
+++ b/core/profiles/standard/config/editor.editor.basic_html.yml
@@ -2,21 +2,32 @@ format: basic_html
 editor: ckeditor
 settings:
   toolbar:
-    buttons:
+    rows:
       -
-        - Bold
-        - Italic
-        - '|'
-        - DrupalLink
-        - DrupalUnlink
-        - '|'
-        - BulletedList
-        - NumberedList
-        - '|'
-        - Blockquote
-        - DrupalImage
-        - '|'
-        - Source
+        -
+          name: Formatting
+          items:
+            - Bold
+            - Italic
+        -
+          name: Linking
+          items:
+            - DrupalLink
+            - DrupalUnlink
+        -
+          name: Lists
+          items:
+            - BulletedList
+            - NumberedList
+        -
+          name: Media
+          items:
+            - Blockquote
+            - DrupalImage
+        -
+          name: Tools
+          items:
+            - Source
   plugins:
     stylescombo:
       styles: ''
@@ -29,4 +40,4 @@ image_upload:
     width: ''
     height: ''
 status: '1'
-langcode: und
+langcode: en
diff --git a/core/profiles/standard/config/editor.editor.full_html.yml b/core/profiles/standard/config/editor.editor.full_html.yml
index eb01c1c..3a61cf8 100644
--- a/core/profiles/standard/config/editor.editor.full_html.yml
+++ b/core/profiles/standard/config/editor.editor.full_html.yml
@@ -2,31 +2,45 @@ format: full_html
 editor: ckeditor
 settings:
   toolbar:
-    buttons:
+    rows:
       -
-        - Bold
-        - Italic
-        - Strike
-        - Superscript
-        - Subscript
-        - -
-        - RemoveFormat
-        - '|'
-        - DrupalLink
-        - DrupalUnlink
-        - '|'
-        - BulletedList
-        - NumberedList
-        - '|'
-        - Blockquote
-        - DrupalImage
-        - Table
-        - HorizontalRule
-        - '|'
-        - Format
-        - '|'
-        - ShowBlocks
-        - Source
+        -
+          name: Formatting
+          items:
+            - Bold
+            - Italic
+            - Strike
+            - Superscript
+            - Subscript
+            - -
+            - RemoveFormat
+        -
+          name: Linking
+          items:
+            - DrupalLink
+            - DrupalUnlink
+        -
+          name: Lists
+          items:
+            - BulletedList
+            - NumberedList
+        -
+          name: Media
+          items:
+            - Blockquote
+            - DrupalImage
+            - Table
+            - HorizontalRule
+        -
+          name: Block Formatting
+          items:
+            - Format
+        -
+          name: Tools
+          items:
+            - ShowBlocks
+            - Source
+
   plugins:
     stylescombo:
       styles: ''
@@ -39,4 +53,4 @@ image_upload:
     width: ''
     height: ''
 status: '1'
-langcode: und
+langcode: en
