diff --git a/core/includes/common.inc b/core/includes/common.inc
index 037c5b0..51b717f 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -568,7 +568,7 @@ function drupal_js_defaults($data = NULL) {
  * @param $elements
  *   A renderable array element having a #states property as described above.
  *
- * @see form_example_states_form()
+ * @see \Drupal\form_test\Form\JavascriptStatesForm
  */
 function drupal_process_states(&$elements) {
   $elements['#attached']['library'][] = 'core/drupal.states';
diff --git a/core/modules/system/tests/modules/form_test/form_test.routing.yml b/core/modules/system/tests/modules/form_test/form_test.routing.yml
index 42a6ba4..716d984 100644
--- a/core/modules/system/tests/modules/form_test/form_test.routing.yml
+++ b/core/modules/system/tests/modules/form_test/form_test.routing.yml
@@ -479,3 +479,10 @@ form_test.get_form:
     _form: '\Drupal\form_test\Form\FormTestGetForm'
   requirements:
     _access: 'TRUE'
+
+form_test.javascript_states_form:
+  path: '/form-test/javascript-states-form'
+  defaults:
+    _form: '\Drupal\form_test\Form\JavascriptStatesForm'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/system/tests/modules/form_test/src/Form/JavascriptStatesForm.php b/core/modules/system/tests/modules/form_test/src/Form/JavascriptStatesForm.php
new file mode 100644
index 0000000..7b166f8
--- /dev/null
+++ b/core/modules/system/tests/modules/form_test/src/Form/JavascriptStatesForm.php
@@ -0,0 +1,183 @@
+<?php
+
+namespace Drupal\form_test\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Builds a simple form to test states.
+ *
+ * @see \Drupal\FunctionalJavascriptTests\Core\Form\JavascriptStatesTest
+ */
+class JavascriptStatesForm extends FormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'javascript_states_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    // Triggers.
+    $form['checkbox_trigger'] = [
+      '#type' => 'checkbox',
+      '#title' => 'Checkbox trigger',
+    ];
+    $form['textfield_trigger'] = [
+      '#type' => 'textfield',
+      '#title' => 'Textfield trigger',
+    ];
+    $form['radios_trigger'] = [
+      '#type' => 'radios',
+      '#title' => 'Radios trigger',
+      '#options' => [
+        'value1' => 'Value 1',
+        'value2' => 'Value 2',
+        'value3' => 'Value 3',
+      ],
+    ];
+    $form['checkboxes_trigger'] = [
+      '#type' => 'checkboxes',
+      '#title' => 'Checkboxes trigger',
+      '#options' => [
+        'value1' => 'Value 1',
+        'value2' => 'Value 2',
+        'value3' => 'Value 3',
+      ],
+    ];
+    $form['select_trigger'] = [
+      '#type' => 'select',
+      '#title' => 'Select trigger',
+      '#options' => [
+        'value1' => 'Value 1',
+        'value2' => 'Value 2',
+        'value3' => 'Value 3',
+      ],
+      '#empty_value' => '_none',
+      '#empty_option' => '- None -',
+    ];
+
+    $form['separator'] = [
+      '#markup' => '<hr />',
+    ];
+
+    // Tested fields.
+    $form['textfield_invisible_when_checkbox_trigger_checked'] = [
+      '#type' => 'textfield',
+      '#title' => 'Texfield invisible when checkbox trigger checked',
+      '#states' => [
+        'invisible' => [
+          ':input[name="checkbox_trigger"]' => ['checked' => TRUE],
+        ],
+      ],
+    ];
+    $form['checkbox_unchecked_when_textfield_trigger_filled'] = [
+      '#type' => 'checkbox',
+      '#title' => 'Checkbox unchecked when textfield trigger filled',
+      '#default_value' => '1',
+      '#states' => [
+        'unchecked' => [
+          ':input[name="textfield_trigger"]' => ['filled' => TRUE],
+        ],
+      ],
+    ];
+    $form['textfield_visible_when_checkboxes_trigger_value2_checked'] = [
+      '#type' => 'textfield',
+      '#title' => 'Texfield visible when checkboxes trigger value2 checked',
+      '#states' => [
+        'visible' => [
+          ':input[name="checkboxes_trigger[value2]"]' => ['checked' => TRUE],
+        ],
+      ],
+    ];
+    $form['textfield_visible_when_checkboxes_trigger_value3_checked'] = [
+      '#type' => 'textfield',
+      '#title' => 'Texfield visible when checkboxes trigger value3 checked',
+      '#states' => [
+        'visible' => [
+          ':input[name="checkboxes_trigger[value3]"]' => ['checked' => TRUE],
+        ],
+      ],
+    ];
+    $form['item_visible_when_select_trigger_has_given_value'] = [
+      '#type' => 'item',
+      '#title' => 'Item visible when select trigger has given value',
+      '#states' => [
+        'visible' => [
+          ':input[name="select_trigger"]' => ['value' => 'value2'],
+        ],
+      ],
+    ];
+    $form['textfield_visible_when_select_trigger_has_given_value'] = [
+      '#type' => 'textfield',
+      '#title' => 'Textfield visible when select trigger has given value',
+      '#states' => [
+        'visible' => [
+          ':input[name="select_trigger"]' => ['value' => 'value3'],
+        ],
+      ],
+    ];
+    $form['item_visible_when_select_trigger_has_given_value_and_textfield_trigger_filled'] = [
+      '#type' => 'item',
+      '#title' => 'Item visible when select trigger has given value and textfield trigger filled',
+      '#states' => [
+        'visible' => [
+          ':input[name="select_trigger"]' => ['value' => 'value2'],
+          ':input[name="textfield_trigger"]' => ['filled' => TRUE],
+        ],
+      ],
+    ];
+    $form['textfield_visible_when_select_trigger_has_given_value_or_another'] = [
+      '#type' => 'textfield',
+      '#title' => 'Textfield visible when select trigger has given value or another',
+      '#states' => [
+        'visible' => [
+          ':input[name="select_trigger"]' => [
+            ['value' => 'value2'],
+            ['value' => 'value3'],
+          ],
+        ],
+      ],
+    ];
+    $form['fieldset_visible_when_radios_trigger_has_given_value'] = [
+      '#type' => 'fieldset',
+      '#title' => 'Fieldset visible when radio trigger has given value',
+      '#states' => [
+        'visible' => [
+          ':input[name="radios_trigger"]' => ['value' => 'value2'],
+        ],
+      ],
+    ];
+    $form['fieldset_visible_when_radios_trigger_has_given_value']['textfield_in_fieldset'] = [
+      '#type' => 'textfield',
+      '#title' => 'Textfield in fieldset',
+    ];
+    $form['details_expanded_when_checkbox_trigger_checked'] = [
+      '#type' => 'details',
+      '#title' => 'Details expanded when checkbox trigger checked',
+      '#states' => [
+        'expanded' => [
+          ':input[name="checkbox_trigger"]' => ['checked' => TRUE],
+        ],
+      ],
+    ];
+    $form['details_expanded_when_checkbox_trigger_checked']['textfield_in_details'] = [
+      '#type' => 'textfield',
+      '#title' => 'Textfield in details',
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+  }
+
+}
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Core/Form/JavascriptStatesTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Core/Form/JavascriptStatesTest.php
new file mode 100644
index 0000000..7d686ae
--- /dev/null
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Core/Form/JavascriptStatesTest.php
@@ -0,0 +1,271 @@
+<?php
+
+namespace Drupal\FunctionalJavascriptTests\Core\Form;
+
+use Behat\Mink\Element\TraversableElement;
+use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
+
+/**
+ * Tests the state of elements based on another elements.
+ *
+ * @group javascript
+ */
+class JavascriptStatesTest extends JavascriptTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['form_test'];
+
+  /**
+   * Test states of elements triggered by a checkbox element.
+   */
+  public function testCheckboxTriggeredElements() {
+    $this->drupalGet('form-test/javascript-states-form');
+    $page = $this->getSession()->getPage();
+
+    $trigger = $page->findField('checkbox_trigger');
+
+    /* Initial state: before the checkbox trigger is checked. */
+
+    // Test that the textfield is visible.
+    $this->assertFieldVisible('textfield_invisible_when_checkbox_trigger_checked');
+    // Test that the details element is collapsed so the textfield inside is not
+    // visible.
+    $this->assertFieldNotVisible('textfield_in_details');
+
+    // Change state: check the checkbox.
+    $trigger->check();
+
+    // Test that the textfield is not visible anymore.
+    $this->assertFieldNotVisible('textfield_invisible_when_checkbox_trigger_checked');
+    // Test that the details element is now open so the textfield inside is now
+    // visible.
+    $this->assertFieldVisible('textfield_in_details');
+
+    // Change back to the initial state to avoid issues running the next tests.
+    $trigger->uncheck();
+  }
+
+  /**
+   * Test states of elements triggered by a textfield element.
+   */
+  public function testTextfieldTriggeredElements() {
+    $this->drupalGet('form-test/javascript-states-form');
+    $page = $this->getSession()->getPage();
+
+    $trigger = $page->findField('textfield_trigger');
+    $target = $page->findField('checkbox_unchecked_when_textfield_trigger_filled');
+
+    /* Initial state: before the textfield is filled. */
+
+    // Test that the checkbox is checked.
+    $this->assertTrue($target->isChecked());
+
+    // Change state: fill the textfield.
+    $trigger->setValue('filled');
+
+    // Test that the checkbox is not checked anymore.
+    $this->assertFalse($target->isChecked());
+
+    // Change back to the initial state to avoid issues running the next tests.
+    $trigger->setValue('');
+    // We need to send a backspace hit to trigger JS events.
+    $trigger->keyUp(8);
+  }
+
+  /**
+   * Test states of elements triggered by a radios element.
+   */
+  public function testRadiosTriggeredElements() {
+    $this->drupalGet('form-test/javascript-states-form');
+    $page = $this->getSession()->getPage();
+
+    $trigger = $page->findField('radios_trigger');
+
+    /* Initial state: before the radios is checked. */
+
+    // Test that the fieldset element is not visible so the textfield inside is
+    // not visible either.
+    $this->assertFieldNotVisible('textfield_in_fieldset');
+
+    // Change state: check the 'Value 2' radio.
+    $trigger->selectOption('value2');
+
+    // Test that the fieldset element is now visible so the textfield inside is
+    // also visible.
+    $this->assertFieldVisible('textfield_in_fieldset');
+
+    // Change back to the initial state to avoid issues running the next tests.
+    $trigger->selectOption('value1');
+  }
+
+  /**
+   * Test states of elements triggered by a checkboxes element.
+   */
+  public function testCheckboxesTriggeredElements() {
+    $this->drupalGet('form-test/javascript-states-form');
+    $page = $this->getSession()->getPage();
+
+    $trigger1 = $page->findField('checkboxes_trigger[value1]');
+    $trigger2 = $page->findField('checkboxes_trigger[value2]');
+    $trigger3 = $page->findField('checkboxes_trigger[value3]');
+
+    /* Initial state: before any of the checkboxes is checked. */
+
+    // Test that textfield dependant on checkbox 'value2' is not visible.
+    $this->assertFieldNotVisible('textfield_visible_when_checkboxes_trigger_value2_checked');
+    // Test that textfield dependant on checkbox 'value3' is not visible.
+    $this->assertFieldNotVisible('textfield_visible_when_checkboxes_trigger_value3_checked');
+
+    // Change state: check the 'Value 1' checkbox.
+    $trigger1->check();
+
+    // Test that textfield dependant on checkbox 'value2' is still not visible.
+    $this->assertFieldNotVisible('textfield_visible_when_checkboxes_trigger_value2_checked');
+    // Test that textfield dependant on checkbox 'value3' is still not visible.
+    $this->assertFieldNotVisible('textfield_visible_when_checkboxes_trigger_value3_checked');
+
+    // Change state: check the 'Value 2' checkbox.
+    $trigger2->check();
+
+    // Test that textfield dependant on checkbox 'value2' is now visible.
+    $this->assertFieldVisible('textfield_visible_when_checkboxes_trigger_value2_checked');
+    // Test that textfield dependant on checkbox 'value3' is still not visible.
+    $this->assertFieldNotVisible('textfield_visible_when_checkboxes_trigger_value3_checked');
+
+    // Change state: check the 'Value 3' checkbox.
+    $trigger3->check();
+
+    // Test that textfield dependant on checkbox 'value2' is still visible.
+    $this->assertFieldVisible('textfield_visible_when_checkboxes_trigger_value2_checked');
+    // Test that textfield dependant on checkbox 'value3' is now visible.
+    $this->assertFieldVisible('textfield_visible_when_checkboxes_trigger_value3_checked');
+
+    // Change state: uncheck the 'Value 2' checkbox.
+    $trigger2->uncheck();
+
+    // Test that textfield dependant on checkbox 'value2' is now invisible.
+    $this->assertFieldNotVisible('textfield_visible_when_checkboxes_trigger_value2_checked');
+    // Test that textfield dependant on checkbox 'value3' is still visible.
+    $this->assertFieldVisible('textfield_visible_when_checkboxes_trigger_value3_checked');
+
+    // Change back to the initial state to avoid issues running the next tests.
+    $trigger1->uncheck();
+    $trigger2->uncheck();
+    $trigger3->uncheck();
+  }
+
+  /**
+   * Test states of elements triggered by a select element.
+   */
+  public function testSelectTriggeredElements() {
+    $this->drupalGet('form-test/javascript-states-form');
+    $page = $this->getSession()->getPage();
+
+    $trigger = $page->findField('select_trigger');
+
+    /* Initial state: before any option of the select box is selected. */
+
+    // Test that item element dependant on select 'Value 2' option is not
+    // visible.
+    $this->assertSession()->assertElementNotVisible('css', '#edit-item-visible-when-select-trigger-has-given-value');
+    // Test that textfield dependant on select 'Value 3' option is not visible.
+    $this->assertFieldNotVisible('textfield_visible_when_select_trigger_has_given_value');
+    // Test that textfield dependant on select 'Value 2' or 'Value 3' options is
+    // not visible.
+    $this->assertFieldNotVisible('textfield_visible_when_select_trigger_has_given_value_or_another');
+
+    // Change state: select the 'Value 2' option.
+    $trigger->setValue('value2');
+
+    // Test that item element dependant on select 'Value 2' option is now
+    // visible.
+    $this->assertSession()->assertElementVisible('css', '#edit-item-visible-when-select-trigger-has-given-value');
+    // Test that textfield dependant on select 'Value 3' option is not visible.
+    $this->assertFieldNotVisible('textfield_visible_when_select_trigger_has_given_value');
+    // Test that textfield dependant on select 'Value 2' or 'Value 3' options is
+    // now visible.
+    $this->assertFieldVisible('textfield_visible_when_select_trigger_has_given_value_or_another');
+
+    // Change state: select the 'Value 3' option.
+    $trigger->setValue('value3');
+
+    // Test that item element dependant on select 'Value 2' option is not
+    // visible anymore.
+    $this->assertSession()->assertElementNotVisible('css', '#edit-item-visible-when-select-trigger-has-given-value');
+    // Test that textfield dependant on select 'Value 3' option is now visible.
+    $this->assertFieldVisible('textfield_visible_when_select_trigger_has_given_value');
+    // Test that textfield dependant on select 'Value 2' or 'Value 3' options is
+    // still visible.
+    $this->assertFieldVisible('textfield_visible_when_select_trigger_has_given_value_or_another');
+
+    // Change back to the initial state to avoid issues running the next tests.
+    $trigger->setValue('_none');
+  }
+
+  /**
+   * Test states of elements triggered by a more than one element.
+   */
+  public function testMultipleTriggeredElements() {
+    $this->drupalGet('form-test/javascript-states-form');
+    $page = $this->getSession()->getPage();
+
+    $selectTrigger = $page->findField('select_trigger');
+    $textfieldTrigger = $page->findField('textfield_trigger');
+
+    /* Initial state: before any option of the select box is selected. */
+
+    // Test that item element dependant on select 'Value 2' option and textfield
+    // is not visible.
+    $this->assertSession()->assertElementNotVisible('css', '#edit-item-visible-when-select-trigger-has-given-value-and-textfield-trigger-filled');
+
+    // Change state: select the 'Value 2' option.
+    $selectTrigger->setValue('value2');
+
+    // Test that item element dependant on select 'Value 2' option and textfield
+    // is still not visible.
+    $this->assertSession()->assertElementNotVisible('css', '#edit-item-visible-when-select-trigger-has-given-value-and-textfield-trigger-filled');
+
+    // Change state: fill the textfield.
+    $textfieldTrigger->setValue('filled');
+
+    // Test that item element dependant on select 'Value 2' option and textfield
+    // is now visible.
+    $this->assertSession()->assertElementVisible('css', '#edit-item-visible-when-select-trigger-has-given-value-and-textfield-trigger-filled');
+
+    // Change back to the initial state to avoid issues running the next tests.
+    $selectTrigger->setValue('_none');
+    $textfieldTrigger->setValue('');
+    // We need to send a backspace hit to trigger JS events.
+    $textfieldTrigger->keyUp(8);
+  }
+
+  /**
+   * Asserts that the given field is visible.
+   *
+   * @param string $locator
+   *   One of id|name|label|value for the field.
+   * @param \Behat\Mink\Element\TraversableElement $container
+   *   (optional) The document to check against. Defaults to the current page.
+   */
+  public function assertFieldVisible($locator, TraversableElement $container = NULL) {
+    $this->assertSession()->assertElementVisible('named', ['field', $locator], $container);
+  }
+
+  /**
+   * Asserts that the given field is not visible.
+   *
+   * @param string $locator
+   *   One of id|name|label|value for the field.
+   * @param \Behat\Mink\Element\TraversableElement $container
+   *   (optional) The document to check against. Defaults to the current page.
+   *
+   * @throws \Behat\Mink\Exception\ElementNotFoundException
+   *   When the element doesn't exist.
+   */
+  public function assertFieldNotVisible($locator, TraversableElement $container = NULL) {
+    $this->assertSession()->assertElementNotVisible('named', ['field', $locator], $container);
+  }
+
+}
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php b/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php
index 8d3ff47..5e8bbe9 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php
@@ -2,6 +2,10 @@
 
 namespace Drupal\FunctionalJavascriptTests;
 
+use Behat\Mink\Element\Element;
+use Behat\Mink\Element\ElementInterface;
+use Behat\Mink\Exception\ElementHtmlException;
+use Behat\Mink\Exception\ElementNotFoundException;
 use Drupal\Tests\WebAssert;
 
 /**
@@ -28,4 +32,92 @@ public function assertWaitOnAjaxRequest($timeout = 10000, $message = 'Unable to
     }
   }
 
+  /**
+   * 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) {
+    $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);
+    }
+    $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) {
+    $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);
+    }
+    $message = sprintf(
+      'Element "%s" is not visible.',
+      $this->getMatchingElementRepresentation($selector_type, $selector)
+    );
+    $this->assertElement(!$node->isVisible(), $message, $node);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function assertElement($condition, $message, Element $element) {
+    if ($condition) {
+      return;
+    }
+
+    throw new ElementHtmlException($message, $this->session->getDriver(), $element);
+  }
+
+  /**
+   * {@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);
+  }
+
 }
