diff --git a/core/includes/form.inc b/core/includes/form.inc
index c3c11882039d7c3c7a282183ba225dd7d633ce7f..972fa3dd434b26aa6c7a7659265807054741583d 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -2714,7 +2714,11 @@ function template_preprocess_form_element(&$variables) {
 
   $variables['description'] = NULL;
   if (!empty($element['#description'])) {
-    $description_attributes = array('class' => 'description');
+    $variables['description_display'] = $element['#description_display'];
+    $description_attributes = array('class' => array('description'));
+    if ($element['#description_display'] === 'invisible') {
+      $description_attributes['class'][] = 'visually-hidden';
+    }
     if (!empty($element['#id'])) {
       $description_attributes['id'] = $element['#id'] . '--description';
     }
diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index ab1c1c57efb65ed9ba4e4bcfb640b7e40d734ddf..adbb8f335681932d49c7fd704521f8adbe27dac5 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -703,6 +703,7 @@ public function doBuildForm($form_id, &$element, FormStateInterface &$form_state
       '#required' => FALSE,
       '#attributes' => array(),
       '#title_display' => 'before',
+      '#description_display' => 'after',
       '#errors' => NULL,
     );
 
diff --git a/core/modules/system/src/Tests/Form/ElementsLabelsTest.php b/core/modules/system/src/Tests/Form/ElementsLabelsTest.php
index 970f29943ca0dea57e276e7093e0c21cd2d2d1be..dbf13d2c86a1e44d7963630d5efff39e32f74e0c 100644
--- a/core/modules/system/src/Tests/Form/ElementsLabelsTest.php
+++ b/core/modules/system/src/Tests/Form/ElementsLabelsTest.php
@@ -94,4 +94,32 @@ function testFormLabels() {
     $elements = $this->xpath('//div[@id="edit-form-radios-title-attribute"]');
     $this->assertEqual($elements[0]['title'], 'Radios test' . ' (' . t('Required') . ')', 'Title attribute found.');
   }
+
+  /**
+   * Tests different display options for form element descriptions.
+   */
+  function testFormDescriptions() {
+    $this->drupalGet('form_test/form-descriptions');
+
+    // Check #description placement with #description_display='after'.
+    $field_id = 'edit-form-textfield-test-description-after';
+    $description_id = $field_id . '--description';
+    $elements = $this->xpath('//input[@id="' . $field_id . '" and @aria-describedby="' . $description_id . '"]/following-sibling::div[@id="' . $description_id . '"]');
+    $this->assertTrue(isset($elements[0]), t('Properly places the #description element after the form item.'));
+
+    // Check #description placement with #description_display='before'.
+    $field_id = 'edit-form-textfield-test-description-before';
+    $description_id = $field_id . '--description';
+    $elements = $this->xpath('//input[@id="' . $field_id . '" and @aria-describedby="' . $description_id . '"]/preceding-sibling::div[@id="' . $description_id . '"]');
+    $this->assertTrue(isset($elements[0]), t('Properly places the #description element before the form item.'));
+
+    // Check if the class is 'visually-hidden' on the form element description
+    // for the option with #description_display='invisible' and also check that
+    // the description is placed after the form element.
+    $field_id = 'edit-form-textfield-test-description-invisible';
+    $description_id = $field_id . '--description';
+    $elements = $this->xpath('//input[@id="' . $field_id . '" and @aria-describedby="' . $description_id . '"]/following-sibling::div[contains(@class, "visually-hidden")]');
+    $this->assertTrue(isset($elements[0]), t('Properly renders the #description element visually-hidden.'));
+  }
+
 }
diff --git a/core/modules/system/templates/form-element.html.twig b/core/modules/system/templates/form-element.html.twig
index ea4d90f63f040d8393a4622335a9422563be0f1e..e029b18901c26f49b5f5aff5c10714238da1b922 100644
--- a/core/modules/system/templates/form-element.html.twig
+++ b/core/modules/system/templates/form-element.html.twig
@@ -30,6 +30,12 @@
  *    - content: A description of the form element, may not be set.
  *    - attributes: (optional) A list of HTML attributes to apply to the
  *      description content wrapper. Will only be set when description is set.
+ * - description_display: Description display setting. It can have these values:
+ *   - before: The description is output before the element.
+ *   - after: The description is output after the element. This is the default
+ *     value.
+ *   - invisible: The description is output after the element, hidden visually
+ *     but available to screen readers.
  *
  * @see template_preprocess_form_element()
  *
@@ -43,6 +49,11 @@
   {% if prefix is not empty %}
     <span class="field-prefix">{{ prefix }}</span>
   {% endif %}
+  {% if description_display == 'before' and description.content %}
+    <div{{ description.attributes }}>
+      {{ description.content }}
+    </div>
+  {% endif %}
   {{ children }}
   {% if suffix is not empty %}
     <span class="field-suffix">{{ suffix }}</span>
@@ -50,7 +61,7 @@
   {% if label_display == 'after' %}
     {{ label }}
   {% endif %}
-  {% if description.content %}
+  {% if description_display in ['after', 'invisible'] and description.content %}
     <div{{ description.attributes }}>
       {{ description.content }}
     </div>
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 86d7fc0938c6c02bfb921e4d1f4aec5433d2369e..109d8f37c3ea654b988340fc44f5830db112fe9e 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
@@ -378,6 +378,14 @@ form_test.button_class:
   requirements:
     _access: 'TRUE'
 
+form_test.description_display:
+  path: '/form_test/form-descriptions'
+  defaults:
+    _form: '\Drupal\form_test\Form\FormTestDescriptionForm'
+    _title: 'Form description test'
+  requirements:
+    _access: 'TRUE'
+
 form_test.group_details:
   path: '/form-test/group-details'
   defaults:
diff --git a/core/modules/system/tests/modules/form_test/src/Form/FormTestDescriptionForm.php b/core/modules/system/tests/modules/form_test/src/Form/FormTestDescriptionForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..aeb11964253ceb309bc3f94f7e257d437201703c
--- /dev/null
+++ b/core/modules/system/tests/modules/form_test/src/Form/FormTestDescriptionForm.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\form_test\Form\FormTestEmailForm.
+ */
+
+namespace Drupal\form_test\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Defines a form for testing form element description display options.
+ *
+ * @see \Drupal\system\Tests\Form\ElementsLabelsTest::testFormDescriptions()
+ */
+class FormTestDescriptionForm extends FormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'form_test_description_display';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['form_textfield_test_description_before'] = array(
+      '#type' => 'textfield',
+      '#title' => 'Textfield test for description before element',
+      '#description' => 'Textfield test for description before element',
+      '#description_display' => 'before',
+    );
+
+    $form['form_textfield_test_description_after'] = array(
+      '#type' => 'textfield',
+      '#title' => 'Textfield test for description after element',
+      '#description' => 'Textfield test for description after element',
+      '#description_display' => 'after',
+    );
+
+    $form['form_textfield_test_description_invisible'] = array(
+      '#type' => 'textfield',
+      '#title' => 'Textfield test for visually-hidden description',
+      '#description' => 'Textfield test for visually-hidden description',
+      '#description_display' => 'invisible',
+    );
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    // The test that uses this form does not submit the form so this is empty.
+  }
+
+}