diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index 4cc6424..d2ee056 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -1712,6 +1712,31 @@ function _field_multiple_value_form_sort_helper($a, $b) {
   return $a_weight - $b_weight;
 }
 
+
+/**
+ * Prepares variables for html_tag element templates.
+ *
+ * Default template: html-tag.html.twig.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #attributes, #children, #value, #void_element.
+ */
+function template_preprocess_html_tag(&$variables) {
+  $element = $variables['element'];
+
+  $variables['tag'] = $element['#tag'];
+  $variables['attributes'] = $element['#attributes'];
+  $variables['children'] = (isset($element['#children'])) ? $element['#children'] : '';
+  $variables['value'] = (isset($element['#value'])) ? $element['#value'] : '';
+  $variables['void_element'] = (isset($element['#void_element'])) ? $element['#void_element'] : FALSE;
+  $variables['noscript'] = (isset($element['#noscript']) && $element['#noscript']) ? $element['#noscript'] : FALSE;
+
+  // Suppress error messages.
+  $variables['errors'] = NULL;
+}
+
 /**
  * Provides theme registration for themes across .inc files.
  */
@@ -1788,6 +1813,9 @@ function drupal_common_theme() {
     'indentation' => [
       'variables' => ['size' => 1],
     ],
+    'html_tag' => [
+      'render element' => 'element',
+    ],
     // From theme.maintenance.inc.
     'maintenance_page' => [
       'render element' => 'page',
diff --git a/core/lib/Drupal/Core/Render/Element/HtmlTag.php b/core/lib/Drupal/Core/Render/Element/HtmlTag.php
index 0b9ac91..7b0c1b7 100644
--- a/core/lib/Drupal/Core/Render/Element/HtmlTag.php
+++ b/core/lib/Drupal/Core/Render/Element/HtmlTag.php
@@ -54,7 +54,9 @@ public function getInfo() {
         [$class, 'preRenderHtmlTag'],
       ],
       '#attributes' => [],
+      '#children' => NULL,
       '#value' => NULL,
+      '#theme_wrappers' => ['html_tag'],
     ];
   }
 
@@ -72,6 +74,7 @@ public function getInfo() {
    *     tag. The attributes are escaped, see \Drupal\Core\Template\Attribute.
    *   - #value: (optional) A string containing tag content, such as inline
    *     CSS. The value of #value will be XSS admin filtered if it is not safe.
+   *   - #children: (option) An array containing unrendered children tags.
    *   - #noscript: (optional) If TRUE, the markup (including any prefix or
    *     suffix) will be wrapped in a <noscript> element. (Note that passing
    *     any non-empty value here will add the <noscript> tag.)
