diff --git a/field_group.libraries.yml b/field_group.libraries.yml
index cef15b0..765d953 100644
--- a/field_group.libraries.yml
+++ b/field_group.libraries.yml
@@ -56,3 +56,21 @@ element.horizontal_tabs:
     - core/jquery
     - core/once
     - core/drupal.collapse
+
+details_validation:
+  js:
+    js/field_group.details_validation.js: {}
+  dependencies:
+    - core/jquery
+
+tab_validation:
+  js:
+    js/field_group.tab_validation.js: {}
+  dependencies:
+    - core/jquery
+
+tabs_validation:
+  js:
+    js/field_group.tabs_validation.js: {}
+  dependencies:
+    - core/jquery
\ No newline at end of file
diff --git a/js/field_group.details_validation.js b/js/field_group.details_validation.js
new file mode 100644
index 0000000..afe2400
--- /dev/null
+++ b/js/field_group.details_validation.js
@@ -0,0 +1,24 @@
+(function ($, once) {
+
+  'use strict';
+
+  /**
+   * Invalid event handler for input elements in Details field group.
+   */
+  var onDetailsInvalid = function(e) {
+    // Open any hidden parents first.
+    $(e.target).parents('details:not([open])').each(function () {
+      $(this).attr('open', '');
+    });
+  }
+
+  /**
+   * Behaviors for details validation.
+   */
+  Drupal.behaviors.fieldGroupDetailsValidation = {
+    attach: function (context) {
+      $(once('field-group-details-validation', $('.field-group-details :input', context))).on('invalid.field_group', onDetailsInvalid);
+    }
+  };
+
+})(jQuery, once);
diff --git a/js/field_group.tab_validation.js b/js/field_group.tab_validation.js
new file mode 100644
index 0000000..a0adb92
--- /dev/null
+++ b/js/field_group.tab_validation.js
@@ -0,0 +1,22 @@
+(function ($, once) {
+
+  'use strict';
+
+  /**
+   * Invalid event handler for input elements in Tab field group.
+   */
+  var onTabInvalid = function (e) {
+    $(e.target).parents('details ').children('summary[aria-expanded=false]:not(.horizontal-tabs-pane > summary, .vertical-tabs__pane > summary)').parent().attr('open', 'open');
+  };
+
+  /**
+   * Make sure tab field groups which contain invalid data are expanded when
+   * they first load, and also when someone clicks the submit button.
+   */
+  Drupal.behaviors.fieldGroupTabValidation = {
+    attach: function (context) {
+      $(once('field-group-tab-validation', $('.field-group-tab :input', context))).on('invalid.field_group', onTabInvalid);
+    }
+  };
+
+})(jQuery, once);
diff --git a/js/field_group.tabs_validation.js b/js/field_group.tabs_validation.js
new file mode 100644
index 0000000..1022dba
--- /dev/null
+++ b/js/field_group.tabs_validation.js
@@ -0,0 +1,43 @@
+(function ($, once) {
+
+  'use strict';
+
+  /**
+   * Opens Tab field group with invalid input elements.
+   */
+  var fieldGroupTabsOpen = function ($field_group) {
+    if ($field_group.data('verticalTab')) {
+      $field_group.data('verticalTab').tabShow();
+    }
+    else if ($field_group.data('horizontalTab')) {
+      $field_group.data('horizontalTab').tabShow();
+    }
+    else {
+      $field_group.attr('open', '');
+    }
+  };
+
+  /**
+   * Behaviors for tab validation.
+   */
+  Drupal.behaviors.fieldGroupTabsValidation = {
+    attach: function (context) {
+      /**
+       * Invalid event handler for input elements in Tabs field group.
+       */
+      var onTabsInvalid = function (e) {
+        $inputs.off('invalid.field_group', onTabsInvalid);
+        $(e.target).parents('details:not(:visible), details.horizontal-tab-hidden, details.vertical-tab-hidden').each(function () {
+          fieldGroupTabsOpen($(this));
+        });
+        requestAnimationFrame(function () {
+          $inputs.on('invalid.field_group', onTabsInvalid);
+        });
+      };
+
+      var $inputs = $('.field-group-tabs-wrapper :input', context);
+      $(once('field-group-tabs-validation', $inputs)).on('invalid.field_group', onTabsInvalid);
+    }
+  };
+
+})(jQuery, once);
diff --git a/src/Plugin/field_group/FieldGroupFormatter/Details.php b/src/Plugin/field_group/FieldGroupFormatter/Details.php
index 3fa04af..34845de 100644
--- a/src/Plugin/field_group/FieldGroupFormatter/Details.php
+++ b/src/Plugin/field_group/FieldGroupFormatter/Details.php
@@ -51,6 +51,8 @@ class Details extends FieldGroupFormatterBase {
       $element['#attached']['library'][] = 'field_group/core';
     }
 
