diff --git a/core/lib/Drupal/Core/Entity/ContentEntityForm.php b/core/lib/Drupal/Core/Entity/ContentEntityForm.php
index 16e3df8694..6d882669bf 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityForm.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityForm.php
@@ -444,12 +444,16 @@ protected function addRevisionableFormFields(array &$form) {
     if ($log_message_field && isset($form[$log_message_field])) {
       $form[$log_message_field] += [
         '#group' => 'revision_information',
-        '#states' => [
-          'visible' => [
-            ':input[name="revision"]' => ['checked' => TRUE],
-          ],
-        ],
       ];
+      if ($form['revision']['#access']) {
+        $form[$log_message_field] += [
+          '#states' => [
+            'visible' => [
+              ':input[name="revision"]' => ['checked' => TRUE],
+            ],
+          ],
+        ];
+      }
     }
   }
 
diff --git a/core/misc/states.es6.js b/core/misc/states.es6.js
index b45e1e0504..19951c2d8b 100644
--- a/core/misc/states.es6.js
+++ b/core/misc/states.es6.js
@@ -138,6 +138,8 @@
     Object.keys(this.dependees || {}).forEach((selector) => {
       this.initializeDependee(selector, this.dependees[selector]);
     });
+    // Reevaluate to execute initial states.
+    this.reevaluate();
   };
 
   /**
@@ -201,12 +203,18 @@
         this.values[selector][state.name] = null;
 
         // Monitor state changes of the specified state for this dependee.
-        $(selector).on(`state:${state}`, { selector, state }, (e) => {
+        let $dependee = $(selector);
+        $dependee.on(`state:${state}`, { selector, state }, e => {
           this.update(e.data.selector, e.data.state, e.value);
         });
 
         // Make sure the event we just bound ourselves to is actually fired.
         new states.Trigger({ selector, state });
+
+        // Update initial state value, if set by data attribute.
+        if ($dependee.data(`trigger:${state.name}`) !== undefined) {
+          this.values[selector][state.name] = $dependee.data(`trigger:${state.name}`);
+        }
       });
     },
 
@@ -437,7 +445,7 @@
 
       // Only call the trigger initializer when it wasn't yet attached to this
       // element. Otherwise we'd end up with duplicate events.
-      if (!this.element.data(`trigger:${this.state}`)) {
+      if (this.element.data(`trigger:${this.state}`) === undefined) {
         this.initialize();
       }
     }
@@ -452,15 +460,16 @@
 
       if (typeof trigger === 'function') {
         // We have a custom trigger initialization function.
+        // Create data attribute for trigger, to prevent multiple
+        // calls to this method.
+        this.element.data('trigger:' + this.state, null);
+        // Call custom trigger initialization function.
         trigger.call(window, this.element);
       } else {
         Object.keys(trigger || {}).forEach((event) => {
           this.defaultTrigger(event, trigger[event]);
         });
       }
-
-      // Mark this trigger as initialized for this element.
-      this.element.data(`trigger:${this.state}`, true);
     },
 
     /**
@@ -474,6 +483,9 @@
     defaultTrigger(event, valueFn) {
       let oldValue = valueFn.call(this.element);
 
+      // Save current value to element data attribute.
+      this.element.data('trigger:' + this.state, oldValue);
+
       // Attach the event callback.
       this.element.on(
         event,
@@ -487,20 +499,11 @@
               oldValue,
             });
             oldValue = value;
+            // Save current value to element data attribute.
+            this.element.data('trigger:' + this.state, value);
           }
         }, this),
       );
-
-      states.postponed.push(
-        $.proxy(function () {
-          // Trigger the event once for initialization purposes.
-          this.element.trigger({
-            type: `state:${this.state}`,
-            value: oldValue,
-            oldValue: null,
-          });
-        }, this),
-      );
     },
   };
 
diff --git a/core/misc/states.js b/core/misc/states.js
index 49330d978a..b80a32cd1e 100644
--- a/core/misc/states.js
+++ b/core/misc/states.js
@@ -72,6 +72,7 @@
     Object.keys(this.dependees || {}).forEach(function (selector) {
       _this.initializeDependee(selector, _this.dependees[selector]);
     });
+    this.reevaluate();
   };
 
   states.Dependent.comparisons = {
@@ -99,7 +100,8 @@
 
         state = states.State.sanitize(state);
         _this2.values[selector][state.name] = null;
-        $(selector).on("state:".concat(state), {
+        var $dependee = $(selector);
+        $dependee.on("state:".concat(state), {
           selector: selector,
           state: state
         }, function (e) {
@@ -109,6 +111,10 @@
           selector: selector,
           state: state
         });
+
+        if ($dependee.data("trigger:".concat(state.name)) !== undefined) {
+          _this2.values[selector][state.name] = $dependee.data("trigger:".concat(state.name));
+        }
       });
     },
     compare: function compare(reference, selector, state) {
@@ -206,7 +212,7 @@
     if (this.state in states.Trigger.states) {
       this.element = $(this.selector);
 
-      if (!this.element.data("trigger:".concat(this.state))) {
+      if (this.element.data("trigger:".concat(this.state)) === undefined) {
         this.initialize();
       }
     }
@@ -219,17 +225,17 @@
       var trigger = states.Trigger.states[this.state];
 
       if (typeof trigger === 'function') {
+        this.element.data('trigger:' + this.state, null);
         trigger.call(window, this.element);
       } else {
         Object.keys(trigger || {}).forEach(function (event) {
           _this3.defaultTrigger(event, trigger[event]);
         });
       }
-
-      this.element.data("trigger:".concat(this.state), true);
     },
     defaultTrigger: function defaultTrigger(event, valueFn) {
       var oldValue = valueFn.call(this.element);
+      this.element.data('trigger:' + this.state, oldValue);
       this.element.on(event, $.proxy(function (e) {
         var value = valueFn.call(this.element, e);
 
@@ -240,15 +246,9 @@
             oldValue: oldValue
           });
           oldValue = value;
+          this.element.data('trigger:' + this.state, value);
         }
       }, this));
-      states.postponed.push($.proxy(function () {
-        this.element.trigger({
-          type: "state:".concat(this.state),
-          value: oldValue,
-          oldValue: null
-        });
-      }, this));
     }
   };
   states.Trigger.states = {
diff --git a/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.routing.yml b/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.routing.yml
index 7b1a826788..e9b37137f2 100644
--- a/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.routing.yml
+++ b/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.routing.yml
@@ -45,3 +45,11 @@ ajax_forms_test.ajax_element_form:
     _form: '\Drupal\ajax_forms_test\Form\AjaxFormsTestAjaxElementsForm'
   requirements:
     _access: 'TRUE'
+
+ajax_forms_test.states_form:
+  path: '/ajax_forms_test_states_form'
+  defaults:
+    _title: 'Ajax Form with States API'
+    _form: '\Drupal\ajax_forms_test\Form\AjaxFormsTestStatesForm'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php b/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php
index 204f0c586e..9b9ee1e5b4 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php
@@ -3,6 +3,7 @@
 namespace Drupal\FunctionalJavascriptTests;
 
 use Behat\Mink\Element\Element;
+use Behat\Mink\Element\ElementInterface;
 use Behat\Mink\Element\NodeElement;
 use Behat\Mink\Exception\ElementHtmlException;
 use Behat\Mink\Exception\ElementNotFoundException;
@@ -49,6 +50,157 @@ function isAjaxing(instance) {
     }
   }
 
+  /**
+   * Asserts that the specific element is visible on the current page.
+   *
+   * @param string $selector_type
+   *   The element selector type (css, xpath).
+   * @param string|array $selector
+   *   The element selector.
+   * @param \Behat\Mink\Element\ElementInterface $container
+   *   The document to check against.
+   *
+   * @throws \Behat\Mink\Exception\ElementNotFoundException
+   *   When the element doesn't exist.
+   */
+  public function assertElementVisible($selector_type, $selector, ElementInterface $container = NULL) {
+    $node = $this->findNode($selector_type, $selector, $container);
+
+    $message = sprintf(
+      'Element "%s" is not visible.',
+      $this->getMatchingElementRepresentation($selector_type, $selector)
+    );
+    $this->assertElement($node->isVisible(), $message, $node);
+  }
+
+  /**
+   * Asserts that the specific element is not visible on the current page.
+   *
+   * @param string $selector_type
+   *   The element selector type (css, xpath).
+   * @param string|array $selector
+   *   The element selector.
+   * @param \Behat\Mink\Element\ElementInterface $container
+   *   The document to check against.
+   *
+   * @throws \Behat\Mink\Exception\ElementNotFoundException
+   *   When the element doesn't exist.
+   */
+  public function assertElementNotVisible($selector_type, $selector, ElementInterface $container = NULL) {
+    $node = $this->findNode($selector_type, $selector, $container);
+
+    $message = sprintf(
+      'Element "%s" is not visible.',
+      $this->getMatchingElementRepresentation($selector_type, $selector)
+    );
+    $this->assertElement(!$node->isVisible(), $message, $node);
+  }
+
+  /**
+   * Asserts that the specific element is not required on the current page.
+   *
+   * @param string $selector_type
+   *   The element selector type (css, xpath).
+   * @param string|array $selector
+   *   The element selector.
+   * @param \Behat\Mink\Element\ElementInterface $container
+   *   The document to check against.
+   *
+   * @throws \Behat\Mink\Exception\ElementNotFoundException
+   *   When the element doesn't exist.
+   */
+  public function assertElementRequired($selector_type, $selector, ElementInterface $container = NULL) {
+    $node = $this->findNode($selector_type, $selector, $container);
+
+    $message = sprintf(
+      'Element "%s" is required.',
+      $this->getMatchingElementRepresentation($selector_type, $selector)
+    );
+    $this->assertElement($node->getAttribute('required') == 'required', $message, $node);
+  }
+
+  /**
+   * Asserts that the specific element is not required on the current page.
+   *
+   * @param string $selector_type
+   *   The element selector type (css, xpath).
+   * @param string|array $selector
+   *   The element selector.
+   * @param \Behat\Mink\Element\ElementInterface $container
+   *   The document to check against.
+   *
+   * @throws \Behat\Mink\Exception\ElementNotFoundException
+   *   When the element doesn't exist.
+   */
+  public function assertElementOptional($selector_type, $selector, ElementInterface $container = NULL) {
+    $node = $this->findNode($selector_type, $selector, $container);
+
+    $message = sprintf(
+      'Element "%s" is optional.',
+      $this->getMatchingElementRepresentation($selector_type, $selector)
+    );
+    $this->assertElement(!$node->hasAttribute('required'), $message, $node);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function assertElement($condition, $message, Element $element) {
+    if ($condition) {
+      return;
+    }
+
+    throw new ElementHtmlException($message, $this->session->getDriver(), $element);
+  }
+
+  /**
+   * Find a node in the container specified usually the current page.
+   *
+   * @param string $selector_type
+   *   The element selector type (CSS, XPath).
+   * @param string $selector
+   *   The selector engine name. See ElementInterface::findAll() for the
+   *   supported selectors.
+   * @param \Behat\Mink\Element\ElementInterface $container
+   *   The document to check against.
+   *
+   * @return \Behat\Mink\Element\NodeElement
+   *   The node element if exists in the current container.
+   *
+   * @throws \Behat\Mink\Exception\ElementNotFoundException
+   *   When the element doesn't exist.
+   */
+  protected function findNode($selector_type, $selector, ElementInterface $container = NULL) {
+    $container = $container ?: $this->session->getPage();
+    $node = $container->find($selector_type, $selector);
+    if ($node === NULL) {
+      if (is_array($selector)) {
+        $selector = implode(' ', $selector);
+      }
+      throw new ElementNotFoundException($this->session->getDriver(), 'element', $selector_type, $selector);
+    }
+    return $node;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getMatchingElementRepresentation($selector_type, $selector, $plural = FALSE) {
+    $pluralization = $plural ? 's' : '';
+
+    if (in_array($selector_type, ['named', 'named_exact', 'named_partial'])
+      && is_array($selector) && 2 === count($selector)
+    ) {
+      return sprintf('%s%s matching locator "%s"', $selector[0], $pluralization, $selector[1]);
+    }
+
+    if (is_array($selector)) {
+      $selector = implode(' ', $selector);
+    }
+
+    return sprintf('element%s matching %s "%s"', $pluralization, $selector_type, $selector);
+  }
+
   /**
    * Waits for the specified selector and returns it when available.
    *