@@ -79,26 +82,8 @@ public function getInfo() {
    * @return array
    */
   public static function preRenderHtmlTag($element) {
-    $attributes = isset($element['#attributes']) ? new Attribute($element['#attributes']) : '';
+    $element['#void_element'] = in_array($element['#tag'], self::$voidElements);
 
-    // An HTML tag should not contain any special characters. Escape them to
-    // ensure this cannot be abused.
-    $escaped_tag = HtmlUtility::escape($element['#tag']);
-    $markup = '<' . $escaped_tag . $attributes;
-    // Construct a void element.
-    if (in_array($element['#tag'], self::$voidElements)) {
-      $markup .= " />\n";
-    }
-    // Construct all other elements.
-    else {
-      $markup .= '>';
-      $markup .= $element['#value'] instanceof MarkupInterface ? $element['#value'] : Xss::filterAdmin($element['#value']);
-      $markup .= '</' . $escaped_tag . ">\n";
-    }
-    if (!empty($element['#noscript'])) {
-      $markup = "<noscript>$markup</noscript>";
-    }
-    $element['#markup'] = Markup::create($markup);
     return $element;
   }
 
diff --git a/core/modules/system/templates/html-tag.html.twig b/core/modules/system/templates/html-tag.html.twig
new file mode 100644
index 0000000..fa5dd16
--- /dev/null
+++ b/core/modules/system/templates/html-tag.html.twig
@@ -0,0 +1,37 @@
+{#
+/**
+ * @file
+ * Default theme implementation for a generic 'html_tag' element.
+ *
+ * Available variables
+ * - attributes: A list of HTML attributes for the wrapper element.
+ * - children: The child elements of the tag.
+ * - noscript: Boolean indicating if tag should be wrapped with <noscript>.
+ * - tag: The tag name.
+ * - value: The contents of the tag.
+ * - void_element: Boolean indicating if tag is a void element.
+ *
+ * @see template_preprocess_form()
+ *
+ * @ingroup themeable
+ */
+#}
+{% if noscript %}
+ <noscript>
+{% endif %}
+
+<{{ tag }}{{ attributes }}{% if void_element %}
+ />
+{% else %}
+>
+{% endif %}
+
+{% if not void_element %}
+    {{ value }}
+    {{ children }}
+  </{{ tag }}>
+{% endif %}
+
+{% if noscript %}
+ </noscript>
+{% endif %}
diff --git a/core/tests/Drupal/Tests/Core/Render/Element/HtmlTagTest.php b/core/tests/Drupal/Tests/Core/Render/Element/HtmlTagTest.php
index 46dd91a..3319698 100644
--- a/core/tests/Drupal/Tests/Core/Render/Element/HtmlTagTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/Element/HtmlTagTest.php
@@ -20,6 +20,7 @@ public function testGetInfo() {
     $info = $htmlTag->getInfo();
     $this->assertArrayHasKey('#pre_render', $info);
     $this->assertArrayHasKey('#attributes', $info);
+    $this->assertArrayHasKey('#children', $info);
     $this->assertArrayHasKey('#value', $info);
   }
 
@@ -29,8 +30,8 @@ public function testGetInfo() {
    */
   public function testPreRenderHtmlTag($element, $expected) {
     $result = HtmlTag::preRenderHtmlTag($element);
-    $this->assertArrayHasKey('#markup', $result);
-    $this->assertEquals($expected, $result['#markup']);
+    $this->assertArrayHasKey('#void_element', $result);
+    $this->assertEquals($expected, $result['#void_element']);
   }
 
   /**
@@ -39,58 +40,25 @@ public function testPreRenderHtmlTag($element, $expected) {
   public function providerPreRenderHtmlTag() {
     $tags = [];
 
-    // Value prefix/suffix.
+    // Normal element with a value should not result in a void element.
     $element = [
       '#value' => 'value',
       '#tag' => 'p',
     ];
-    $tags[] = [$element, '<p>value</p>' . "\n"];
+    $tags[] = [$element, FALSE];
 
     // Normal element without a value should not result in a void element.
     $element = [
       '#tag' => 'p',
       '#value' => NULL,
     ];
-    $tags[] = [$element, "<p></p>\n"];
+    $tags[] = [$element, FALSE];
 
     // A void element.
     $element = [
       '#tag' => 'br',
     ];
-    $tags[] = [$element, "<br />\n"];
-
-    // Attributes.
-    $element = [
-      '#tag' => 'div',
-      '#attributes' => ['class' => 'test', 'id' => 'id'],
-      '#value' => 'value',
-    ];
-    $tags[] = [$element, '<div class="test" id="id">value</div>' . "\n"];
-
-    // No script tags.
-    $element['#noscript'] = TRUE;
-    $tags[] = [$element, '<noscript><div class="test" id="id">value</div>' . "\n" . '</noscript>'];
-
-    // Ensure that #tag is sanitised.
-    $element = [
-      '#tag' => 'p><script>alert()</script><p',
-      '#value' => 'value',
-    ];
-    $tags[] = [$element, "<p&gt;&lt;script&gt;alert()&lt;/script&gt;&lt;p>value</p&gt;&lt;script&gt;alert()&lt;/script&gt;&lt;p>\n"];
-
-    // Ensure that #value is not filtered if it is marked as safe.
-    $element = [
-      '#tag' => 'p',
-      '#value' => Markup::create('<script>value</script>'),
-    ];
-    $tags[] = [$element, "<p><script>value</script></p>\n"];
-
-    // Ensure that #value is filtered if it is not safe.
-    $element = [
-      '#tag' => 'p',
-      '#value' => '<script>value</script>',
-    ];
-    $tags[] = [$element, "<p>value</p>\n"];
+    $tags[] = [$element, TRUE];
 
     return $tags;
   }
diff --git a/core/themes/stable/templates/misc/html-tag.html.twig b/core/themes/stable/templates/misc/html-tag.html.twig
new file mode 100644
index 0000000..fa5dd16
--- /dev/null
+++ b/core/themes/stable/templates/misc/html-tag.html.twig
@@ -0,0 +1,37 @@
+{#
+/**
+ * @file
+ * Default theme implementation for a generic 'html_tag' element.
+ *
+ * Available variables
+ * - attributes: A list of HTML attributes for the wrapper element.
+ * - children: The child elements of the tag.
+ * - noscript: Boolean indicating if tag should be wrapped with <noscript>.
+ * - tag: The tag name.
+ * - value: The contents of the tag.
+ * - void_element: Boolean indicating if tag is a void element.
+ *
+ * @see template_preprocess_form()
+ *
+ * @ingroup themeable
+ */
+#}
+{% if noscript %}
+ <noscript>
+{% endif %}
+
+<{{ tag }}{{ attributes }}{% if void_element %}
+ />
+{% else %}
+>
+{% endif %}
+
+{% if not void_element %}
+    {{ value }}
+    {{ children }}
+  </{{ tag }}>
+{% endif %}
+
+{% if noscript %}
+ </noscript>
+{% endif %}
