diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index 4230087..82e600e 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -1460,6 +1460,13 @@ function drupal_pre_render_table(array $element) {
  *       )
  *     );
  *     @endcode
+ *   - footer: An array containing the table footers. Each element of the array
+ *     can be either a localized string or an associative array with the
+ *     following keys:
+ *     - "data": The localized summary information of the table column.
+ *     - "header": Indicates this cell is a header.
+ *     - Any HTML attributes, such as "colspan", to apply to the column footer
+ *       cell.
  *   - attributes: An array of HTML attributes to apply to the table tag.
  *   - caption: A localized string to use for the <caption> tag.
  *   - colgroups: An array of column groups. Each element of the array can be
@@ -1548,53 +1555,75 @@ function template_preprocess_table(&$variables) {
   // Build an associative array of responsive classes keyed by column.
   $responsive_classes = array();
 
-  // Format the table header:
-  $ts = array();
-  if (!empty($variables['header'])) {
-    $ts = tablesort_init($variables['header']);
-
-    foreach ($variables['header'] as $col_key => $cell) {
-      if (!is_array($cell)) {
-        $cell_content = $cell;
-        $cell_attributes = new Attribute();
-        $is_header = TRUE;
-      }
-      else {
-        $cell_content = '';
-        if (isset($cell['data'])) {
-          $cell_content = $cell['data'];
-          unset($cell['data']);
+  // Format the table header & footer:
+  $table_parts = array('header' , 'footer');
+  foreach ($table_parts as $table_part) {
+    $ts = array();
+    if (!empty($variables[$table_part])) {
+      $ts = tablesort_init($variables[$table_part]);
+
+      foreach ($variables[$table_part] as $col_key => $cell) {
+        if (!is_array($cell)) {
+          $cell_content = $cell;
+          $cell_attributes = new Attribute();
+          if ($table_part == 'header') {
+            $is_header = TRUE;
+          } else {
+            $is_header = FALSE;
+          }
         }
-        // Flag the cell as a header or not and remove the flag.
-        $is_header = isset($cell['header']) ? $cell['header'] : TRUE;
-        unset($cell['header']);
-
-        // Track responsive classes for each column as needed. Only the header
-        // cells for a column are marked up with the responsive classes by a
-        // module developer or themer. The responsive classes on the header cells
-        // must be transferred to the content cells.
-        if (!empty($cell['class']) && is_array($cell['class'])) {
-          if (in_array(RESPONSIVE_PRIORITY_MEDIUM, $cell['class'])) {
-            $responsive_classes[$col_key] = RESPONSIVE_PRIORITY_MEDIUM;
+        else {
+          $cell_content = '';
+          if (isset($cell['data'])) {
+            $cell_content = $cell['data'];
+            unset($cell['data']);
           }
-          elseif (in_array(RESPONSIVE_PRIORITY_LOW, $cell['class'])) {
-            $responsive_classes[$col_key] = RESPONSIVE_PRIORITY_LOW;
+          if ($table_part == 'header') {
+            // Default header tag is <th>.
+            $cell_header_default = TRUE;
+          } else {
+            // Default header tag is <td>.
+            $cell_header_default = FALSE;
           }
-        }
+          // Flag the cell as a header or not and remove the flag.
+          $is_header = isset($cell['header']) ? $cell['header'] : $cell_header_default;
+          unset($cell['header']);
+
+          if ($table_part == 'header') {
+            // Track responsive classes for each column as needed. Only the header
+            // cells for a column are marked up with the responsive classes by a
+            // module developer or themer. The responsive classes on the header cells
+            // must be transferred to the content cells.
+            if (!empty($cell['class']) && is_array($cell['class'])) {
+              if (in_array(RESPONSIVE_PRIORITY_MEDIUM, $cell['class'])) {
+                $responsive_classes[$col_key] = RESPONSIVE_PRIORITY_MEDIUM;
+              }
+              elseif (in_array(RESPONSIVE_PRIORITY_LOW, $cell['class'])) {
+                $responsive_classes[$col_key] = RESPONSIVE_PRIORITY_LOW;
+              }
+            }
 
-        if (is_array($cell_content)) {
-          $cell_content = drupal_render($cell_content);
-        }
+            if (is_array($cell_content)) {
+              $cell_content = drupal_render($cell_content);
+            }
 
-        tablesort_header($cell_content, $cell, $variables['header'], $ts);
+            tablesort_header($cell_content, $cell, $variables['header'], $ts);
 
-        // tablesort_header() removes the 'sort' and 'field' keys.
-        $cell_attributes = new Attribute($cell);
+            // tablesort_header() removes the 'sort' and 'field' keys.
+            $cell_attributes = new Attribute($cell);
+          } else {
+            $cell_attributes = new Attribute($cell);
+
+            if (is_array($cell_content)) {
+              $cell_content = drupal_render($cell_content);
+            }
+          }
+        }
+        $variables[$table_part][$col_key] = array();
+        $variables[$table_part][$col_key]['tag'] = $is_header ? 'th' : 'td';
+        $variables[$table_part][$col_key]['attributes'] = $cell_attributes;
+        $variables[$table_part][$col_key]['content'] = $cell_content;
       }
-      $variables['header'][$col_key] = array();
-      $variables['header'][$col_key]['tag'] = $is_header ? 'th' : 'td';
-      $variables['header'][$col_key]['attributes'] = $cell_attributes;
-      $variables['header'][$col_key]['content'] = $cell_content;
     }
   }
 
@@ -2553,7 +2582,7 @@ function drupal_common_theme() {
       'template' => 'breadcrumb',
     ),
     'table' => array(
-      'variables' => array('header' => NULL, 'rows' => NULL, 'attributes' => array(), 'caption' => NULL, 'colgroups' => array(), 'sticky' => FALSE, 'responsive' => TRUE, 'empty' => ''),
+      'variables' => array('header' => NULL, 'footer' => NULL, 'rows' => NULL, 'attributes' => array(), 'caption' => NULL, 'colgroups' => array(), 'sticky' => FALSE, 'responsive' => TRUE, 'empty' => ''),
       'template' => 'table',
     ),
     'tablesort_indicator' => array(
diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/TableTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/TableTest.php
index 6b9c5ef..6457309 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Theme/TableTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Theme/TableTest.php
@@ -31,71 +31,67 @@ public static function getInfo() {
   }
 
   /**
-   * Tableheader.js provides 'sticky' table headers, and is included by default.
+   * Tests that the table header is printed correctly even if there are no rows,
+   * and that the empty text is displayed correctly.
    */
-  function testThemeTableStickyHeaders() {
-    $header = array('one', 'two', 'three');
-    $rows = array(array(1,2,3), array(4,5,6), array(7,8,9));
-    $table = array(
-      '#type' => 'table',
-      '#header' => $header,
-      '#rows' => $rows,
-      '#sticky' => TRUE,
+  function testThemeTableWithEmptyMessage() {
+    $header = array(
+      'Header 1',
+      array(
+        'data' => 'Header 2',
+        'colspan' => 2,
+      ),
     );
-    $this->render($table);
-    $js = _drupal_add_js();
-    $this->assertTrue(isset($js['core/misc/tableheader.js']), 'tableheader.js found.');
-    $this->assertRaw('sticky-enabled');
-    drupal_static_reset('_drupal_add_js');
-  }
-
-  /**
-   * If $sticky is FALSE, no tableheader.js should be included.
-   */
-  function testThemeTableNoStickyHeaders() {
-    $header = array('one', 'two', 'three');
-    $rows = array(array(1,2,3), array(4,5,6), array(7,8,9));
-    $attributes = array();
-    $caption = NULL;
-    $colgroups = array();
     $table = array(
       '#type' => 'table',
       '#header' => $header,
-      '#rows' => $rows,
-      '#attributes' => $attributes,
-      '#caption' => $caption,
-      '#colgroups' => $colgroups,
-      '#sticky' => FALSE,
+      '#rows' => array(),
+      '#empty' => 'Empty row.',
     );
     $this->render($table);
-    $js = _drupal_add_js();
-    $this->assertFalse(isset($js['core/misc/tableheader.js']), 'tableheader.js not found.');
-    $this->assertNoRaw('sticky-enabled');
-    drupal_static_reset('_drupal_add_js');
+    $this->removeWhiteSpace();
+    $this->assertRaw('<thead><tr><th>Header 1</th><th colspan="2">Header 2</th></tr>', 'Table header found.');
+    $this->assertRaw('<tr class="odd"><td colspan="3" class="empty message">Empty row.</td>', 'Colspan on #empty row found.');
   }
 
   /**
-   * Tests that the table header is printed correctly even if there are no rows,
-   * and that the empty text is displayed correctly.
+   * Tests that the table header and footer is printed correctly..
    */
-  function testThemeTableWithEmptyMessage() {
+  function testThemeTableHeaderAndFooter() {
     $header = array(
-      'Header 1',
+      array(
+        'data' => 'Header 1',
+        'header' => FALSE,
+      ),
       array(
         'data' => 'Header 2',
         'colspan' => 2,
       ),
+      'Header 3',
+    );
+    $footer = array(
+      array(
+        'data' => 'Footer 1',
+        'header' => TRUE,
+      ),
+      array(
+        'data' => 'Footer 2',
+        'colspan' => 2,
+      ),
+      'Footer 3',
     );
     $table = array(
       '#type' => 'table',
       '#header' => $header,
+      '#footer' => $footer,
       '#rows' => array(),
       '#empty' => 'Empty row.',
     );
     $this->render($table);
     $this->removeWhiteSpace();
-    $this->assertRaw('<thead><tr><th>Header 1</th><th colspan="2">Header 2</th></tr>', 'Table header found.');
-    $this->assertRaw('<tr class="odd"><td colspan="3" class="empty message">Empty row.</td>', 'Colspan on #empty row found.');
+    $this->assertRaw('<thead><tr><td>Header 1</td><th colspan="2">Header 2</th><th>Header 3</th></tr>', 'Table header found.');
+    $this->assertRaw('<tfoot><tr><th>Footer 1</th><td colspan="2">Footer 2</td><td>Footer 3</td></tr>', 'Table footer found.');
+    $this->assertRaw('<tr class="odd"><td colspan="4" class="empty message">Empty row.</td>', 'Colspan on #empty row found.');
   }
 
   /**
diff --git a/core/modules/system/templates/table.html.twig b/core/modules/system/templates/table.html.twig
index ce69286..474d6db 100644
--- a/core/modules/system/templates/table.html.twig
+++ b/core/modules/system/templates/table.html.twig
@@ -28,6 +28,10 @@
  *     - attributes: Any HTML attributes, such as "colspan", to apply to the
  *       table cell.
  *     - content: The string to display in the table cell.
+ * - footer: Table footer cells. Each cell contains the following properties:
+ *   - tag: The HTML tag name to use; either TH or TD.
+ *   - attributes: HTML attributes to apply to the tag.
+ *   - content: A localized string for the title of the column.
  * - empty: The message to display in an extra row if table does not have
  *   any rows.
  *
@@ -40,7 +44,6 @@
   {% if caption %}
     <caption>{{ caption }}</caption>
   {% endif %}
-
   {% for colgroup in colgroups %}
     {% if colgroup.cols %}
       <colgroup{{ colgroup.attributes }}>
@@ -52,7 +55,6 @@
       <colgroup{{ colgroup.attributes }} />
     {% endif %}
   {% endfor %}
-
   {% if header %}
     <thead>
       <tr>
@@ -64,7 +66,6 @@
       </tr>
     </thead>
   {% endif %}
-
   {% if rows %}
     <tbody>
       {% for row in rows %}
@@ -78,4 +79,15 @@
       {% endfor %}
     </tbody>
   {% endif %}
+  {% if footer %}
+    <tfoot>
+      <tr>
+        {% for cell in footer %}
+          <{{ cell.tag }}{{ cell.attributes }}>
+            {{- cell.content -}}
+          </{{ cell.tag }}>
+        {% endfor %}
+      </tr>
+    </tfoot>
+  {% endif %}
 </table>
