diff --git a/config/schema/webform.schema.yml b/config/schema/webform.schema.yml
index a914588d..d81a6b28 100644
--- a/config/schema/webform.schema.yml
+++ b/config/schema/webform.schema.yml
@@ -980,7 +980,7 @@ block.settings.webform_block:
   label: 'Webforms block'
   mapping:
     webform_id:
-      type: string
+      type: text
       label: 'Webform'
     default_data:
       type: text
@@ -1009,13 +1009,13 @@ block.settings.webform_submission_limit_block:
       type: label
       label: 'Progress bar message'
     webform_id:
-      type: string
+      type: text
       label: 'Webform'
     entity_type:
-      type: string
+      type: text
       label: 'Source entity type'
     entity_id:
-      type: string
+      type: text
       label: 'Source entity id'
 
 # Schema for the configuration of the webform field type.
diff --git a/js/webform.element.details.save.js b/js/webform.element.details.save.js
index 9c8a066d..72191068 100644
--- a/js/webform.element.details.save.js
+++ b/js/webform.element.details.save.js
@@ -22,6 +22,12 @@
       $('details > summary', context).once('webform-details-summary-save').click(function () {
         var $details = $(this).parent();
 
+
+        // @see https://css-tricks.com/snippets/jquery/make-an-jquery-hasattr/
+        if ($details[0].hasAttribute('data-webform-details-nosave')) {
+          return;
+        }
+
         var name = Drupal.webformDetailsSaveGetName($details);
         if (!name) {
           return;
diff --git a/src/Element/WebformElementStates.php b/src/Element/WebformElementStates.php
index 651cc38d..0f980f34 100644
--- a/src/Element/WebformElementStates.php
+++ b/src/Element/WebformElementStates.php
@@ -274,6 +274,8 @@ class WebformElementStates extends FormElement {
     $row['selector'] = [
       '#type' => 'webform_select_other',
       '#options' => $element['#selector_options'],
+      '#other__option_label' => t('Custom selector...'),
+      '#other__placeholder' => t('Enter custom selector...'),
       '#default_value' => $condition['selector'],
       '#empty_option' => '',
       '#empty_value' => '',
diff --git a/src/Element/WebformOtherBase.php b/src/Element/WebformOtherBase.php
index 2c15d4a4..419bd243 100644
--- a/src/Element/WebformOtherBase.php
+++ b/src/Element/WebformOtherBase.php
@@ -169,14 +169,16 @@ abstract class WebformOtherBase extends FormElement {
     $other_value = $value['other'];
     if (static::isMultiple($element)) {
       $element_value = array_filter($element_value);
+      $element_value = array_combine($element_value, $element_value);
       $return_value += $element_value;
-      if (isset($element_value[static::OTHER_OPTION])) {
+      if (isset($return_value[static::OTHER_OPTION])) {
         unset($return_value[static::OTHER_OPTION]);
         if ($has_access && $other_value === '') {
           static::setOtherError($element, $form_state);
-          return;
         }
-        $return_value += [$other_value => $other_value];
+        else {
+          $return_value += [$other_value => $other_value];
+        }
       }
     }
     else {
@@ -184,7 +186,7 @@ abstract class WebformOtherBase extends FormElement {
       if ($element_value == static::OTHER_OPTION) {
         if ($has_access && $other_value === '') {
           static::setOtherError($element, $form_state);
-          return;
+          $return_value = '';
         }
         else {
           $return_value = $other_value;
diff --git a/src/Plugin/WebformElement/Checkboxes.php b/src/Plugin/WebformElement/Checkboxes.php
index 1772ac8b..40055cf0 100644
--- a/src/Plugin/WebformElement/Checkboxes.php
+++ b/src/Plugin/WebformElement/Checkboxes.php
@@ -3,6 +3,7 @@
 namespace Drupal\webform\Plugin\WebformElement;
 
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\webform\WebformSubmissionStatesValidator;
 use Drupal\webform\WebformSubmissionInterface;
 
 /**
@@ -65,6 +66,21 @@ class Checkboxes extends OptionsBase {
     return $selectors;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getElementSelectorInputValue($selector, $trigger, array $element, WebformSubmissionInterface $webform_submission) {
+    $input_name = WebformSubmissionStatesValidator::getSelectorInputName($selector);
+    $option_value = WebformSubmissionStatesValidator::getInputNameAsArray($input_name, 1);
+    $value = $this->getRawValue($element, $webform_submission);
+    if (in_array($option_value, $value)) {
+      return (in_array($trigger, ['checked', 'unchecked'])) ? TRUE : $value;
+    }
+    else {
+      return (in_array($trigger, ['checked', 'unchecked'])) ? FALSE : NULL;
+    }
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/src/Plugin/WebformElement/DateList.php b/src/Plugin/WebformElement/DateList.php
index f1ceae10..d4b1db7a 100644
--- a/src/Plugin/WebformElement/DateList.php
+++ b/src/Plugin/WebformElement/DateList.php
@@ -2,7 +2,9 @@
 
 namespace Drupal\webform\Plugin\WebformElement;
 
+use Drupal\Core\Datetime\DrupalDateTime;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\webform\WebformSubmissionStatesValidator;
 use Drupal\webform\WebformSubmissionInterface;
 
 /**
@@ -74,6 +76,55 @@ class DateList extends DateBase {
     return $selectors;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getElementSelectorInputValue($selector, $trigger, array $element, WebformSubmissionInterface $webform_submission) {
+    $value = $this->getRawValue($element, $webform_submission);
+    if (empty($value)) {
+      return NULL;
+    }
+
+    // Return date part value.
+    // @see \Drupal\Core\Datetime\Element\Datelist::valueCallback
+    $input_name = WebformSubmissionStatesValidator::getSelectorInputName($selector);
+    $part = WebformSubmissionStatesValidator::getInputNameAsArray($input_name, 1);
+    switch ($part) {
+      case 'day':
+        $format = 'j';
+        break;
+
+      case 'month':
+        $format = 'n';
+        break;
+
+      case 'year':
+        $format = 'Y';
+        break;
+
+      case 'hour':
+        $format = in_array('ampm', $element['#date_part_order']) ? 'g' : 'G';
+        break;
+
+      case 'minute':
+        $format = 'i';
+        break;
+
+      case 'second':
+        $format = 's';
+        break;
+
+      case 'ampm':
+        $format = 'a';
+        break;
+
+      default:
+        $format = '';
+    }
+    $date = DrupalDateTime::createFromTimestamp(strtotime($value));
+    return $date->format($format);
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/src/Plugin/WebformElement/DateTime.php b/src/Plugin/WebformElement/DateTime.php
index 62820201..a6bf0c69 100644
--- a/src/Plugin/WebformElement/DateTime.php
+++ b/src/Plugin/WebformElement/DateTime.php
@@ -2,8 +2,10 @@
 
 namespace Drupal\webform\Plugin\WebformElement;
 
+use Drupal\Core\Datetime\DrupalDateTime;
 use Drupal\Core\Datetime\Entity\DateFormat;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\webform\WebformSubmissionStatesValidator;
 use Drupal\webform\WebformSubmissionInterface;
 
 /**
@@ -103,6 +105,25 @@ class DateTime extends DateBase {
     ];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getElementSelectorInputValue($selector, $trigger, array $element, WebformSubmissionInterface $webform_submission) {
+    $value = $this->getRawValue($element, $webform_submission);
+    if (empty($value)) {
+      return NULL;
+    }
+
+    // Get date/time format pattern.
+    $input_name = WebformSubmissionStatesValidator::getSelectorInputName($selector);
+    $format = 'html_' . WebformSubmissionStatesValidator::getInputNameAsArray($input_name, 1);
+    $pattern = DateFormat::load($format)->getPattern();
+
+    // Return date/time.
+    $date = DrupalDateTime::createFromTimestamp(strtotime($value));
+    return $date->format($pattern);
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/src/Plugin/WebformElement/OptionsBase.php b/src/Plugin/WebformElement/OptionsBase.php
index 649be0e2..0961ce45 100644
--- a/src/Plugin/WebformElement/OptionsBase.php
+++ b/src/Plugin/WebformElement/OptionsBase.php
@@ -10,6 +10,7 @@ use Drupal\webform\Utility\WebformElementHelper;
 use Drupal\webform\Utility\WebformOptionsHelper;
 use Drupal\webform\Plugin\WebformElementBase;
 use Drupal\webform\Plugin\WebformElementEntityReferenceInterface;
+use Drupal\webform\WebformSubmissionStatesValidator;
 use Drupal\webform\WebformSubmissionInterface;
 
 /**
@@ -24,6 +25,13 @@ abstract class OptionsBase extends WebformElementBase {
    */
   protected $exportDelta = FALSE;
 
+  /**
+   * The other option base element type.
+   *
+   * @var string
+   */
+  protected $otherOptionType;
+
   /**
    * {@inheritdoc}
    */
@@ -36,38 +44,66 @@ abstract class OptionsBase extends WebformElementBase {
       unset($default_properties['wrapper_attributes']);
     }
 
-    return $default_properties + [
+    $default_properties += [
       // Options settings.
       'options' => [],
       'options_randomize' => FALSE,
     ];
+
+    // Add other properties to elements that include the other text field.
+    if ($this->isOptionsOther()) {
+      $default_properties += [
+        'other__option_label' => $this->t('Other...'),
+        'other__type' => 'textfield',
+        'other__title' => '',
+        'other__placeholder' => $this->t('Enter other...'),
+        'other__description' => '',
+        // Text field or textarea.
+        'other__size' => '',
+        'other__maxlength' => '',
+        'other__field_prefix' => '',
+        'other__field_suffix' => '',
+        // Textarea.
+        'other__rows' => '',
+        // Number.
+        'other__min' => '',
+        'other__max' => '',
+        'other__step' => '',
+      ];
+    }
+
+    return $default_properties;
   }
 
   /**
-   * Get option (option) properties.
+   * Determine if the element plugin type includes an other option text field.
    *
-   * @return array
-   *   An associative array containing other (option) properties.
+   * @return bool
+   *   TRUE if the element plugin type includes an other option text field.
    */
-  public function getOtherProperties() {
-    return [
-      'other__option_label' => $this->t('Other...'),
-      'other__type' => 'textfield',
-      'other__title' => '',
-      'other__placeholder' => $this->t('Enter other...'),
-      'other__description' => '',
-      // Text field or textarea.
-      'other__size' => '',
-      'other__maxlength' => '',
-      'other__field_prefix' => '',
-      'other__field_suffix' => '',
-      // Textarea.
-      'other__rows' => '',
-      // Number.
-      'other__min' => '',
-      'other__max' => '',
-      'other__step' => '',
-    ];
+  protected function isOptionsOther() {
+    return $this->getOptionsOtherType() ? TRUE : FALSE;
+  }
+
+  /**
+   * Get the other option base element type.
+   *
+   * @return string|NULL
+   *   The base element type (select|radios|checkboxes|buttons).
+   */
+  protected function getOptionsOtherType() {
+    if (isset($this->otherOptionType)) {
+      return $this->otherOptionType;
+    }
+
+    if (preg_match('/webform_(select|radios|checkboxes|buttons)_other$/', $this->getPluginId(), $match)) {
+      $this->otherOptionType = $match[1];
+    }
+    else {
+      $this->otherOptionType = FALSE;
+    }
+
+    return $this->otherOptionType ;
   }
 
   /**
@@ -371,6 +407,51 @@ abstract class OptionsBase extends WebformElementBase {
     }
   }
 
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getElementSelectorInputValue($selector, $trigger, array $element, WebformSubmissionInterface $webform_submission) {
+    if ($this->isOptionsOther()) {
+      $input_name = WebformSubmissionStatesValidator::getSelectorInputName($selector);
+      $other_type = WebformSubmissionStatesValidator::getInputNameAsArray($input_name, 1);
+      $value = $this->getRawValue($element, $webform_submission);
+      if ($other_type === 'other') {
+        if ($this->hasMultipleValues($element)) {
+          $other_value = array_diff($value, array_keys($element['#options']));
+          return ($other_value) ? implode( ', ', $other_value) : NULL;
+        }
+        else {
+          // Make sure other value is not valid option.
+          return ($value && !isset($element['#options'][$value])) ? $value : NULL;
+        }
+      }
+      else {
+        if ($this->hasMultipleValues($element)) {
+          // Return array of valid #options.
+          return array_intersect($value, array_keys($element['#options']));
+        }
+        else {
+          // Return valid #option.
+          return (isset($element['#options'][$value])) ? $value : NULL;
+        }
+      }
+
+    }
+    else {
+
+      return parent::getElementSelectorInputValue($selector, $trigger, $element, $webform_submission);
+
+    }
+
+    if (in_array($option_value, $value)) {
+      return (in_array($trigger, ['checked', 'unchecked'])) ? TRUE : $value;
+    }
+    else {
+      return (in_array($trigger, ['checked', 'unchecked'])) ? FALSE : NULL;
+    }
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/src/Plugin/WebformElement/TextFormat.php b/src/Plugin/WebformElement/TextFormat.php
index 56f8ee09..484e6531 100644
--- a/src/Plugin/WebformElement/TextFormat.php
+++ b/src/Plugin/WebformElement/TextFormat.php
@@ -7,6 +7,7 @@ use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Mail\MailFormatHelper;
 use Drupal\filter\Entity\FilterFormat;
 use Drupal\webform\Plugin\WebformElementBase;
+use Drupal\webform\WebformSubmissionStatesValidator;
 use Drupal\webform\WebformSubmissionInterface;
 
 /**
diff --git a/src/Plugin/WebformElement/WebformButtonsOther.php b/src/Plugin/WebformElement/WebformButtonsOther.php
index 26d40c06..8ae9842e 100644
--- a/src/Plugin/WebformElement/WebformButtonsOther.php
+++ b/src/Plugin/WebformElement/WebformButtonsOther.php
@@ -12,13 +12,4 @@ namespace Drupal\webform\Plugin\WebformElement;
  *   category = @Translation("Options elements"),
  * )
  */
-class WebformButtonsOther extends OptionsBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getDefaultProperties() {
-    return parent::getDefaultProperties() + static::getOtherProperties();
-  }
-
-}
+class WebformButtonsOther extends OptionsBase {}
diff --git a/src/Plugin/WebformElement/WebformCheckboxesOther.php b/src/Plugin/WebformElement/WebformCheckboxesOther.php
index e792b149..89a0461a 100644
--- a/src/Plugin/WebformElement/WebformCheckboxesOther.php
+++ b/src/Plugin/WebformElement/WebformCheckboxesOther.php
@@ -16,13 +16,6 @@ use Drupal\webform\WebformSubmissionInterface;
  */
 class WebformCheckboxesOther extends Checkboxes {
 
-  /**
-   * {@inheritdoc}
-   */
-  public function getDefaultProperties() {
-    return parent::getDefaultProperties() + static::getOtherProperties();
-  }
-
   /**
    * {@inheritdoc}
    */
diff --git a/src/Plugin/WebformElement/WebformRadiosOther.php b/src/Plugin/WebformElement/WebformRadiosOther.php
index 9b94933a..5d156a07 100644
--- a/src/Plugin/WebformElement/WebformRadiosOther.php
+++ b/src/Plugin/WebformElement/WebformRadiosOther.php
@@ -12,13 +12,4 @@ namespace Drupal\webform\Plugin\WebformElement;
  *   category = @Translation("Options elements"),
  * )
  */
-class WebformRadiosOther extends Radios {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getDefaultProperties() {
-    return parent::getDefaultProperties() + static::getOtherProperties();
-  }
-
-}
+class WebformRadiosOther extends Radios {}
diff --git a/src/Plugin/WebformElement/WebformSelectOther.php b/src/Plugin/WebformElement/WebformSelectOther.php
index cb485043..4c69fe5a 100644
--- a/src/Plugin/WebformElement/WebformSelectOther.php
+++ b/src/Plugin/WebformElement/WebformSelectOther.php
@@ -12,13 +12,4 @@ namespace Drupal\webform\Plugin\WebformElement;
  *   category = @Translation("Options elements"),
  * )
  */
-class WebformSelectOther extends Select {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getDefaultProperties() {
-    return parent::getDefaultProperties() + static::getOtherProperties();
-  }
-
-}
+class WebformSelectOther extends Select {}
diff --git a/src/Plugin/WebformElementBase.php b/src/Plugin/WebformElementBase.php
index 8ba1692a..2bbf2778 100644
--- a/src/Plugin/WebformElementBase.php
+++ b/src/Plugin/WebformElementBase.php
@@ -25,6 +25,7 @@ use Drupal\webform\Utility\WebformElementHelper;
 use Drupal\webform\Utility\WebformReflectionHelper;
 use Drupal\webform\WebformInterface;
 use Drupal\webform\WebformLibrariesManagerInterface;
+use Drupal\webform\WebformSubmissionStatesValidator;
 use Drupal\webform\WebformSubmissionInterface;
 use Drupal\webform\WebformTokenManagerInterface;
 use Psr\Log\LoggerInterface;
@@ -1182,6 +1183,14 @@ class WebformElementBase extends PluginBase implements WebformElementInterface {
     return $value;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getRawValue(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
+    $element['#format'] = 'raw';
+    return $this->getValue($element, $webform_submission, $options);
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -1552,6 +1561,25 @@ class WebformElementBase extends PluginBase implements WebformElementInterface {
     return [];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getElementSelectorInputValue($selector, $trigger, array $element, WebformSubmissionInterface $webform_submission) {
+    if ($this->isComposite()) {
+      $input_name = WebformSubmissionStatesValidator::getSelectorInputName($selector);
+      $composite_key = WebformSubmissionStatesValidator::getInputNameAsArray($input_name, 1);
+      if ($composite_key) {
+        return $this->getRawValue($element, $webform_submission, ['composite_key' => $composite_key]);
+      }
+      else {
+        return NULL;
+      }
+    }
+    else {
+      return $this->getRawValue($element, $webform_submission);
+    }
+  }
+
   /****************************************************************************/
   // Operation methods.
   /****************************************************************************/
diff --git a/src/Plugin/WebformElementInterface.php b/src/Plugin/WebformElementInterface.php
index b2081a85..7a2adf50 100644
--- a/src/Plugin/WebformElementInterface.php
+++ b/src/Plugin/WebformElementInterface.php
@@ -445,6 +445,21 @@ interface WebformElementInterface extends PluginInspectionInterface, PluginFormI
    */
   public function getValue(array $element, WebformSubmissionInterface $webform_submission, array $options = []);
 
+  /**
+   * Get an element's submission raw value.
+   *
+   * @param array $element
+   *   An element.
+   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
+   *   A webform submission.
+   * @param array $options
+   *   An array of options.
+   *
+   * @return array|string
+   *   The element's submission value.
+   */
+  public function getRawValue(array $element, WebformSubmissionInterface $webform_submission, array $options = []);
+
   /**
    * Get an element's available single value formats.
    *
@@ -655,6 +670,23 @@ interface WebformElementInterface extends PluginInspectionInterface, PluginFormI
    */
   public function getElementSelectorOptions(array $element);
 
+  /**
+   * Get an element's (sub)input selector value.
+   *
+   * @param string $selector
+   *   CSS :input selector.
+   * @param string $trigger
+   *   Trigger from #states.
+   * @param array $element
+   *   An element.
+   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
+   *   A webform submission.
+   *
+   * @return mixed
+   *   The element input's value.
+   */
+  public function getElementSelectorInputValue($selector, $trigger, array $element, WebformSubmissionInterface $webform_submission);
+
   /****************************************************************************/
   // Operation methods.
   /****************************************************************************/
diff --git a/src/Tests/Element/WebformElementComputedTest.php b/src/Tests/Element/WebformElementComputedTest.php
index 39550203..6c0cdd49 100644
--- a/src/Tests/Element/WebformElementComputedTest.php
+++ b/src/Tests/Element/WebformElementComputedTest.php
@@ -18,7 +18,7 @@ class WebformElementComputedTest extends WebformTestBase {
    *
    * @var array
    */
-  public static $modules = ['webform', 'filter'];
+  public static $modules = ['filter', 'webform'];
 
   /**
    * Webforms to load.
diff --git a/src/Tests/Element/WebformElementHtmlEditorTest.php b/src/Tests/Element/WebformElementHtmlEditorTest.php
index 162f2f14..7253a4ac 100644
--- a/src/Tests/Element/WebformElementHtmlEditorTest.php
+++ b/src/Tests/Element/WebformElementHtmlEditorTest.php
@@ -17,7 +17,7 @@ class WebformElementHtmlEditorTest extends WebformTestBase {
    *
    * @var array
    */
-  public static $modules = ['webform', 'filter'];
+  public static $modules = ['filter', 'webform'];
 
   /**
    * Webforms to load.
diff --git a/src/Tests/Element/WebformElementStatesTest.php b/src/Tests/Element/WebformElementStatesTest.php
index 0d4fbc8e..d1d391aa 100644
--- a/src/Tests/Element/WebformElementStatesTest.php
+++ b/src/Tests/Element/WebformElementStatesTest.php
@@ -76,7 +76,7 @@ states_unsupported_nesting:
     $this->drupalGet('webform/test_element_states');
 
     // Check 'States custom selector'.
-    $this->assertRaw('<input data-drupal-selector="edit-states-custom-selector-states-1-selector-other" type="text" id="edit-states-custom-selector-states-1-selector-other" name="states_custom_selector[states][1][selector][other]" value="custom_selector" size="60" maxlength="128" placeholder="Enter other..." class="form-text" />');
+    $this->assertRaw('<input data-drupal-selector="edit-states-custom-selector-states-1-selector-other" type="text" id="edit-states-custom-selector-states-1-selector-other" name="states_custom_selector[states][1][selector][other]" value="custom_selector" size="60" maxlength="128" placeholder="Enter custom selector..." class="form-text" />');
 
     // Check 'States unsupport operator'.
     $this->assertRaw('Conditional logic (Form API #states) is using the <em class="placeholder">XOR</em> operator. Form API #states must be manually entered.');
diff --git a/src/Tests/WebformEntityReferenceItemNormalizerTest.php b/src/Tests/WebformEntityReferenceItemNormalizerTest.php
index 0579d76f..7664af70 100644
--- a/src/Tests/WebformEntityReferenceItemNormalizerTest.php
+++ b/src/Tests/WebformEntityReferenceItemNormalizerTest.php
@@ -16,7 +16,7 @@ class WebformEntityReferenceItemNormalizerTest extends WebformTestBase {
    *
    * @var array
    */
-  public static $modules = ['webform', 'webform_node', 'hal', 'serialization', 'rest'];
+  public static $modules = ['rest', 'serialization', 'hal', 'webform', 'webform_node'];
 
   /**
    * Tests the normalization of a node with a webform entity reference.
diff --git a/src/Tests/WebformSubmissionStatesValidatorTest.php b/src/Tests/WebformSubmissionStatesValidatorTest.php
new file mode 100644
index 00000000..a91396f9
--- /dev/null
+++ b/src/Tests/WebformSubmissionStatesValidatorTest.php
@@ -0,0 +1,237 @@
+<?php
+
+namespace Drupal\webform\Tests;
+
+use Drupal\webform\Element\WebformOtherBase;
+use Drupal\webform\Entity\Webform;
+
+/**
+ * Tests for webform submission states validator.
+ *
+ * @group Webform
+ */
+class WebformSubmissionStatesValidatorTest extends WebformTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['filter', 'webform'];
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = ['test_form_states_server_required', 'test_form_states_server_wizard'];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    // Create filters.
+    $this->createFilters();
+
+    $this->drupalLogin($this->rootUser);
+  }
+
+  /**
+   * Tests webform submission states validator required.
+   */
+  public function testFormStatesValidatorRequired() {
+    $webform = Webform::load('test_form_states_server_required');
+
+    // Check no #states required errors.
+    $this->postSubmission($webform);
+    $this->assertRaw('New submission added to Test: Form API #states server-side required validation.');
+
+    // Check required multiple dependents 'AND' and 'OR' operator.
+    $edit = [
+      'trigger_checkbox' => TRUE,
+      'trigger_textfield' => '{value}',
+      'trigger_select' => 'option',
+    ];
+    $this->postSubmission($webform, $edit);
+    $this->assertRaw('dependent_textfield_required_and field is required.');
+    $this->assertRaw('dependent_textfield_required_or field is required.');
+
+    // Check required multiple dependents 'OR' operator.
+    $edit = [
+      'trigger_checkbox' => TRUE,
+    ];
+    $this->postSubmission($webform, $edit);
+    $this->assertNoRaw('dependent_textfield_required_and field is required.');
+    $this->assertRaw('dependent_textfield_required_or field is required.');
+
+    // Check required checkboxes.
+    $edit = [
+      'checkboxes_trigger[one]' => TRUE,
+    ];
+    $this->postSubmission($webform, $edit);
+    $this->assertRaw('checkboxes_dependent_required field is required.');
+
+    // Check required text_format.
+    $edit = [
+      'text_format_trigger[format]' => 'full_html',
+    ];
+    $this->postSubmission($webform, $edit);
+    $this->assertRaw('text_format_dependent_required field is required.');
+
+    // Check required webform_select_other select #options.
+    $edit = [
+      'select_other_trigger[select]' => 'one',
+    ];
+    $this->postSubmission($webform, $edit);
+    $this->assertRaw('select_other_dependent_required field is required.');
+
+    // Check required webform_select_other other textfield.
+    $edit = [
+      'select_other_trigger[select]' => WebformOtherBase::OTHER_OPTION,
+      'select_other_trigger[other]' => '{value}',
+    ];
+    $this->postSubmission($webform, $edit);
+    $this->assertRaw('select_other_dependent_required field is required.');
+
+    // Check required webform_select_other_multiple select #options.
+    $edit = [
+      'select_other_multiple_trigger[select][]' => 'one',
+    ];
+    $this->postSubmission($webform, $edit);
+    $this->assertRaw('select_other_multiple_dependent_required field is required.');
+
+    // Check required webform_email_confirm.
+    $edit = [
+      'email_confirm_trigger[mail_1]' => 'example@example.com',
+      'email_confirm_trigger[mail_2]' => 'example@example.com',
+    ];
+    $this->postSubmission($webform, $edit);
+    $this->assertRaw('email_confirm_dependent_required field is required.');
+
+    // Check required webform_likert.
+    $edit = [
+      'likert_trigger[q1]' => 'a1',
+    ];
+    $this->postSubmission($webform, $edit);
+    $this->assertRaw('likert_dependent_required field is required.');
+    
+    // Check required datelist.
+    $edit = [
+      'datelist_trigger[year]' => date('Y'),
+      'datelist_trigger[month]' => 1,
+      'datelist_trigger[day]' => 1,
+      'datelist_trigger[hour]' => 1,
+      'datelist_trigger[minute]' => 1,
+      'datelist_trigger[second]' => 1,
+      'datelist_trigger[ampm]' => 'am',
+    ];
+    $this->postSubmission($webform, $edit);
+    $this->assertRaw('datelist_dependent_required field is required.');
+
+    // Check required datetime.
+    $edit = [
+      'datetime_trigger[date]' => date('2001-01-01'),
+      'datetime_trigger[time]' => date('12:12:12'),
+    ];
+    $this->postSubmission($webform, $edit);
+    $this->assertRaw('datetime_dependent_required field is required.');
+
+    // Check required address.
+    $edit = [
+      'address_trigger[address]' => '{value}',
+      'address_trigger[address_2]' => '{value}',
+      'address_trigger[city]' => '{value}',
+      'address_trigger[state_province]' => 'Alabama',
+      'address_trigger[postal_code]' => '11111',
+      'address_trigger[country]' => 'Afghanistan',
+    ];
+    $this->postSubmission($webform, $edit);
+    $this->assertRaw('address_dependent_required field is required.');
+  }
+
+  /**
+   * Tests webform submission states validator wizard cross-page conditionals.
+   */
+  public function testFormStatesValidatorWizard() {
+    $webform = Webform::load('test_form_states_server_wizard');
+
+    /**************************************************************************/
+    
+    // Go to default #states for page 02 with trigger-checkbox unchecked.
+    $this->postSubmission($webform, [], t('Next Page >'));
+
+    // Check trigger-checkbox value is No.
+    $this->assertRaw('<input data-drupal-selector="edit-page-01-trigger-checkbox-computed" type="hidden" name="page_01_trigger_checkbox_computed" value="No" />');
+
+    // Check page_02_textfield_required is not required.
+    $this->assertRaw('<input data-drupal-selector="edit-page-02-textfield-required" aria-describedby="edit-page-02-textfield-required--description" type="text" id="edit-page-02-textfield-required" name="page_02_textfield_required" value="{default_value}" size="60" maxlength="255" class="form-text" />');
+
+    // Check page_02_textfield_optional is required.
+    $this->assertRaw('<input data-drupal-selector="edit-page-02-textfield-optional" aria-describedby="edit-page-02-textfield-optional--description" type="text" id="edit-page-02-textfield-optional" name="page_02_textfield_optional" value="{default_value}" size="60" maxlength="255" class="form-text required" required="required" aria-required="true" />');
+
+    // Check page_02_textfield_disabled is not disabled.
+    $this->assertRaw('<input data-drupal-selector="edit-page-02-textfield-disabled" aria-describedby="edit-page-02-textfield-disabled--description" type="text" id="edit-page-02-textfield-disabled" name="page_02_textfield_disabled" value="" size="60" maxlength="255" class="form-text" />');
+
+    // Check page_02_textfield_enabled is disabled.
+    $this->assertRaw('<input data-drupal-selector="edit-page-02-textfield-enabled" aria-describedby="edit-page-02-textfield-enabled--description" disabled="disabled" type="text" id="edit-page-02-textfield-enabled" name="page_02_textfield_enabled" value="" size="60" maxlength="255" class="form-text" />');
+
+    // Check page_02_textfield_visible is not visible.
+    $this->assertNoFieldByName('page_02_textfield_visible');
+
+    // Check page_02_textfield_invisible is visible.
+    $this->assertFieldByName('page_02_textfield_invisible');
+
+    // Check page_02_checkbox_checked is not checked.
+    $this->assertRaw('<input data-drupal-selector="edit-page-02-checkbox-checked" aria-describedby="edit-page-02-checkbox-checked--description" type="checkbox" id="edit-page-02-checkbox-checked" name="page_02_checkbox_checked" value="1" class="form-checkbox" />');
+
+    // Check page_02_checkbox_unchecked is checked.
+    $this->assertRaw('<input data-drupal-selector="edit-page-02-checkbox-unchecked" aria-describedby="edit-page-02-checkbox-unchecked--description" type="checkbox" id="edit-page-02-checkbox-unchecked" name="page_02_checkbox_unchecked" value="1" checked="checked" class="form-checkbox" />');
+
+    // Check page_02_details_expanded is not open.
+    $this->assertRaw('<details data-webform-details-nosave data-webform-key="page_02_details_expanded" data-drupal-selector="edit-page-02-details-expanded" aria-describedby="edit-page-02-details-expanded--description" id="edit-page-02-details-expanded" class="js-form-wrapper form-wrapper"> ');
+
+    // Check page_02_details_collapsed is open.
+    $this->assertRaw('<details data-webform-details-nosave data-webform-key="page_02_details_collapsed" data-drupal-selector="edit-page-02-details-collapsed" aria-describedby="edit-page-02-details-collapsed--description" id="edit-page-02-details-collapsed" class="js-form-wrapper form-wrapper" open="open">');
+
+    /**************************************************************************/
+
+    // Go to default #states for page 02 with trigger_checkbox checked.
+    $this->postSubmission($webform, ['page_01_trigger_checkbox' => TRUE], t('Next Page >'));
+
+    // Check trigger-checkbox value is Yes
+    $this->assertRaw('<input data-drupal-selector="edit-page-01-trigger-checkbox-computed" type="hidden" name="page_01_trigger_checkbox_computed" value="Yes" />');
+
+    // Check page_02_textfield_required is required.
+    $this->assertRaw('<input data-drupal-selector="edit-page-02-textfield-required" aria-describedby="edit-page-02-textfield-required--description" type="text" id="edit-page-02-textfield-required" name="page_02_textfield_required" value="{default_value}" size="60" maxlength="255" class="form-text required" required="required" aria-required="true" />');
+
+    // Check page_02_textfield_optional is not required.
+    $this->assertRaw('<input data-drupal-selector="edit-page-02-textfield-optional" aria-describedby="edit-page-02-textfield-optional--description" type="text" id="edit-page-02-textfield-optional" name="page_02_textfield_optional" value="{default_value}" size="60" maxlength="255" class="form-text" />');
+
+    // Check page_02_textfield_disabled is disabled.
+    $this->assertRaw('<input data-drupal-selector="edit-page-02-textfield-disabled" aria-describedby="edit-page-02-textfield-disabled--description" disabled="disabled" type="text" id="edit-page-02-textfield-disabled" name="page_02_textfield_disabled" value="" size="60" maxlength="255" class="form-text" />');
+
+    // Check page_02_textfield_enabled is not disabled.
+    $this->assertRaw('<input data-drupal-selector="edit-page-02-textfield-enabled" aria-describedby="edit-page-02-textfield-enabled--description" type="text" id="edit-page-02-textfield-enabled" name="page_02_textfield_enabled" value="" size="60" maxlength="255" class="form-text" />');
+
+    // Check page_02_textfield_visible is visible.
+    $this->assertFieldByName('page_02_textfield_visible');
+
+    // Check page_02_textfield_invisible is not visible.
+    $this->assertNoFieldByName('page_02_textfield_invisible');
+
+    // Check page_02_checkbox_checked is checked.
+    $this->assertRaw('<input data-drupal-selector="edit-page-02-checkbox-checked" aria-describedby="edit-page-02-checkbox-checked--description" type="checkbox" id="edit-page-02-checkbox-checked" name="page_02_checkbox_checked" value="1" checked="checked" class="form-checkbox" />');
+
+    // Check page_02_checkbox_unchecked is not checked.
+    $this->assertRaw('<input data-drupal-selector="edit-page-02-checkbox-unchecked" aria-describedby="edit-page-02-checkbox-unchecked--description" type="checkbox" id="edit-page-02-checkbox-unchecked" name="page_02_checkbox_unchecked" value="1" class="form-checkbox" />');
+
+    // Check page_02_details_expanded is open.
+    $this->assertRaw('<details data-webform-details-nosave data-webform-key="page_02_details_expanded" data-drupal-selector="edit-page-02-details-expanded" aria-describedby="edit-page-02-details-expanded--description" id="edit-page-02-details-expanded" class="js-form-wrapper form-wrapper" open="open">');
+
+    // Check page_02_details_collapsed is not open.
+    $this->assertRaw('<details data-webform-details-nosave data-webform-key="page_02_details_collapsed" data-drupal-selector="edit-page-02-details-collapsed" aria-describedby="edit-page-02-details-collapsed--description" id="edit-page-02-details-collapsed" class="js-form-wrapper form-wrapper">');
+  }
+
+}
diff --git a/src/WebformRequestInterface.php b/src/WebformRequestInterface.php
index cd75e96f..36c32d7a 100644
--- a/src/WebformRequestInterface.php
+++ b/src/WebformRequestInterface.php
@@ -4,9 +4,6 @@ namespace Drupal\webform;
 
 use Drupal\Core\Entity\EntityInterface;
 
-/**
- * Helper class webform entity methods.
- */
 /**
  * Provides an interface defining a webform request handler.
  */
diff --git a/src/WebformSubmissionForm.php b/src/WebformSubmissionForm.php
index cf0ca121..2b5bcf80 100644
--- a/src/WebformSubmissionForm.php
+++ b/src/WebformSubmissionForm.php
@@ -115,6 +115,13 @@ class WebformSubmissionForm extends ContentEntityForm {
    */
   protected $tokenManager;
 
+  /**
+   * The webform submission (server-side) #states validator.
+   *
+   * @var \Drupal\webform\WebformSubmissionStatesValidator
+   */
+  protected $statesValidator;
+
   /**
    * The webform settings.
    *
@@ -158,7 +165,7 @@ class WebformSubmissionForm extends ContentEntityForm {
    * @param \Drupal\webform\WebformTokenManagerInterface $token_manager
    *   The webform token manager.
    */
-  public function __construct(EntityManagerInterface $entity_manager, RendererInterface $renderer, AliasManagerInterface $alias_manager, PathValidatorInterface $path_validator, WebformRequestInterface $request_handler, WebformElementManagerInterface $element_manager, WebformThirdPartySettingsManagerInterface $third_party_settings_manager, WebformMessageManagerInterface $message_manager, WebformTokenManagerInterface $token_manager) {
+  public function __construct(EntityManagerInterface $entity_manager, RendererInterface $renderer, AliasManagerInterface $alias_manager, PathValidatorInterface $path_validator, WebformRequestInterface $request_handler, WebformElementManagerInterface $element_manager, WebformThirdPartySettingsManagerInterface $third_party_settings_manager, WebformMessageManagerInterface $message_manager, WebformTokenManagerInterface $token_manager, WebformSubmissionStatesValidator $states_validator) {
     parent::__construct($entity_manager);
     $this->renderer = $renderer;
     $this->requestHandler = $request_handler;
@@ -169,6 +176,7 @@ class WebformSubmissionForm extends ContentEntityForm {
     $this->thirdPartySettingsManager = $third_party_settings_manager;
     $this->messageManager = $message_manager;
     $this->tokenManager = $token_manager;
+    $this->statesValidator = $states_validator;
   }
 
   /**
@@ -184,7 +192,8 @@ class WebformSubmissionForm extends ContentEntityForm {
       $container->get('plugin.manager.webform.element'),
       $container->get('webform.third_party_settings_manager'),
       $container->get('webform.message_manager'),
-      $container->get('webform.token_manager')
+      $container->get('webform.token_manager'),
+      $container->get('webform_submission.states_validator')
     );
   }
 
@@ -259,6 +268,20 @@ class WebformSubmissionForm extends ContentEntityForm {
    */
   protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
     parent::copyFormValuesToEntity($entity, $form, $form_state);
+
+    /* @var $webform_submission \Drupal\webform\WebformSubmissionInterface */
+    $webform_submission = $entity;
+    $webform = $webform_submission->getWebform();
+
+    // Get elements values from webform submission.
+    $values = array_intersect_key(
+      $form_state->getValues(),
+      $webform->getElementsInitializedFlattenedAndHasValue()
+    );
+
+    // Serialize the values as YAML and merge existing data.
+    $webform_submission->setData($values + $webform_submission->getData());
+
     // Set current page.
     if ($current_page = $this->getCurrentPage($form, $form_state)) {
       $entity->setCurrentPage($current_page);
@@ -304,6 +327,9 @@ class WebformSubmissionForm extends ContentEntityForm {
     $form_id = $this->getFormId();
     $this->thirdPartySettingsManager->alter('webform_submission_form', $form, $form_state, $form_id);
 
+    // Server side #states API validation.
+    $this->statesValidator->buildForm($form, $form_state);
+
     return $this->buildAjaxForm($form, $form_state);
   }
 
@@ -1035,29 +1061,21 @@ class WebformSubmissionForm extends ContentEntityForm {
         call_user_func_array($form_state->prepareCallback($callback), [&$form, &$form_state]);
       }
     }
+
+    // Build webform submission with validated and processed data.
+    $this->entity = $this->buildEntity($form, $form_state);
+
+    // Server side #states API validation.
+    $this->statesValidator->validateForm($form, $form_state);
   }
 
   /**
    * {@inheritdoc}
    */
   public function submitForm(array &$form, FormStateInterface $form_state) {
-    /* @var $webform_submission \Drupal\webform\WebformSubmissionInterface */
-    $webform_submission = $this->entity;
-    $webform = $webform_submission->getWebform();
-
-    // Get elements values from webform submission.
-    $values = array_intersect_key(
-      $form_state->getValues(),
-      $webform->getElementsInitializedFlattenedAndHasValue()
-    );
-
-    // Serialize the values as YAML and merge existing data.
-    $webform_submission->setData($values + $webform_submission->getData());
-
     parent::submitForm($form, $form_state);
-
     // Submit webform via webform handler.
-    $this->getWebform()->invokeHandlers('submitForm', $form, $form_state, $webform_submission);
+    $this->getWebform()->invokeHandlers('submitForm', $form, $form_state, $this->entity);
   }
 
   /**
diff --git a/src/WebformSubmissionStatesValidator.php b/src/WebformSubmissionStatesValidator.php
new file mode 100644
index 00000000..61a9da26
--- /dev/null
+++ b/src/WebformSubmissionStatesValidator.php
@@ -0,0 +1,435 @@
+<?php
+
+namespace Drupal\webform;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\webform\Plugin\WebformElementManagerInterface;
+
+/**
+ * Webform submission (server-side) states validator.
+ *
+ * @see \Drupal\webform\Element\WebformElementStates
+ * @see drupal_process_states()
+ */
+class WebformSubmissionStatesValidator implements WebformSubmissionStatesValidatorInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * State aliases.
+   *
+   * @var array
+   *
+   * @see Drupal.states.State.aliases
+   */
+  protected $aliases = [
+    'enabled' => '!disabled',
+    'invisible' => '!visible',
+    'invalid' => '!valid',
+    'optional' => '!required',
+    'filled' => '!empty',
+    'unchecked' => '!checked',
+    'expanded' => '!collapsed',
+    'open' => '!collapsed',
+    'closed' => 'collapsed',
+    // Below states are never used by the #states API.
+    // 'untouched' => '!touched',
+    // 'irrelevant' => '!relevant',
+    // 'readwrite' => '!readonly',
+  ];
+
+  /**
+   * The webform element manager.
+   *
+   * @var \Drupal\webform\Plugin\WebformElementManagerInterface
+   */
+  protected $elementManager;
+
+  /**
+   * Constructs a WebformSubmissionStatesValidator object.
+   *
+   * @param \Drupal\webform\Plugin\WebformElementManagerInterface $element_manager
+   *   The webform element manager.
+   */
+  public function __construct(WebformElementManagerInterface $element_manager) {
+    $this->elementManager = $element_manager;
+  }
+
+  /****************************************************************************/
+  // Webform submission form methods.
+  /****************************************************************************/
+
+  /**
+   * {@inheritdoc}
+   *
+   * @see \Drupal\webform\WebformSubmissionForm::buildForm
+   */
+  public function buildForm(array &$form, FormStateInterface $form_state) {
+    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
+    $webform_submission = $form_state->getFormObject()->getEntity();
+
+    // Get visible form elements.
+    $elements = &$this->getVisibleElements($form);
+
+    // Loop through visible elements with #states.
+    foreach ($elements as $element_key => &$element) {
+      if (empty($element['#states'])) {
+        continue;
+      }
+
+      foreach ($element['#states'] as $state => $conditions) {
+        if ($this->isConditionsTargetsVisible($conditions, $elements)) {
+          continue;
+        }
+
+        $result = $this->validateConditions($conditions, $webform_submission);
+        // Skip invalid conditions.
+        if ($result === NULL) {
+          continue;
+        }
+
+        // Remove #states state/conditions.
+        unset($element['#states'][$state]);
+
+        // Process state/negate.
+        list($state, $negate) = $this->processState($state);
+
+        // Negate the result.
+        $result = ($negate) ? !$result : $result;
+
+        // Apply result to element state.
+        switch ($state) {
+          case 'required':
+            $element['#required'] = $result;
+            break;
+
+          case 'disabled':
+            $element['#disabled'] = $result;
+            break;
+
+          case 'visible':
+            $element['#access'] = $result;
+            break;
+
+          case 'collapsed':
+            $element['#open'] = !$result;
+            break;
+
+          case 'checked':
+            $element['#default_value'] = $result;
+            break;
+        }
+      }
+
+      if (empty($element['#states'])) {
+        unset($element['#states']);
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @see \Drupal\webform\WebformSubmissionForm::validateForm
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state)  {
+    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
+    $webform_submission = $form_state->getFormObject()->getEntity();
+
+    // Get visible form elements.
+    $elements = $this->getVisibleElements($form);
+
+    // Loop through visible elements with #states.
+    foreach ($elements as $element_key => $element) {
+      if (empty($element['#states'])) {
+        continue;
+      }
+
+      foreach ($element['#states'] as $state => $conditions) {
+        if (!in_array($state,  ['required', 'optional'])) {
+          continue;
+        }
+
+        $is_required = $this->validateConditions($conditions, $webform_submission);
+        $is_required = ($state == 'optional') ? !$is_required : $is_required;
+        if ($is_required && empty($form_state->getValue($element_key))) {
+          $this->setRequiredError($element, $form_state);
+        }
+      }
+    }
+  }
+
+  /****************************************************************************/
+  // Condition methods.
+  /****************************************************************************/
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateConditions(array $conditions, WebformSubmissionInterface $webform_submission) {
+    /** @var \Drupal\webform\Plugin\WebformElementManagerInterface $element_manager */
+    $element_manager = \Drupal::service('plugin.manager.webform.element');
+
+    $condition_logic = 'and';
+    $condition_results = [];
+    foreach ($conditions as $index => $value) {
+      if (is_string($value) && in_array($value, ['and', 'or', 'xor'])) {
+        $condition_logic = $value;
+        continue;
+      }
+      elseif (is_int($index)) {
+        $selector = key($value);
+        $condition = $value[$selector];
+      }
+      else {
+        $selector = $index;
+        $condition = $value;
+      }
+
+      // Ignore invalid selector and return NULL.
+      $input_name = static::getSelectorInputName($selector);
+      if (!$input_name) {
+        return NULL;
+      }
+
+      $element_key = static::getInputNameAsArray($input_name, 0);
+
+      // Ignore missing dependee element and return NULL.
+      $element = $webform_submission->getWebform()->getElement($element_key);
+      if (!$element) {
+        return NULL;
+      }
+
+      $trigger_state = key($condition);
+      $trigger_value = $condition[$trigger_state];
+
+      $element_handler = $element_manager->getElementInstance($element);
+      $element_value = $element_handler->getElementSelectorInputValue($selector, $trigger_state, $element, $webform_submission);
+
+      // Process trigger state/negate.
+      list($trigger_state, $trigger_negate) = $this->processState($trigger_state);
+
+      // Process triggers (aka remote conditions).
+      // @see \Drupal\webform\Element\WebformElementStates::processWebformStates
+      switch ($trigger_state) {
+        case 'empty':
+          $result = (empty($element_value) === (boolean) $trigger_value);
+          break;
+
+        case 'checked':
+          $result = ((boolean) $element_value === (boolean) $trigger_value);
+          break;
+
+        case 'value':
+          if ($element_handler->hasMultipleValues($element)) {
+            $trigger_values = (array) $trigger_value;
+            $element_values = (array) $element_value;
+            $result = (array_intersect($trigger_values, $element_values)) ? TRUE : FALSE;
+          }
+          else {
+            $result = ((string) $element_value === (string) $trigger_value);
+          }
+          break;
+
+        default:
+          return NULL;
+      }
+
+      $condition_results[$selector] = ($trigger_negate) ? !$result : $result;
+    }
+
+    // Process condition logic. (XOR, AND, or OR)
+    $conditions_sum = array_sum($condition_results);
+    $conditions_total = count($condition_results);
+    switch ($condition_logic) {
+      case 'xor':
+        return ($conditions_sum && $conditions_sum !== $conditions_total);
+
+      case 'or':
+        return (boolean) $conditions_sum;
+
+      case 'and':
+        return ($conditions_sum === $conditions_total);
+
+      default:
+        // Never called.
+        return NULL;
+    }
+  }
+
+  /****************************************************************************/
+  // State methods.
+  /****************************************************************************/
+
+  /**
+   * Process state by mapping aliases and negation.
+   *
+   * @param string $state
+   *
+   * @return array
+   *   An array containing state and negate
+   */
+  protected function processState($state) {
+    // Set aliases.
+    if (isset($this->aliases[$state])) {
+      $state = $this->aliases[$state];
+    }
+
+    // Set negate.
+    $negate = FALSE;
+    if ($state[0] === '!') {
+      $negate = TRUE;
+      $state = ltrim($state, '!');
+    }
+
+    return [$state, $negate];
+  }
+
+  /****************************************************************************/
+  // Validation methods.
+  /****************************************************************************/
+
+  /**
+   * Set required validation message for an element.
+   *
+   * @param array $element
+   *   An element.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  protected function setRequiredError(array $element, FormStateInterface $form_state) {
+    if (isset($element['#required_error'])) {
+      $form_state->setError($element, $element['#required_error']);
+    }
+    elseif (isset($element['#title'])) {
+      $form_state->setError($element, $this->t('@name field is required.', ['@name' => $element['#title']]));
+    }
+    else {
+      $form_state->setError($element);
+    }
+  }
+
+  /****************************************************************************/
+  // Element methods.
+  /****************************************************************************/
+
+  /**
+   * Get visible elements with from a form.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   *
+   * @return array
+   *   Visible elements.
+   */
+  protected function &getVisibleElements(array &$form) {
+    $elements = [];
+    $this->getVisibleElementsRecusive($elements, $form);
+    return $elements;
+  }
+
+  /**
+   * Recurse through a form and get visible elements.
+   *
+   * @param array $elements
+   *   Visible elements with #states property
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   */
+  protected function getVisibleElementsRecusive(array &$elements, array &$form) {
+    foreach ($form as $key => &$element) {
+      if (Element::property($key) || !is_array($element)) {
+        continue;
+      }
+
+      if (isset($element['#access']) && $element['#access'] === FALSE) {
+        continue;
+      }
+
+      $elements[$key] = &$element;
+
+      $this->getVisibleElementsRecusive($elements, $element);
+    }
+  }
+
+  /**
+   * Determine if #states conditions targets are visible.
+   *
+   * @param array $conditions
+   *   An associative array containing conditions.
+   * @param array $elements
+   *   An associative array of visible elements.
+   *
+   * @return bool
+   *   TRUE if ALL #states conditions targets are visible.
+   */
+  protected function isConditionsTargetsVisible(array $conditions, array $elements) {
+    foreach ($conditions as $index => $value) {
+      if (is_string($value) && in_array($value, ['and', 'or', 'xor'])) {
+        continue;
+      }
+      elseif (is_int($index)) {
+        $selector = key($value);
+      }
+      else {
+        $selector = $index;
+      }
+
+      // Ignore invalid selector and return FALSE.
+      $input_name = static::getSelectorInputName($selector);
+      if (!$input_name) {
+        return FALSE;
+      }
+
+      $element_key = static::getInputNameAsArray($input_name, 0);
+      return isset($elements[$element_key]) ? TRUE : FALSE;
+    }
+  }
+
+  /****************************************************************************/
+  // Static input and selector methods.
+  // @see \Drupal\webform\Plugin\WebformElementBase::getElementSelectorInputValue
+  /****************************************************************************/
+
+  /**
+   * Get input name from CSS :input[name="*"] selector.
+   *
+   * @param $selector
+   *   A CSS :input[name="*"] selector.
+   *
+   * @return string|NULL
+   *   The input name or NULL if selector can not be parsed
+   */
+  public static function getSelectorInputName($selector) {
+    return (preg_match('/\:input\[name="([^"]+)"\]/', $selector, $match)) ? $match[1] : NULL;
+  }
+
+  /**
+   * Parse input name which can container nested elements defined using #tree.
+   *
+   * Converts 'input[subkey]' into ['input', 'subkey'].
+   *
+   * @param string $name
+   *   The input name.
+   * @param int $index
+   *   A specific input name index to be returned.
+   *
+   * @return array|string
+   *   An array containing the input name and keys or specific input name.
+   *
+   * @see http://php.net/manual/en/faq.html.php#faq.html.arrays
+   */
+  public static function getInputNameAsArray($name, $index = NULL){
+    $name = str_replace(['][', '[', ']'], ['|', '|', ''], $name);
+    $array = explode('|', $name);
+    if ($index !== NULL) {
+      return isset($array[$index]) ? $array[$index] : NULL;
+    }
+    else {
+      return $array;
+    }
+  }
+
+}
diff --git a/src/WebformSubmissionStatesValidatorInterface.php b/src/WebformSubmissionStatesValidatorInterface.php
new file mode 100644
index 00000000..73d3c4e1
--- /dev/null
+++ b/src/WebformSubmissionStatesValidatorInterface.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Drupal\webform;
+
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Provides an interface defining a webform states validator.
+ */
+interface WebformSubmissionStatesValidatorInterface {
+
+  /**
+   * Apply form #states to visible elements.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public function buildForm(array &$form, FormStateInterface $form_state);
+
+  /**
+   * Validate form #states for visible elements.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state);
+
+  /**
+   * Validate #state conditions.
+   *
+   * @param array $conditions
+   *   An associative array containing conditions.
+   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
+   *   A webform submission.
+   *
+   * @return bool|NULL
+   *   TRUE if conditions validate. NULL if conditions can't be processed.
+   *
+   * @see drupal_process_states()
+   */
+  public function validateConditions(array $conditions, WebformSubmissionInterface $webform_submission);
+
+}
\ No newline at end of file
diff --git a/tests/modules/webform_test/config/install/webform.webform.test_form_states_server_required.yml b/tests/modules/webform_test/config/install/webform.webform.test_form_states_server_required.yml
new file mode 100644
index 00000000..64d295d0
--- /dev/null
+++ b/tests/modules/webform_test/config/install/webform.webform.test_form_states_server_required.yml
@@ -0,0 +1,358 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+uid: null
+template: false
+id: test_form_states_server_required
+title: 'Test: Form API #states server-side required validation'
+description: 'Test Drupal''s #states API via server-side required validation.'
+category: 'Test: Form API #states'
+elements: |
+  multiple_triggers:
+    '#type': details
+    '#title': multiple_triggers
+    '#open': true
+    trigger_checkbox:
+      '#type': checkbox
+      '#title': trigger_checkbox
+    trigger_textfield:
+      '#type': textfield
+      '#title': trigger_textfield
+    trigger_select:
+      '#type': select
+      '#title': trigger_select
+      '#options':
+        option: Option
+  multiple_dependents:
+    '#type': details
+    '#title': multiple_dependents
+    '#open': true
+    dependent_textfield_required_and:
+      '#type': textfield
+      '#title': dependent_textfield_required_and
+      '#description': '<b>Required:</b> checkbox:checked=true AND textfield:filled=true AND select:value=options'
+      '#states':
+        required:
+          ':input[name="trigger_checkbox"]':
+            checked: true
+          ':input[name="trigger_textfield"]':
+            filled: true
+          ':input[name="trigger_select"]':
+            value: option
+    dependent_textfield_required_or:
+      '#type': textfield
+      '#title': dependent_textfield_required_or
+      '#description': '<b>Required:</b> checkbox:checked=true OR textfield:filled=true OR select:value=options'
+      '#states':
+        required:
+          - ':input[name="trigger_checkbox"]':
+              checked: true
+          - or
+          - ':input[name="trigger_textfield"]':
+              filled: true
+          - or
+          - ':input[name="trigger_select"]':
+              value: option
+  checkboxes_trigger_details:
+    '#type': details
+    '#title': checkboxes_trigger
+    '#open': true
+    checkboxes_trigger:
+      '#type': checkboxes
+      '#title': checkboxes_trigger
+      '#options':
+        one: One
+        two: Two
+        three: Three
+    checkboxes_dependent_required:
+      '#type': textfield
+      '#title': checkboxes_dependent_required
+      '#description': '<b>Required:</b> checkboxes[one]:checked=true'
+      '#states':
+        required:
+          ':input[name="checkboxes_trigger[one]"]':
+            checked: true
+  text_format_trigger_details:
+    '#type': details
+    '#title': text_format_trigger
+    '#open': true
+    text_format_trigger:
+      '#type': text_format
+      '#title': text_format_trigger
+      '#format': basic_html
+    text_format_dependent_required:
+      '#type': textfield
+      '#title': text_format_dependent_required
+      '#description': '<b>Required:</b> text_format_trigger[value]:filled=true OR text_format_trigger[format]:value=full_html'
+      '#states':
+        required:
+          - ':input[name="text_format_trigger[value]"]':
+              filled: true
+          - or
+          - ':input[name="text_format_trigger[format]"]':
+              value: full_html
+  select_other_trigger_details:
+    '#type': details
+    '#title': select_other_trigger
+    '#open': true
+    select_other_trigger:
+      '#type': webform_select_other
+      '#title': select_other_trigger
+      '#options':
+        one: One
+        two: Two
+        three: Three
+    select_other_dependent_required:
+      '#type': textfield
+      '#title': select_other_dependent_required
+      '#description': '<b>Required:</b> select_other_trigger[select]:value=one OR select_other_trigger[other]:filled=true'
+      '#states':
+        required:
+          - ':input[name="select_other_trigger[select]"]':
+              value: one
+          - or
+          - ':input[name="select_other_trigger[other]"]':
+              filled: true
+  select_other_multiple_trigger_details:
+    '#type': details
+    '#title': select_other_multiple_trigger
+    '#open': true
+    select_other_multiple_trigger:
+      '#type': webform_select_other
+      '#title': select_other_multiple_trigger
+      '#multiple': true
+      '#options':
+        one: One
+        two: Two
+        three: Three
+    select_other_multiple_dependent_required:
+      '#type': textfield
+      '#title': select_other_multiple_dependent_required
+      '#description': '<b>Required:</b> select_other_multiple_trigger[select]:value=one OR select_other_multiple_trigger[other]:filled=true'
+      '#states':
+        required:
+          - ':input[name="select_other_multiple_trigger[select]"]':
+              value: one
+          - or
+          - ':input[name="select_other_multiple_trigger[other]"]':
+              filled: true
+  email_confirm_trigger_details:
+    '#type': details
+    '#title': email_confirm_trigger
+    '#open': true
+    email_confirm_trigger:
+      '#type': webform_email_confirm
+      '#title': email_confirm_trigger
+    email_confirm_dependent_required:
+      '#type': textfield
+      '#title': email_confirm_dependent_required
+      '#description': '<b>Required:</b> email_confirm_trigger[mail_1]:filled=true'
+      '#states':
+        required:
+          ':input[name="email_confirm_trigger[mail_1]"]':
+            filled: true
+  likert_trigger_details:
+    '#type': details
+    '#title': likert_trigger
+    '#open': true
+    likert_trigger:
+      '#type': webform_likert
+      '#title': likert_trigger
+      '#questions':
+        q1: q1
+        q2: q2
+      '#answers':
+        a1: a1
+        a2: a2
+    likert_dependent_required:
+      '#type': textfield
+      '#title': likert_dependent_required
+      '#description': '<b>Required:</b> likert_trigger[q1]:value=a1'
+      '#states':
+        required:
+          ':input[name="likert_trigger[q1]"]':
+            value: a1
+  datelist_trigger_details:
+    '#type': details
+    '#title': datelist_trigger
+    '#open': true
+    datelist_trigger:
+      '#type': datelist
+      '#title': datelist_trigger
+      '#date_part_order':
+        - year
+        - month
+        - day
+        - hour
+        - minute
+        - second
+        - ampm
+    datelist_dependent_required:
+      '#type': textfield
+      '#title': datelist_dependent_required
+      '#description': '<b>Required:</b> datelist_trigger[*]:!value='''''
+      '#states':
+        required:
+          ':input[name="datelist_trigger[year]"]':
+            '!value': ''
+          ':input[name="datelist_trigger[month]"]':
+            '!value': ''
+          ':input[name="datelist_trigger[day]"]':
+            '!value': ''
+          ':input[name="datelist_trigger[hour]"]':
+            '!value': ''
+          ':input[name="datelist_trigger[minute]"]':
+            '!value': ''
+          ':input[name="datelist_trigger[second]"]':
+            '!value': ''
+          ':input[name="datelist_trigger[ampm]"]':
+            '!value': ''
+  datetime_trigger_details:
+    '#type': details
+    '#title': datetime_trigger
+    '#open': true
+    datetime_trigger:
+      '#type': datetime
+      '#title': datetime_trigger
+    datetime_dependent_required:
+      '#type': textfield
+      '#title': datetime_dependent_required
+      '#description': '<b>Required:</b> datetime_trigger[date]:!value='''' AND datetime_trigger[time]:!value='''''
+      '#states':
+        required:
+          ':input[name="datetime_trigger[date]"]':
+            '!value': ''
+          ':input[name="datetime_trigger[time]"]':
+            '!value': ''
+  address_trigger_details:
+    '#type': details
+    '#title': address_trigger
+    '#open': true
+    address_trigger:
+      '#type': webform_address
+      '#title': address_trigger
+    address_dependent_required:
+      '#type': textfield
+      '#title': address_dependent_required
+      '#description': '<b>Required:</b> address_trigger[*]:!value='''''
+      '#states':
+        required:
+          ':input[name="address_trigger[address]"]':
+            '!value': ''
+          ':input[name="address_trigger[address_2]"]':
+            '!value': ''
+          ':input[name="address_trigger[city]"]':
+            '!value': ''
+          ':input[name="address_trigger[state_province]"]':
+            '!value': ''
+          ':input[name="address_trigger[postal_code]"]':
+            '!value': ''
+          ':input[name="address_trigger[country]"]':
+            '!value': ''
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: true
+  form_unsaved: false
+  form_disable_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  submission_label: ''
+  submission_log: false
+  submission_user_columns: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_start_label: ''
+  wizard_complete: true
+  wizard_complete_label: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: message
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  limit_total: null
+  limit_total_message: ''
+  limit_user: null
+  limit_user_message: ''
+  purge: none
+  purge_days: null
+  entity_limit_total: null
+  entity_limit_user: null
+  results_disabled: true
+  results_disabled_ignore: true
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/tests/modules/webform_test/config/install/webform.webform.test_form_states_server_wizard.yml b/tests/modules/webform_test/config/install/webform.webform.test_form_states_server_wizard.yml
new file mode 100644
index 00000000..4e5cebd7
--- /dev/null
+++ b/tests/modules/webform_test/config/install/webform.webform.test_form_states_server_wizard.yml
@@ -0,0 +1,302 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+uid: null
+template: false
+id: test_form_states_server_wizard
+title: 'Test: Form API #states server-side wizard pages'
+description: 'Test Drupal''s #states API for wizard pages via server-side.'
+category: 'Test: Form API #states'
+elements: |
+  page_01:
+    '#type': webform_wizard_page
+    '#title': page_01
+    page_01_trigger_checkbox:
+      '#type': checkbox
+      '#title': trigger_checkbox
+    page_01_textfield_required:
+      '#type': textfield
+      '#title': page_01_textfield_required
+      '#default_value': '{default_value}'
+      '#description': '<b>Required:</b> page_01_trigger_checkbox:checked'
+      '#states':
+        required:
+          ':input[name="page_01_trigger_checkbox"]':
+            checked: true
+    page_01_textfield_optional:
+      '#type': textfield
+      '#title': page_01_textfield_optional
+      '#default_value': '{default_value}'
+      '#description': '<b>Optional:</b> page_01_trigger_checkbox:checked'
+      '#states':
+        optional:
+          ':input[name="page_01_trigger_checkbox"]':
+            checked: true
+    page_01_textfield_disabled:
+      '#type': textfield
+      '#title': page_01_textfield_disabled
+      '#description': '<b>Disabled:</b> page_01_trigger_checkbox:checked'
+      '#states':
+        disabled:
+          ':input[name="page_01_trigger_checkbox"]':
+            checked: true
+    page_01_textfield_enabled:
+      '#type': textfield
+      '#title': page_01_textfield_enabled
+      '#description': '<b>Enabled:</b> page_01_trigger_checkbox:checked'
+      '#states':
+        enabled:
+          ':input[name="page_01_trigger_checkbox"]':
+            checked: true
+    page_01_textfield_visible:
+      '#type': textfield
+      '#title': page_01_textfield_visible
+      '#description': '<b>Visible:</b> page_01_trigger_checkbox:checked'
+      '#states':
+        visible:
+          ':input[name="page_01_trigger_checkbox"]':
+            checked: true
+    page_01_textfield_invisible:
+      '#type': textfield
+      '#title': page_01_textfield_invisible
+      '#description': '<b>Invisible:</b> page_01_trigger_checkbox:checked'
+      '#states':
+        invisible:
+          ':input[name="page_01_trigger_checkbox"]':
+            checked: true
+    page_01_checkbox_checked:
+      '#type': checkbox
+      '#title': page_01_checkbox_checked
+      '#description': '<b>Checked:</b> page_01_trigger_checkbox:checked'
+      '#states':
+        checked:
+          ':input[name="page_01_trigger_checkbox"]':
+            checked: true
+    page_01_checkbox_unchecked:
+      '#type': checkbox
+      '#title': page_01_checkbox_unchecked
+      '#description': '<b>Unchecked:</b> page_01_trigger_checkbox:checked'
+      '#states':
+        unchecked:
+          ':input[name="page_01_trigger_checkbox"]':
+            checked: true
+    page_01_details_expanded:
+      '#type': details
+      '#title': page_01_details_open
+      '#description': '<b>Expanded:</b> page_01_trigger_checkbox:checked'
+      '#attributes':
+        data-webform-details-nosave: true
+      '#states':
+        expanded:
+          ':input[name="page_01_trigger_checkbox"]':
+            checked: true
+    page_01_details_collapsed:
+      '#type': details
+      '#title': page_01_details_collapsed
+      '#description': '<b>Collapsed:</b> page_01_trigger_checkbox:checked'
+      '#attributes':
+        data-webform-details-nosave: true
+      '#states':
+        collapsed:
+          ':input[name="page_01_trigger_checkbox"]':
+            checked: true
+  page_02:
+    '#type': webform_wizard_page
+    '#title': page_02
+    page_01_trigger_checkbox_computed:
+      '#type': webform_computed_twig
+      '#title': trigger_checkbox
+      '#value': '{{ data.page_01_trigger_checkbox ? ''Yes'' : ''No'' }}'
+    page_02_textfield_required:
+      '#type': textfield
+      '#title': page_02_textfield_required
+      '#default_value': '{default_value}'
+      '#description': '<b>Required:</b> page_01_trigger_checkbox:checked'
+      '#states':
+        required:
+          ':input[name="page_01_trigger_checkbox"]':
+            checked: true
+    page_02_textfield_optional:
+      '#type': textfield
+      '#title': page_02_textfield_optional
+      '#default_value': '{default_value}'
+      '#description': '<b>Optional:</b> page_01_trigger_checkbox:checked'
+      '#states':
+        optional:
+          ':input[name="page_01_trigger_checkbox"]':
+            checked: true
+    page_02_textfield_disabled:
+      '#type': textfield
+      '#title': page_02_textfield_disabled
+      '#description': '<b>Disabled:</b> page_01_trigger_checkbox:checked'
+      '#states':
+        disabled:
+          ':input[name="page_01_trigger_checkbox"]':
+            checked: true
+    page_02_textfield_enabled:
+      '#type': textfield
+      '#title': page_02_textfield_enabled
+      '#description': '<b>Enabled:</b> page_01_trigger_checkbox:checked'
+      '#states':
+        enabled:
+          ':input[name="page_01_trigger_checkbox"]':
+            checked: true
+    page_02_textfield_visible:
+      '#type': textfield
+      '#title': page_02_textfield_visible
+      '#description': '<b>Visible:</b> page_01_trigger_checkbox:checked'
+      '#states':
+        visible:
+          ':input[name="page_01_trigger_checkbox"]':
+            checked: true
+    page_02_textfield_invisible:
+      '#type': textfield
+      '#title': 'page_02_textfield_invisible:'
+      '#description': '<b>Invisible:</b> page_01_trigger_checkbox:checked'
+      '#states':
+        invisible:
+          ':input[name="page_01_trigger_checkbox"]':
+            checked: true
+    page_02_checkbox_checked:
+      '#type': checkbox
+      '#title': page_02_checkbox_checked
+      '#description': '<b>Checked:</b> page_01_trigger_checkbox:checked'
+      '#states':
+        checked:
+          ':input[name="page_01_trigger_checkbox"]':
+            checked: true
+    page_02_checkbox_unchecked:
+      '#type': checkbox
+      '#title': page_02_checkbox_unchecked
+      '#description': '<b>Unchecked:</b> page_01_trigger_checkbox:checked'
+      '#states':
+        unchecked:
+          ':input[name="page_01_trigger_checkbox"]':
+            checked: true
+    page_02_details_expanded:
+      '#type': details
+      '#title': page_02_details_open
+      '#description': '<b>Expanded:</b> page_01_trigger_checkbox:checked'
+      '#attributes':
+        data-webform-details-nosave: true
+      '#states':
+        expanded:
+          ':input[name="page_01_trigger_checkbox"]':
+            checked: true
+    page_02_details_collapsed:
+      '#type': details
+      '#title': page_02_details_collapsed
+      '#description': '<b>Collapsed:</b> page_01_trigger_checkbox:checked'
+      '#attributes':
+        data-webform-details-nosave: true
+      '#states':
+        collapsed:
+          ':input[name="page_01_trigger_checkbox"]':
+            checked: true
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: true
+  form_unsaved: false
+  form_disable_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  submission_label: ''
+  submission_log: false
+  submission_user_columns: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_start_label: ''
+  wizard_complete: true
+  wizard_complete_label: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: message
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  limit_total: null
+  limit_total_message: ''
+  limit_user: null
+  limit_user_message: ''
+  purge: none
+  purge_days: null
+  entity_limit_total: null
+  entity_limit_user: null
+  results_disabled: true
+  results_disabled_ignore: true
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
\ No newline at end of file
diff --git a/webform.services.yml b/webform.services.yml
index e3fb59a5..483ea82e 100644
--- a/webform.services.yml
+++ b/webform.services.yml
@@ -69,6 +69,10 @@ services:
     class: Drupal\webform\WebformTokenManager
     arguments: ['@module_handler', '@token']
 
+  webform_submission.states_validator:
+    class: Drupal\webform\WebformSubmissionStatesValidator
+    arguments: ['@plugin.manager.webform.element']
+
   # Breadcrumb builder.
 
   webform.breadcrumb:
