diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/field/FieldPluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/field/FieldPluginBase.php
index 0289031..08310f0 100644
--- a/core/modules/views/lib/Drupal/views/Plugin/views/field/FieldPluginBase.php
+++ b/core/modules/views/lib/Drupal/views/Plugin/views/field/FieldPluginBase.php
@@ -9,6 +9,7 @@
 
 use Drupal\views\Plugin\views\HandlerBase;
 use Drupal\views\Plugin\views\display\DisplayPluginBase;
+use Drupal\views\Plugin\views\style\StylePluginBase;
 use Drupal\views\ViewExecutable;
 
 /**
@@ -264,27 +265,6 @@ public function elementWrapperType($none_supported = FALSE, $default_empty = FAL
   }
 
   /**
-   * Provide a list of elements valid for field HTML.
-   *
-   * This function can be overridden by fields that want more or fewer
-   * elements available, though this seems like it would be an incredibly
-   * rare occurence.
-   */
-  public function getElements() {
-    static $elements = NULL;
-    if (!isset($elements)) {
-      // @todo Add possible html5 elements.
-      $elements = array(
-        '' => t(' - Use default -'),
-        '0' => t('- None -')
-      );
-      $elements += config('views.settings')->get('field_rewrite_elements');
-    }
-
-    return $elements;
-  }
-
-  /**
    * Return the class of the field.
    */
   public function elementClasses($row_index = NULL) {
@@ -547,7 +527,7 @@ public function buildOptionsForm(&$form, &$form_state) {
     );
     $form['element_type'] = array(
       '#title' => t('HTML element'),
-      '#options' => $this->getElements(),
+      '#options' => StylePluginBase::getHtmlElementTypes(),
       '#type' => 'select',
       '#default_value' => $this->options['element_type'],
       '#description' => t('Choose the HTML element to wrap around this field, e.g. H1, H2, etc.'),
@@ -592,7 +572,7 @@ public function buildOptionsForm(&$form, &$form_state) {
     );
     $form['element_label_type'] = array(
       '#title' => t('Label HTML element'),
-      '#options' => $this->getElements(FALSE),
+      '#options' => StylePluginBase::getHtmlElementTypes(FALSE),
       '#type' => 'select',
       '#default_value' => $this->options['element_label_type'],
       '#description' => t('Choose the HTML element to wrap around this label, e.g. H1, H2, etc.'),
@@ -636,7 +616,7 @@ public function buildOptionsForm(&$form, &$form_state) {
     );
     $form['element_wrapper_type'] = array(
       '#title' => t('Wrapper HTML element'),
-      '#options' => $this->getElements(FALSE),
+      '#options' => StylePluginBase::getHtmlElementTypes(FALSE),
       '#type' => 'select',
       '#default_value' => $this->options['element_wrapper_type'],
       '#description' => t('Choose the HTML element to wrap around this field and label, e.g. H1, H2, etc. This may not be used if the field and label are not rendered together, such as with a table.'),
diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/style/StylePluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/style/StylePluginBase.php
index e271d8e..07e6009 100644
--- a/core/modules/views/lib/Drupal/views/Plugin/views/style/StylePluginBase.php
+++ b/core/modules/views/lib/Drupal/views/Plugin/views/style/StylePluginBase.php
@@ -9,6 +9,7 @@
 
 use Drupal\views\Plugin\views\PluginBase;
 use Drupal\views\Plugin\views\display\DisplayPluginBase;
+use Drupal\views\Plugin\views\field\FieldPluginBase;
 use Drupal\views\Plugin\views\wizard\WizardInterface;
 use Drupal\Component\Annotation\Plugin;
 use Drupal\Core\Annotation\Translation;
@@ -231,6 +232,7 @@ public function evenEmpty() {
   protected function defineOptions() {
     $options = parent::defineOptions();
     $options['grouping'] = array('default' => array());
+    $options['title_element_type'] = array('default' => 'h3');
     if ($this->usesRowClass()) {
       $options['row_class'] = array('default' => '');
       $options['default_row_class'] = array('default' => TRUE, 'bool' => TRUE);
@@ -299,6 +301,20 @@ public function buildOptionsForm(&$form, &$form_state) {
           );
         }
       }
+
+      $form['title_element_type'] = array(
+        '#title' => t('Title HTML Element'),
+        '#type' => 'select',
+        '#default_value' => $this->options['title_element_type'],
+        '#options' => static::getHtmlElementTypes(),
+        '#description' => t('Choose the HTML element to wrap around the grouping title of a view.'),
+        // Hide the element unless at least one grouping field is configured.
+        '#states' => array(
+          'invisible' => array(
+            ':input[name="style_options[grouping][0][field]"]' => array('value' => ''),
+          ),
+        ),
+      );
     }
 
     if ($this->usesRowClass()) {
@@ -381,6 +397,27 @@ public function wizardSubmit(&$form, &$form_state, WizardInterface $wizard, &$di
   }
 
   /**
+   * Provide a list of elements valid for field HTML.
+   *
+   * This function can be overridden by fields that want more or fewer
+   * elements available, though this seems like it would be an incredibly
+   * rare occurence.
+   */
+  public static function getHtmlElementTypes() {
+    static $elements = NULL;
+    if (!isset($elements)) {
+      // @todo Add possible html5 elements.
+      $elements = array(
+        '' => t(' - Use default -'),
+        '0' => t('- None -')
+      );
+      $elements += \Drupal::config('views.settings')->get('field_rewrite_elements');
+    }
+
+    return $elements;
+  }
+
+  /**
    * Called by the view builder to see if this style handler wants to
    * interfere with the sorts. If so it should build; if it returns
    * any non-TRUE value, normal sorting will NOT be added to the query.
@@ -469,6 +506,7 @@ public function renderGroupingSets($sets, $level = 0) {
           '#grouping_level' => $level,
           '#rows' => $set['rows'],
           '#title' => $set['group'],
+          '#title_element_type' => $this->options['title_element_type'],
         );
       }
       // Render as a record set.
@@ -485,6 +523,7 @@ public function renderGroupingSets($sets, $level = 0) {
         $single_output = $this->renderRowGroup($set['rows']);
         $single_output['#grouping_level'] = $level;
         $single_output['#title'] = $set['group'];
+        $single_output['#title_element_type'] = $this->options['title_element_type'];
         $output[] = $single_output;
       }
     }
diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/style/Table.php b/core/modules/views/lib/Drupal/views/Plugin/views/style/Table.php
index 178af9f..f0f908e 100644
--- a/core/modules/views/lib/Drupal/views/Plugin/views/style/Table.php
+++ b/core/modules/views/lib/Drupal/views/Plugin/views/style/Table.php
@@ -99,6 +99,9 @@ protected function defineOptions() {
     $options['description'] = array('default' => '', 'translatable' => TRUE);
     $options['empty_table'] = array('default' => FALSE, 'bool' => TRUE);
 
+    // Override to set the title to caption by default.
+    $options['title_element_type']['default'] = 'caption';
+
     return $options;
   }
 
diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/FieldWebTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/FieldWebTest.php
index 6d73636..fe27e2f 100644
--- a/core/modules/views/lib/Drupal/views/Tests/Handler/FieldWebTest.php
+++ b/core/modules/views/lib/Drupal/views/Tests/Handler/FieldWebTest.php
@@ -405,7 +405,7 @@ public function testFieldClasses() {
     }
 
     // Tests the available html elements.
-    $element_types = $id_field->getElements();
+    $element_types = $id_field->getHTMLElementTypes();
     $expected_elements = array(
       '',
       0,
diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/StyleTest.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/StyleTest.php
index 25a10f3..8fcfd9a 100644
--- a/core/modules/views/lib/Drupal/views/Tests/Plugin/StyleTest.php
+++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/StyleTest.php
@@ -249,6 +249,33 @@ function testCustomRowClasses() {
   }
 
   /**
+   * Tests the custom element type for group titles.
+   */
+  public function testCustomElementTitle() {
+    $style_plugins = array('default', 'grid', 'table', 'html_list');
+    $view = views_get_view('test_view');
+
+    foreach ($style_plugins as $plugin) {
+      $view->initDisplay();
+
+      $style = $view->display_handler->getOption('style');
+      $style['type'] = $plugin;
+      $style['options']['title_element_type'] = 'h6';
+      $style['options']['grouping'] = 'name';
+      $view->display_handler->overrideOption('style', $style);
+
+      $view->initStyle();
+
+      $output = $view->preview();
+      $this->drupalSetContent(drupal_render($output));
+      $elements = $this->xpath('//h6');
+      $this->assertEqual(count($elements), 5, format_string('Custom element tag found for @plugin plugin', array('@plugin' => $plugin)));
+
+      $view->destroy();
+    }
+  }
+
+  /**
    * Stores a view output in the elements.
    */
   protected function storeViewPreview($output) {
diff --git a/core/modules/views/templates/views-view-grid.html.twig b/core/modules/views/templates/views-view-grid.html.twig
index 8b63792..4cb77a8 100644
--- a/core/modules/views/templates/views-view-grid.html.twig
+++ b/core/modules/views/templates/views-view-grid.html.twig
@@ -6,6 +6,7 @@
  * Available variables:
  * - attributes: HTML attributes for the table element.
  * - title: The title of this group of rows.
+ * - title_element_type: The HTML tag element for the title.
  * - rows: A list of rows. Each row contains a list of columns.
  * - row_classes: HTML classes for each row including the row number and first
  *   or last.
@@ -18,7 +19,7 @@
  */
 #}
 {% if title %}
-  <h3>{{ title }}</h3>
+  <{{ title_element_type }}>{{ title }}</{{ title_element_type }}>
 {% endif %}
 <table{{ attributes }}>
   <tbody>
diff --git a/core/modules/views/templates/views-view-list.html.twig b/core/modules/views/templates/views-view-list.html.twig
index 364ab2e..adfb6aa 100644
--- a/core/modules/views/templates/views-view-list.html.twig
+++ b/core/modules/views/templates/views-view-list.html.twig
@@ -7,6 +7,7 @@
  * - rows: A list of rows for this list.
  * - row_classes: The row's HTML attributes correlating with the row's 'id'.
  * - title: The title of this group of rows. May be empty.
+ * - title_element_type: The HTML tag element for the title.
  * - list: @todo.
  *   - type: Starting tag will be either a ul or ol.
  *   - attributes: HTML attributes for the list element.
@@ -20,7 +21,7 @@
   <div{{ wrapper_attributes }}>
 {% endif %}
   {% if title %}
-    <h3>{{ title }}</h3>
+    <{{ title_element_type }}>{{ title }}</{{ title_element_type }}>
   {% endif %}
 
   {% if list.type == 'ul' %}
diff --git a/core/modules/views/templates/views-view-table.html.twig b/core/modules/views/templates/views-view-table.html.twig
index 2242c0b..7511a87 100644
--- a/core/modules/views/templates/views-view-table.html.twig
+++ b/core/modules/views/templates/views-view-table.html.twig
@@ -7,6 +7,7 @@
  * - attributes: Remaining HTML attributes for the element.
  *   - class: HTML classes that can be used to style contextually through CSS.
  * - title : The title of this group of rows.
+ * - title_element_type: The HTML tag element for the title.
  * - header: Header labels.
  * - header_classes: HTML classes to apply to each header cell, indexed by
  *   the header's key.
@@ -34,7 +35,7 @@
     {% if caption %}
       {{ caption }}
     {% else %}
-      {{ title }}
+      <{{ title_element_type }}>{{ title }}</{{ title_element_type }}>
     {% endif %}
     {% if (summary is not empty) or (description is not empty) %}
       <details>
diff --git a/core/modules/views/templates/views-view-unformatted.html.twig b/core/modules/views/templates/views-view-unformatted.html.twig
index e1c820f..ca701ce 100644
--- a/core/modules/views/templates/views-view-unformatted.html.twig
+++ b/core/modules/views/templates/views-view-unformatted.html.twig
@@ -5,6 +5,7 @@
  *
  * Available variables:
  * - title: The title of this group of rows. May be empty.
+ * - title_element_type: The HTML tag element for the title.
  * - rows: A list of the view's row items.
  * - row_classes: A list of row class attributes keyed by the row's ID.
  *
@@ -14,7 +15,7 @@
  */
 #}
 {% if title %}
-  <h3>{{ title }}</h3>
+  <{{ title_element_type }}>{{ title }}</{{ title_element_type }}>
 {% endif %}
 {% for id, row in rows %}
   <div{{ row_classes[id] }}>
diff --git a/core/modules/views/views.module b/core/modules/views/views.module
index 85aecf9..2730153 100644
--- a/core/modules/views/views.module
+++ b/core/modules/views/views.module
@@ -99,7 +99,7 @@ function views_theme($existing, $type, $theme, $path) {
     // $view is an object but the core contextual_preprocess() function only
     // attaches contextual links when the primary theme argument is an array.
     'display' => array('view_array' => array(), 'view' => NULL),
-    'style' => array('view' => NULL, 'options' => NULL, 'rows' => NULL, 'title' => NULL),
+    'style' => array('view' => NULL, 'options' => NULL, 'rows' => NULL, 'title' => NULL, 'title_element_type' => NULL),
     'row' => array('view' => NULL, 'options' => NULL, 'row' => NULL, 'field_alias' => NULL),
     'exposed_form' => array('view' => NULL, 'options' => NULL),
     'pager' => array(
diff --git a/core/modules/views/views.theme.inc b/core/modules/views/views.theme.inc
index 444633b..f2e6974 100644
--- a/core/modules/views/views.theme.inc
+++ b/core/modules/views/views.theme.inc
@@ -722,7 +722,7 @@ function template_preprocess_views_view_table(&$vars) {
 
   $vars['summary'] = $handler->options['summary'];
   $vars['description'] = $handler->options['description'];
-  $vars['caption_needed'] |= !empty($vars['summary']) || !empty($vars['description']);
+  $vars['caption_needed'] |= !empty($vars['summary']) || !empty($vars['description']) || !empty($vars['title']);
 
   // If the table has headers and it should react responsively to columns hidden
   // with the classes represented by the constants RESPONSIVE_PRIORITY_MEDIUM
@@ -828,7 +828,7 @@ function template_preprocess_views_view_grid(&$vars) {
         $row_classes['class'][] = 'row-last';
       }
     }
-    $row_classes = new Attribute($row_classes);
+    $vars['row_classes'][] = new Attribute($row_classes);
 
     foreach ($rows[$row_number] as $column_number => $item) {
       $vars['column_classes'][$row_number][$column_number] = array();
