diff --git a/core/includes/form.inc b/core/includes/form.inc
index 54feee0..b31e863 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -11,6 +11,7 @@
 use Drupal\Component\Utility\UrlHelper;
 use Drupal\Component\Utility\Xss;
 use Drupal\Core\Database\Database;
+use Drupal\Core\Form\FormElementHelper;
 use Drupal\Core\Language\Language;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Template\Attribute;
@@ -1151,6 +1152,7 @@ function form_process_password_confirm($element) {
     '#value' => empty($element['#value']) ? NULL : $element['#value']['pass1'],
     '#required' => $element['#required'],
     '#attributes' => array('class' => array('password-field')),
+    '#error_use_parent' => TRUE,
   );
   $element['pass2'] =  array(
     '#type' => 'password',
@@ -1158,6 +1160,7 @@ function form_process_password_confirm($element) {
     '#value' => empty($element['#value']) ? NULL : $element['#value']['pass2'],
     '#required' => $element['#required'],
     '#attributes' => array('class' => array('password-confirm')),
+    '#error_use_parent' => TRUE,
   );
   $element['#element_validate'] = array('password_confirm_validate');
   $element['#tree'] = TRUE;
@@ -1261,6 +1264,8 @@ function form_process_radios($element) {
         '#parents' => $element['#parents'],
         '#id' => drupal_html_id('edit-' . implode('-', $parents_for_id)),
         '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
+        // Errors should only be shown on the parent radios element.
+        '#error_use_parent' => TRUE,
         '#weight' => $weight,
       );
     }
