diff --git a/includes/webform.theme.inc b/includes/webform.theme.inc
index 8a65c7f5..9d61d9e4 100644
--- a/includes/webform.theme.inc
+++ b/includes/webform.theme.inc
@@ -80,6 +80,9 @@ function webform_theme() {
     'webform_submission_information' => [
       'variables' => ['webform_submission' => NULL, 'source_entity' => NULL, 'open' => TRUE],
     ],
+    'webform_submission_data' => [
+      'render element' => 'elements',
+    ],
 
     'webform_element_base_html' => [
       'variables' => ['element' => [], 'value' => NULL, 'webform_submission' => NULL, 'options' => []],
@@ -917,22 +920,22 @@ function webform_theme_suggestions_webform_preview(array $variables) {
 /**
  * Implements hook_theme_suggestions_HOOK().
  */
-function webform_theme_suggestions_webform_submission_navigation(array $variables) {
-  return _webform_theme_suggestions($variables, 'webform_submission_navigation');
+function webform_theme_suggestions_webform_submission(array $variables) {
+  return _webform_theme_suggestions($variables, 'webform_submission');
 }
 
 /**
  * Implements hook_theme_suggestions_HOOK().
  */
-function webform_theme_suggestions_webform_submission(array $variables) {
-  return _webform_theme_suggestions($variables, 'webform_submission');
+function webform_theme_suggestions_webform_submission_form(array $variables) {
+  return _webform_theme_suggestions($variables, 'webform_submission_form');
 }
 
 /**
  * Implements hook_theme_suggestions_HOOK().
  */
-function webform_theme_suggestions_webform_submission_form(array $variables) {
-  return _webform_theme_suggestions($variables, 'webform_submission_form');
+function webform_theme_suggestions_webform_submission_navigation(array $variables) {
+  return _webform_theme_suggestions($variables, 'webform_submission_navigation');
 }
 
 /**
@@ -942,6 +945,13 @@ function webform_theme_suggestions_webform_submission_information(array $variabl
   return _webform_theme_suggestions($variables, 'webform_submission_information');
 }
 
+/**
+ * Implements hook_theme_suggestions_HOOK().
+ */
+function webform_theme_suggestions_webform_submission_data(array $variables) {
+  return _webform_theme_suggestions($variables, 'webform_submission_data');
+}
+
 /**
  * Implements hook_theme_suggestions_HOOK().
  */
diff --git a/includes/webform.theme.template.inc b/includes/webform.theme.template.inc
index b94eb8ed..7a65b0d3 100644
--- a/includes/webform.theme.template.inc
+++ b/includes/webform.theme.template.inc
@@ -294,6 +294,25 @@ function template_preprocess_webform_submission(array &$variables) {
   }
 }
 
+/**
+ * Prepares variables for webform submission data templates.
+ *
+ * Default template: webform-submission-data.html.twig.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *   - data: An array of elements to display in view mode.
+ *   - webform_submission: The webform submissions object.
+ *   - view_mode: View mode; e.g., 'html', 'text', 'table', 'yaml', etc.
+ */
+function template_preprocess_webform_submission_data(array &$variables) {
+  $variables['view_mode'] = $variables['elements']['#view_mode'];
+  $variables['webform_submission'] = $variables['elements']['#webform_submission'];
+  if ($variables['webform_submission'] instanceof WebformSubmissionInterface) {
+    $variables['webform'] = $variables['webform_submission']->getWebform();
+  }
+}
+
 /**
  * Prepares variables for webform submission information template.
  *
diff --git a/src/Tests/Element/WebformElementFormatCustomTest.php b/src/Tests/Element/WebformElementFormatCustomTest.php
index 41947f82..f563397e 100644
--- a/src/Tests/Element/WebformElementFormatCustomTest.php
+++ b/src/Tests/Element/WebformElementFormatCustomTest.php
@@ -65,7 +65,7 @@ class WebformElementFormatCustomTest extends WebformElementTestBase {
 
     // Check caught exception is displayed to users with update access.
     // @see \Drupal\webform\Twig\TwigExtension::renderTwigTemplate
-    $this->assertRaw('(&quot;The &quot;[webform_submission:values:textfield_custom_token_exception]&quot; token is being called recursively.&quot;)');
+    $this->assertRaw('(&quot;The &quot;[webform_submission:values:textfield_custom_token_exception]&quot; is being called recursively.&quot;)');
     $this->assertRaw('<label>textfield_custom_token_exception</label>');
     $this->assertRaw('<em>EXCEPTION</em>');
 
diff --git a/src/Tests/Settings/WebformSettingsPreviewTest.php b/src/Tests/Settings/WebformSettingsPreviewTest.php
index 9ac5fbfb..afdaf8c4 100644
--- a/src/Tests/Settings/WebformSettingsPreviewTest.php
+++ b/src/Tests/Settings/WebformSettingsPreviewTest.php
@@ -57,7 +57,9 @@ class WebformSettingsPreviewTest extends WebformTestBase {
     $this->assertFieldByName('op', 'Submit');
     $this->assertFieldByName('op', '< Previous');
 
-    $this->assertRaw('<div class="webform-preview js-form-wrapper form-wrapper" data-drupal-selector="edit-preview" id="edit-preview"><fieldset class="format-attributes-class webform-container webform-container-type-fieldset js-form-item form-item js-form-wrapper form-wrapper" id="test_form_preview--fieldset">');
+    $this->assertRaw('<div class="webform-preview js-form-wrapper form-wrapper" data-drupal-selector="edit-preview" id="edit-preview">');
+    $this->assertRaw('<div data-drupal-selector="edit-submission" class="webform-submission-data webform-submission-data--webform-test-form-preview webform-submission-data--view-mode-preview">');
+    $this->assertRaw('<fieldset class="format-attributes-class webform-container webform-container-type-fieldset js-form-item form-item js-form-wrapper form-wrapper" id="test_form_preview--fieldset">');
     $this->assertRaw('<div class="format-attributes-class webform-element webform-element-type-textfield js-form-item form-item js-form-type-item form-type-item js-form-item-name form-item-name" id="test_form_preview--name">');
     $this->assertRaw('<label>Name</label>' . PHP_EOL . '        test');
 
diff --git a/src/Twig/TwigExtension.php b/src/Twig/TwigExtension.php
index 020c8734..bd189cfd 100644
--- a/src/Twig/TwigExtension.php
+++ b/src/Twig/TwigExtension.php
@@ -5,6 +5,7 @@ namespace Drupal\webform\Twig;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\webform\Element\WebformMessage;
 use Drupal\webform\Utility\WebformHtmlHelper;
+use Drupal\webform\Utility\WebformLogicHelper;
 use Drupal\webform\WebformSubmissionInterface;
 
 /**
@@ -52,23 +53,15 @@ class TwigExtension extends \Twig_Extension {
    * @see \Drupal\Core\Utility\Token::replace
    */
   public function webformToken($token, EntityInterface $entity = NULL, array $data = [], array $options = []) {
-    static $processing = [];
-
     // Allow the webform_token function to be tested during validation without
     // a valid entity.
     if (!$entity) {
       return $token;
     }
 
-    // Prevent token replacement recursion.
     $original_token = $token;
-    $processing += [$original_token => 0];
-    $processing[$original_token]++;
-    if ($processing[$original_token] > 100) {
-      // Cancel token processing by settings the processing token to FALSE.
-      $processing[$original_token] = FALSE;
-      // Throw exception which is caught by ::renderTwigTemplate.
-      throw new \LogicException(sprintf('The "%s" token is being called recursively.', $token));
+    if (WebformLogicHelper::startRecursionTracking($token) === FALSE) {
+      return '';
     }
 
     // Parse options included in the token.
@@ -89,13 +82,10 @@ class TwigExtension extends \Twig_Extension {
     $token_manager = \Drupal::service('webform.token_manager');
     $value = $token_manager->replace($token, $entity, $data, $options);
 
-    // If token replacement caused a recursion exception return an empty value.
-    if ($processing[$original_token] === FALSE) {
+    if (WebformLogicHelper::stopRecursionTracking($original_token) === FALSE) {
       return '';
     }
 
-    $processing[$original_token]--;
-
     return (WebformHtmlHelper::containsHtml($value)) ? ['#markup' => $value] : $value;
   }
 
diff --git a/src/Utility/WebformLogicHelper.php b/src/Utility/WebformLogicHelper.php
new file mode 100644
index 00000000..ec75e302
--- /dev/null
+++ b/src/Utility/WebformLogicHelper.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Drupal\webform\Utility;
+
+/**
+ * Provides helper to handle logic related issues.
+ */
+class WebformLogicHelper {
+
+  /**
+   * Track recursions.
+   *
+   * @var array
+   */
+  static private $recursionTracker = [];
+
+  /**
+   * Track recursions by counting how many times a value is called.
+   *
+   * @param string $value
+   *   A string value typically a token.
+   * @param bool $increment
+   *   TRUE to increment tracking and FALSE to deincrement tracking.
+   *
+   * @return bool
+   *   FALSE when recursion is detected.
+   */
+  protected static function trackRecursion($value, $increment = TRUE) {
+    
+    self::$recursionTracker += [$value => 0];
+
+    if (self::$recursionTracker[$value] === FALSE) {
+      return FALSE;
+    }
+
+    if ($increment) {
+      self::$recursionTracker[$value]++;
+      if (self::$recursionTracker[$value] > 100) {
+        // Cancel processing by setting the recursion tracker value to FALSE.
+        self::$recursionTracker[$value] = FALSE;
+        throw new \LogicException(sprintf('The "%s" is being called recursively.', $value));
+      }
+    }
+    else {
+      self::$recursionTracker[$value]--;
+    }
+  }
+
+  /**
+   * Start recursion tracking.
+   *
+   * @param string $value
+   *   A string value typically a token.
+   *
+   * @return bool
+   *   FALSE when recursion is detected.
+   */
+  public static function startRecursionTracking($value) {
+    return self::trackRecursion($value, TRUE);
+  }
+
+  /**
+   * Stop recursion tracking.
+   *
+   * @param string $value
+   *   A string value typically a token.
+   *
+   * @return bool
+   *   FALSE when recursion is detected.
+   */
+  public static function stopRecursionTracking($value) {
+    return self::trackRecursion($value, FALSE);
+  }
+
+}
diff --git a/src/WebformSubmissionViewBuilder.php b/src/WebformSubmissionViewBuilder.php
index 6ef69d5c..05071c50 100644
--- a/src/WebformSubmissionViewBuilder.php
+++ b/src/WebformSubmissionViewBuilder.php
@@ -82,11 +82,13 @@ class WebformSubmissionViewBuilder extends EntityViewBuilder implements WebformS
   protected function getBuildDefaults(EntityInterface $entity, $view_mode) {
     $build = parent::getBuildDefaults($entity, $view_mode);
     // The webform submission will be rendered in the wrapped webform submission
-    // template already and thus has no entity template itself.
+    // template already. Instead we are going to wrap the rendered submission
+    // in a webform submission data template.
     // @see \Drupal\contact_storage\ContactMessageViewBuilder
     // @see \Drupal\comment\CommentViewBuilder::getBuildDefaults
     // @see \Drupal\block_content\BlockContentViewBuilder::getBuildDefaults
-    unset($build['#theme']);
+    // @see webform-submission-data.html.twig
+    $build['#theme'] = 'webform_submission_data';
     return $build;
   }
 
diff --git a/templates/webform-submission-data.html.twig b/templates/webform-submission-data.html.twig
new file mode 100644
index 00000000..b94570cd
--- /dev/null
+++ b/templates/webform-submission-data.html.twig
@@ -0,0 +1,24 @@
+{#
+/**
+ * @file
+ * Default theme implementation for webform submission data.
+ *
+ * Available variables:
+ * - webform_submission: The webform submission.
+ * - webform: The webform.
+ *
+ * @see template_preprocess_webform_submission_data()
+ *
+ * @ingroup themeable
+ */
+#}
+{%
+set classes = [
+'webform-submission-data',
+'webform-submission-data--webform-' ~ webform.id()|clean_class,
+view_mode ? 'webform-submission-data--view-mode-' ~ view_mode|clean_class,
+]
+%}
+<div{{ attributes.addClass(classes) }}>
+{{ elements }}
+</div>
diff --git a/webform.tokens.inc b/webform.tokens.inc
index 6cccecbe..24c831b0 100644
--- a/webform.tokens.inc
+++ b/webform.tokens.inc
@@ -15,6 +15,7 @@ use Drupal\webform\Plugin\WebformElementManagerInterface;
 use Drupal\webform\Plugin\WebformElementEntityReferenceInterface;
 use Drupal\webform\Plugin\WebformElement\WebformComputedBase;
 use Drupal\webform\Utility\WebformDateHelper;
+use Drupal\webform\Utility\WebformLogicHelper;
 use Drupal\webform\WebformSubmissionInterface;
 use Drupal\Core\Url;
 
@@ -794,6 +795,11 @@ function _webform_token_get_submission_value($value_token, array $options, Webfo
     $element['#format_items'] = array_shift($keys);
   }
 
+  $token = "[webform_submission:values:$value_token]";
+  if (WebformLogicHelper::startRecursionTracking($token) === FALSE) {
+    return '';
+  }
+
   $format_method = (empty($options['html'])) ? 'formatText' : 'formatHtml';
   $token_value = $element_manager->invokeMethod($format_method, $element, $webform_submission, $options);
   if (is_array($token_value)) {
@@ -812,6 +818,10 @@ function _webform_token_get_submission_value($value_token, array $options, Webfo
     $token_value = (string) $token_value;
   }
 
+  if (WebformLogicHelper::stopRecursionTracking($token) === FALSE) {
+    return '';
+  }
+
   return $token_value;
 }
 
@@ -827,13 +837,11 @@ function _webform_token_get_submission_value($value_token, array $options, Webfo
  *   Webform submission values.
  */
 function _webform_token_get_submission_values(array $options, WebformSubmissionInterface $webform_submission) {
-  static $rendering;
-  if ($rendering) {
-    $token = (!empty($options['html'])) ? '[webform_submission:values:html]' : '[webform_submission:values]';
-    throw new \LogicException("Recursive rendering of $token detected.");
-  }
+  $token = (!empty($options['html'])) ? '[webform_submission:values:html]' : '[webform_submission:values]';
 
-  $rendering = TRUE;
+  if (WebformLogicHelper::startRecursionTracking($token) === FALSE) {
+    return '';
+  }
 
   $submission_format = (!empty($options['html'])) ? 'html' : 'text';
   /** @var \Drupal\webform\WebformSubmissionViewBuilderInterface $view_builder */
@@ -845,7 +853,9 @@ function _webform_token_get_submission_values(array $options, WebformSubmissionI
   // included in an email.
   $value = \Drupal::service('renderer')->renderPlain($token_value);
 
-  $rendering = FALSE;
+  if (WebformLogicHelper::stopRecursionTracking($token) === FALSE) {
+    return '';
+  }
 
   return $value;
 }