+    // Add details validation behaviour.
+    $element['#attached']['library'][] = 'field_group/details_validation';
   }
 
   /**
diff --git a/src/Plugin/field_group/FieldGroupFormatter/Tab.php b/src/Plugin/field_group/FieldGroupFormatter/Tab.php
index 4135a29..9dfd420 100644
--- a/src/Plugin/field_group/FieldGroupFormatter/Tab.php
+++ b/src/Plugin/field_group/FieldGroupFormatter/Tab.php
@@ -66,6 +66,8 @@ class Tab extends FieldGroupFormatterBase {
 
     $element += $add;
 
+    // Add tab validation behaviour.
+    $element['#attached']['library'][] = 'field_group/tab_validation';
   }
 
   /**
diff --git a/src/Plugin/field_group/FieldGroupFormatter/Tabs.php b/src/Plugin/field_group/FieldGroupFormatter/Tabs.php
index 5218a43..07e7226 100644
--- a/src/Plugin/field_group/FieldGroupFormatter/Tabs.php
+++ b/src/Plugin/field_group/FieldGroupFormatter/Tabs.php
@@ -59,6 +59,9 @@ class Tabs extends FieldGroupFormatterBase {
     if ($width_breakpoint = $this->getSetting('width_breakpoint')) {
       $element['#attached']['drupalSettings']['widthBreakpoint'] = $width_breakpoint;
     }
+
+    // Add tabs validation behaviour.
+    $element['#attached']['library'][] = 'field_group/tabs_validation';
   }
 
   /**
diff --git a/tests/src/FunctionalJavascript/EntityFormTest.php b/tests/src/FunctionalJavascript/EntityFormTest.php
new file mode 100644
index 0000000..67fd829
--- /dev/null
+++ b/tests/src/FunctionalJavascript/EntityFormTest.php
@@ -0,0 +1,225 @@
+<?php
+
+namespace Drupal\Tests\field_group\FunctionalJavascript;
+
+use Drupal\Core\Entity\Entity\EntityFormDisplay;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use Drupal\Tests\field_group\Functional\FieldGroupTestTrait;
+
+/**
+ * Tests for form display.
+ *
+ * @group field_group
+ */
+class EntityFormTest extends WebDriverTestBase {
+
+  use FieldGroupTestTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'node',
+    'field_test',
+    'field_ui',
+    'field_group',
+    'field_group_test',
+  ];
+
+  /**
+   * The node type id.
+   *
+   * @var string
+   */
+  protected $type;
+
+  /**
+   * A node to use for testing.
+   *
+   * @var \Drupal\node\NodeInterface
+   */
+  protected $node;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp(): void {
+    parent::setUp();
+
+    // Create test user.
+    $admin_user = $this->drupalCreateUser([
+      'access content',
+      'administer content types',
+      'administer node fields',
+      'administer node form display',
+      'administer node display',
+      'bypass node access',
+    ]);
+    $this->drupalLogin($admin_user);
+
+    // Create content type, with underscores.
+    $type_name = strtolower($this->randomMachineName(8)) . '_test';
+    $type = $this->drupalCreateContentType([
+      'name' => $type_name,
+      'type' => $type_name,
+    ]);
+    $this->type = $type->id();
+
+    // Create required test field.
+    $field_storage = FieldStorageConfig::create([
+      'field_name' => 'field_test',
+      'entity_type' => 'node',
+      'type' => 'test_field',
+    ]);
+    $field_storage->save();
+
+    $instance = FieldConfig::create([
+      'field_storage' => $field_storage,
+      'bundle' => $type_name,
+      'label' => 'field_test',
+      'required' => TRUE,
+    ]);
+    $instance->save();
+
+    /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display */
+    $form_display = EntityFormDisplay::load('node.' . $this->type . '.default');
+
+    // Set the field visible on the form display object.
+    $display_options = [
+      'type' => 'string_textfield',
+      'region' => 'content',
+      'settings' => [
+        'size' => 60,
+      ],
+    ];
+    $form_display->setComponent('field_test', $display_options);
+
+    // Save the form display.
+    $form_display->save();
+  }
+
+  /**
+   * Tests required field validation visibility.
+   */
+  public function testRequiredFieldValidationVisibility() {
+    $data = [
+      'label' => 'Tab 1',
+      'weight' => '1',
+      'children' => [
+        0 => 'title',
+        1 => 'body',
+      ],
+      'format_type' => 'tab',
+      'format_settings' => [
+        'label' => 'Tab 1',
+        'classes' => 'test-class',
+        'description' => '',
+        'formatter' => 'open',
+      ],
+    ];
+    $first_tab = $this->createGroup('node', $this->type, 'form', 'default', $data);
+
+    $data = [
+      'label' => 'Field group details',
+      'weight' => '1',
+      'children' => [
+        0 => 'field_test',
+      ],
+      'format_type' => 'details',
+      'format_settings' => [
+        'open' => FALSE,
+        'required_fields' => TRUE,
+      ],
+    ];
+    $field_group_details = $this->createGroup('node', $this->type, 'form', 'default', $data);
+
+    $data = [
+      'label' => 'Tab 2',
+      'weight' => '1',
+      'children' => [
+        0 => $field_group_details->group_name,
+      ],
+      'format_type' => 'tab',
+      'format_settings' => [
+        'label' => 'Tab 1',
+        'classes' => 'test-class-2',
+        'description' => 'description of second tab',
+        'formatter' => 'closed',
+      ],
+    ];
+    $second_tab = $this->createGroup('node', $this->type, 'form', 'default', $data);
+
+    $data = [
+      'label' => 'Tabs',
+      'weight' => '1',
+      'children' => [
+        0 => $first_tab->group_name,
+        1 => $second_tab->group_name,
+      ],
+      'format_type' => 'tabs',
+      'format_settings' => [
+        'direction' => 'vertical',
+        'label' => 'Tab 1',
+        'classes' => 'test-class-wrapper',
+      ],
+    ];
+    $tabs_group = $this->createGroup('node', $this->type, 'form', 'default', $data);
+
+    // Load the node creation page.
+    $this->drupalGet('node/add/' . $this->type);
+
+    // Test if it's a vertical tab.
+    $this->assertSession()->elementExists('xpath', $this->assertSession()
+      ->buildXPathQuery('//div[@data-vertical-tabs-panes=""]'));
+    $this->requiredFieldVisibilityAssertions();
+
+    // Switch to horizontal.
+    $tabs_group->format_settings['direction'] = 'horizontal';
+    field_group_group_save($tabs_group);
+
+    // Reload the node creation page.
+    $this->drupalGet('node/add/' . $this->type);
+
+    // Test if it's a horizontal tab.
+    $this->assertSession()->elementExists('xpath', $this->assertSession()
+      ->buildXPathQuery('//div[@data-horizontal-tabs-panes=""]'));
+    $this->requiredFieldVisibilityAssertions();
+  }
+
+  /**
+   * Tests the required field_test to assert its visibility.
+   */
+  private function requiredFieldVisibilityAssertions(): void {
+    // Assert that the required field, field_test is present but not visible.
+    $this->assertSession()->fieldExists('field_test');
+    $this->assertFalse($this->getSession()
+      ->getDriver()
+      ->isVisible($this->cssSelectToXpath('input[name="field_test[0][value]"]')));
+
+    // Submit the form without filling any required field.
+    $this->getSession()->getPage()->pressButton('Save');
+
+    // Assert that the field_test is not visible because it's in the first tab.
+    $this->assertFalse($this->getSession()
+      ->getDriver()
+      ->isVisible($this->cssSelectToXpath('input[name="field_test[0][value]"]')));
+
+    // Fill in the title field and leave the required field_test empty.
+    $this->getSession()->getPage()->fillField('Title', 'Node title');
+    $this->getSession()->getPage()->pressButton('Save');
+
+    // Assert that the field_test is visible because the second tab is in focus
+    // and the collapsible field group is open.
+    $this->assertTrue($this->getSession()
+      ->getDriver()
+      ->isVisible($this->cssSelectToXpath('input[name="field_test[0][value]"]')));
+  }
+
+}