@@ -1335,6 +1340,7 @@ function form_pre_render_conditional_form_element($element) {
 
   if (isset($element['#title']) || isset($element['#description'])) {
     // @see #type 'fieldgroup'
+    $element['#theme_wrappers'][] = 'form_element';
     $element['#theme_wrappers'][] = 'fieldset';
     $element['#attributes']['class'][] = 'fieldgroup';
     $element['#attributes']['class'][] = 'form-composite';
@@ -1414,6 +1420,8 @@ function form_process_checkboxes($element) {
         '#default_value' => isset($value[$key]) ? $key : NULL,
         '#attributes' => $element['#attributes'],
         '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
+        // Errors should only be shown on the parent checkboxes element.
+        '#error_use_parent' => TRUE,
         '#weight' => $weight,
       );
     }
@@ -2689,6 +2697,25 @@ function template_preprocess_form(&$variables) {
   }
   $variables['attributes'] = $element['#attributes'];
   $variables['children'] = $element['#children'];
+
+  if (!empty($element['#errors'])) {
+    $error_links = array();
+    // Loop through all form errors, and display a link for each error that
+    // is associated with a visible form element.
+    foreach ($element['#errors'] as $key => $error) {
+      if (($form_element = FormElementHelper::getElementByName($key, $element)) && Element::isVisibleElement($form_element)) {
+        $title = FormElementHelper::getElementTitle($form_element);
+        $error_links[] = l($title, '', array('fragment' => 'edit-' . str_replace('_', '-', $key), 'external' => TRUE));
+      }
+      else {
+        drupal_set_message($error, 'error');
+      }
+    }
+
+    if (!empty($error_links)) {
+      drupal_set_message(format_plural(count($error_links), '1 error has been found', '@count errors have been found') . ': ' . implode(', ', $error_links), 'error');
+    }
+  }
 }
 
 /**
@@ -2882,6 +2909,14 @@ function template_preprocess_form_element(&$variables) {
     $variables['attributes']['class'][] = 'form-disabled';
   }
 
+  // Display any error messages.
+  $variables['errors'] = NULL;
+  if (!empty($element['#errors']) && empty($element['#error_use_parent'])) {
+    // Add a class if an error exists.
+    $variables['attributes']['class'][] = 'form-error';
+    $variables['errors'] = $element['#errors'];
+  }
+
   // If #title is not set, we don't display any label or required marker.
   if (!isset($element['#title'])) {
     $element['#title_display'] = 'none';
diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index 2c61c8d..edda445 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -20,6 +20,7 @@
 use Drupal\Core\Routing\UrlGeneratorInterface;
 use Drupal\Core\StringTranslation\TranslationInterface;
 use Drupal\Core\Url;
+use Drupal\Core\Utility\Error;
 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\HttpFoundation\RedirectResponse;
 use Symfony\Component\HttpFoundation\Request;
@@ -1228,9 +1229,6 @@ public function setErrorByName($name, array &$form_state, $message = '') {
       if ($record) {
         $form_state['errors'][$name] = $message;
         $this->request->attributes->set('_form_errors', TRUE);
-        if ($message) {
-          $this->drupalSetMessage($message, 'error');
-        }
       }
     }
 
@@ -1758,15 +1756,6 @@ protected function watchdog($type, $message, array $variables = NULL, $severity
   }
 
   /**
-   * Wraps drupal_set_message().
-   *
-   * @return array|null
-   */
-  protected function drupalSetMessage($message = NULL, $type = 'status', $repeat = FALSE) {
-    return drupal_set_message($message, $type, $repeat);
-  }
-
-  /**
    * Wraps drupal_html_class().
    *
    * @return string
diff --git a/core/lib/Drupal/Core/Form/FormElementHelper.php b/core/lib/Drupal/Core/Form/FormElementHelper.php
new file mode 100644
index 0000000..ec5616e
--- /dev/null
+++ b/core/lib/Drupal/Core/Form/FormElementHelper.php
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Form\FormElementHelper.
+ */
+
+namespace Drupal\Core\Form;
+
+use Drupal\Core\Render\Element;
+
+/**
+ * Provides common functionality for form elements.
+ */
+class FormElementHelper {
+
+  /**
+   * Retrieves a form element.
+   *
+   * @param string $name
+   *   The name of the form element. If the #parents property of your form
+   *   element is array('foo', 'bar', 'baz') then the name is 'foo][bar][baz'.
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   *
+   * @return array
+   *   The form element.
+   */
+  public static function getElementByName($name, array $form) {
+    foreach (Element::children($form) as $key) {
+      if ($key === $name) {
+        return $form[$key];
+      }
+      elseif ($element = static::getElementByName($name, $form[$key])) {
+        return $element;
+      }
+    }
+    return array();
+  }
+
+  /**
+   * Returns the title for the element.
+   *
+   * If the element has no title, this will recurse through all children of the
+   * element until a title is found.
+   *
+   * @param array $element
+   *   An associative array containing the properties of the form element.
+   *
+   * @return string
+   *   The title of the element, or an empty string if none is found.
+   */
+  public static function getElementTitle(array $element) {
+    $title = '';
+    if (isset($element['#title'])) {
+      $title = $element['#title'];
+    }
+    else {
+      foreach (Element::children($element) as $key) {
+        if ($title = static::getElementTitle($element[$key])) {
+          break;
+        }
+      }
+    }
+    return $title;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Render/Element.php b/core/lib/Drupal/Core/Render/Element.php
index 501f2de..3dcb446 100644
--- a/core/lib/Drupal/Core/Render/Element.php
+++ b/core/lib/Drupal/Core/Render/Element.php
@@ -127,7 +127,7 @@ public static function getVisibleChildren(array $elements) {
       }
 
       // Skip value and hidden elements, since they are not rendered.
-      if (isset($child['#type']) && in_array($child['#type'], array('value', 'hidden'))) {
+      if (!static::isVisibleElement($child)) {
         continue;
       }
 
@@ -138,6 +138,19 @@ public static function getVisibleChildren(array $elements) {
   }
 
   /**
+   * Determines if an element is visible.
+   *
+   * @param array $element
+   *   The element to check for visibility.
+   *
+   * @return bool
+   *   TRUE if the element is visible, otherwise FALSE.
+   */
+  public static function isVisibleElement($element) {
+    return !isset($element['#type']) || !in_array($element['#type'], array('value', 'hidden', 'token'));
+  }
+
+  /**
    * Sets HTML attributes based on element properties.
    *
    * @param array $element
diff --git a/core/modules/system/css/system.theme.css b/core/modules/system/css/system.theme.css
index 01ee3d1..60a36d9 100644
--- a/core/modules/system/css/system.theme.css
+++ b/core/modules/system/css/system.theme.css
@@ -44,6 +44,16 @@ td.active {
 /**
  * Markup generated by Form API.
  */
+.form-error {
+  background-color: #fef5f1;
+  border: 1px solid #ed541d;
+  color: #8c2e0b;
+  padding: 5px;
+}
+.form-error-message {
+  margin-bottom: 10px;
+  min-height: 25px;
+}
 .form-item,
 .form-actions {
   margin-top: 1em;
@@ -593,6 +603,7 @@ table tr.warning {
   background-image: url(../../../misc/icons/ea2800/error.svg);
   border-color: #f9c9bf #f9c9bf #f9c9bf transparent;  /* LTR */
   box-shadow: -8px 0 0 #e62600; /* LTR */
+  margin-left: 8px;
 }
 [dir="rtl"] .messages--error {
   border-color: #f9c9bf transparent #f9c9bf #f9c9bf;
diff --git a/core/modules/system/lib/Drupal/system/Tests/Form/FormTest.php b/core/modules/system/lib/Drupal/system/Tests/Form/FormTest.php
index b006591..a13a287 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Form/FormTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Form/FormTest.php
@@ -188,7 +188,7 @@ function testRequiredCheckboxesRadio() {
     }
 
     // Check the page for error messages.
-    $errors = $this->xpath('//div[contains(@class, "error")]//li');
+    $errors = $this->xpath('//div[contains(@class, "form-error-message")]//strong');
     foreach ($errors as $error) {
       $expected_key = array_search($error[0], $expected);
       // If the error message is not one of the expected messages, fail.
diff --git a/core/modules/system/lib/Drupal/system/Tests/Form/ValidationTest.php b/core/modules/system/lib/Drupal/system/Tests/Form/ValidationTest.php
index 43f1c88..0aa148b 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Form/ValidationTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Form/ValidationTest.php
@@ -213,17 +213,34 @@ function testCustomRequiredError() {
     $edit = array();
     $this->drupalPostForm('form-test/validate-required', $edit, 'Submit');
 
+    $messages = array();
     foreach (Element::children($form) as $key) {
       if (isset($form[$key]['#required_error'])) {
         $this->assertNoText(t('!name field is required.', array('!name' => $form[$key]['#title'])));
-        $this->assertText($form[$key]['#required_error']);
+        $messages[] = array(
+          'title' => $form[$key]['#title'],
+          'message' => $form[$key]['#required_error'],
+          'key' => $key,
+        );
       }
       elseif (isset($form[$key]['#form_test_required_error'])) {
         $this->assertNoText(t('!name field is required.', array('!name' => $form[$key]['#title'])));
-        $this->assertText($form[$key]['#form_test_required_error']);
+        $messages[] = array(
+          'title' => $form[$key]['#title'],
+          'message' => $form[$key]['#form_test_required_error'],
+          'key' => $key,
+        );
+      }
+      elseif (!empty($form[$key]['#required'])) {
+        $messages[] = array(
+          'title' => $form[$key]['#title'],
+          'message' => t('!name field is required.', array('!name' => $form[$key]['#title'])),
+          'key' => $key,
+        );
       }
     }
-    $this->assertNoText(t('An illegal choice has been detected. Please contact the site administrator.'));
+    $this->assertErrorMessages($messages);
+
 
     // Verify that no custom validation error appears with valid values.
     $edit = array(
@@ -233,6 +250,7 @@ function testCustomRequiredError() {
     );
     $this->drupalPostForm('form-test/validate-required', $edit, 'Submit');
 
+    $messages = array();
     foreach (Element::children($form) as $key) {
       if (isset($form[$key]['#required_error'])) {
         $this->assertNoText(t('!name field is required.', array('!name' => $form[$key]['#title'])));
@@ -242,7 +260,47 @@ function testCustomRequiredError() {
         $this->assertNoText(t('!name field is required.', array('!name' => $form[$key]['#title'])));
         $this->assertNoText($form[$key]['#form_test_required_error']);
       }
+      elseif (!empty($form[$key]['#required'])) {
+        $messages[] = array(
+          'title' => $form[$key]['#title'],
+          'message' => t('!name field is required.', array('!name' => $form[$key]['#title'])),
+          'key' => $key,
+        );
+      }
     }
+    $this->assertErrorMessages($messages);
+  }
+
+  /**
+   * Asserts that the given error messages are displayed.
+   *
+   * @param array $messages
+   *   An associative array of error messages keyed by the order they appear on
+   *   the page, with the following key-value pairs:
+   *   - title: The human readable form element title.
+   *   - message: The error message for this form element.
+   *   - key: The key used for the form element.
+   */
+  protected function assertErrorMessages($messages) {
+    $element = $this->xpath('//div[@class = "form-error-message"]/strong');
+    $this->assertIdentical(count($messages), count($element));
+
+    $error_links = array();
+    foreach ($messages as $delta => $message) {
+      // Ensure the message appears in the correct place.
+      if (!isset($element[$delta])) {
+        $this->fail(format_string('The error message for the "@title" element with key "@key" was not found.', array('@title' => $message['title'], '@key' => $message['key'])));
+      }
+      else {
+        $this->assertIdentical($message['message'], (string) $element[$delta]);
+      }
+
+      // Gather the element for checking the jump link section.
+      $error_links[] = l($message['title'], '', array('fragment' => 'edit-' . str_replace('_', '-', $message['key']), 'external' => TRUE));
+    }
+    $top_message = format_plural(count($error_links), '1 error has been found', '@count errors have been found') . ': ' . implode(', ', $error_links);
+    $this->assertRaw($top_message);
     $this->assertNoText(t('An illegal choice has been detected. Please contact the site administrator.'));
   }
+
 }
diff --git a/core/modules/system/templates/form-element.html.twig b/core/modules/system/templates/form-element.html.twig
index ea4d90f..4c0d8d9 100644
--- a/core/modules/system/templates/form-element.html.twig
+++ b/core/modules/system/templates/form-element.html.twig
@@ -5,6 +5,7 @@
  *
  * Available variables:
  * - attributes: HTML attributes for the containing element.
+ * - errors: (optional) Any errors for this form element, may not be set.
  * - prefix: (optional) The form element prefix, may not be set.
  * - suffix: (optional) The form element suffix, may not be set.
  * - required: The required marker, or empty if the associated form element is
@@ -37,6 +38,11 @@
  */
 #}
 <div{{ attributes }}>
+  {% if errors %}
+    <div class="form-error-message">
+      <strong>{{ errors }}</strong>
+    </div>
+  {% endif %}
   {% if label_display in ['before', 'invisible'] %}
     {{ label }}
   {% endif %}
diff --git a/core/tests/Drupal/Tests/Core/Form/FormElementHelperTest.php b/core/tests/Drupal/Tests/Core/Form/FormElementHelperTest.php
new file mode 100644
index 0000000..58973bd
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Form/FormElementHelperTest.php
@@ -0,0 +1,81 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Form\FormBuilderTest.
+ */
+
+namespace Drupal\Tests\Core\Form;
+
+use Drupal\Core\Form\FormElementHelper;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests the form element helper.
+ *
+ * @group Drupal
+ * @group Form
+ *
+ * @coversDefaultClass \Drupal\Core\Form\FormElementHelper
+ */
+class FormElementHelperTest extends UnitTestCase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'FormElementHelper test',
+      'description' => 'Tests the form element helper.',
+      'group' => 'Form API',
+    );
+  }
+
+  /**
+   * Tests the getElementByName() method.
+   *
+   * @covers ::getElementByName()
+   *
+   * @dataProvider getElementByNameProvider
+   */
+  public function testGetElementByName($name, $form, $expected) {
+    $this->assertSame($expected, FormElementHelper::getElementByName($name, $form));
+  }
+
+  /**
+   * Provides test data.
+   */
+  public function getElementByNameProvider() {
+    return array(
+      array('id', array(), array()),
+      array('id', array('id' => array('#title' => 'ID')), array('#title' => 'ID')),
+      array('id', array('fieldset' => array('id' => array('#title' => 'ID'))), array('#title' => 'ID')),
+      array('fieldset', array('fieldset' => array('id' => array('#title' => 'ID'))), array('id' => array('#title' => 'ID'))),
+    );
+  }
+
+  /**
+   * Tests the getElementTitle() method.
+   *
+   * @covers ::getElementTitle()
+   *
+   * @dataProvider getElementTitleProvider
+   */
+  public function testGetElementTitle($name, $form, $expected) {
+    $element = FormElementHelper::getElementByName($name, $form);
+    $this->assertSame($expected, FormElementHelper::getElementTitle($element));
+  }
+
+  /**
+   * Provides test data.
+   */
+  public function getElementTitleProvider() {
+    return array(
+      array('id', array(), ''),
+      array('id', array('id' => array('#title' => 'ID')), 'ID'),
+      array('id', array('fieldset' => array('id' => array('#title' => 'ID'))), 'ID'),
+      array('fieldset', array('fieldset' => array('id' => array('#title' => 'ID'))), 'ID'),
+    );
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Form/FormTestBase.php b/core/tests/Drupal/Tests/Core/Form/FormTestBase.php
index 44059c4..9e789a0 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormTestBase.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormTestBase.php
@@ -291,12 +291,6 @@ protected function menuGetItem() {
   /**
    * {@inheritdoc}
    */
-  protected function drupalSetMessage($message = NULL, $type = 'status', $repeat = FALSE) {
-  }
-
-  /**
-   * {@inheritdoc}
-   */
   protected function watchdog($type, $message, array $variables = NULL, $severity = WATCHDOG_NOTICE, $link = NULL) {
   }
 
diff --git a/core/themes/seven/style.css b/core/themes/seven/style.css
index 33b3eee..ab9e32b 100644
--- a/core/themes/seven/style.css
+++ b/core/themes/seven/style.css
@@ -787,6 +787,11 @@ label {
   color: #777;
 }
 
+.form-error {
+  border-radius: 2px;
+  border-color: #F9C9BF;
+}
+
 /* Filter */
 .filter-wrapper {
   font-size: 0.923em;
