diff --git a/core/includes/form.inc b/core/includes/form.inc
index c33ff6f..a3cc0eb 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -10,7 +10,9 @@
 use Drupal\Component\Utility\String;
 use Drupal\Component\Utility\Url;
 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;
 use Drupal\Core\Utility\Color;
 use Symfony\Component\HttpFoundation\RedirectResponse;
@@ -1076,6 +1078,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',
@@ -1083,6 +1086,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;
@@ -1186,6 +1190,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,
       );
     }
@@ -1336,6 +1342,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,
       );
     }
@@ -2556,6 +2564,36 @@ function form_pre_render_color($element) {
 }
 
 /**
+ * Preprocesses variables for theme_form().
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *
+ * @ingroup themeable
+ */
+function template_preprocess_form(&$variables) {
+  if (!empty($variables['element']['#errors'])) {
+    $error_links = array();
+    // Loop through all form errors, and display a link for each error that
+    // is associated with a specific form element.
+    foreach ($variables['element']['#errors'] as $key => $error) {
+      if ($element = FormElementHelper::getElementByName($key, $variables['element'])) {
+        $title = FormElementHelper::getElementTitle($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');
+    }
+  }
+}
+
+/**
  * Returns HTML for a form.
  *
  * @param $variables
@@ -2771,6 +2809,13 @@ function template_preprocess_form_element(&$variables) {
     $variables['attributes']['class'][] = 'form-disabled';
   }
 
+  // Display any error messages.
+  if (!empty($element['#errors']) && empty($element['#error_use_parent'])) {
+    // Add a class if an error exists.
+    $variables['attributes']['class'][] = 'form-error';
+    $output .= ' ' . _theme('form_error_message', array('element' => $element));
+  }
+
   // If #title is not set, we don't display any label or required marker.
   if (!isset($element['#title'])) {
     $element['#title_display'] = 'none';
@@ -2873,6 +2918,25 @@ function theme_form_element_label($variables) {
 }
 
 /**
+ * Returns HTML for an inline error associated with a specific form element.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: '#error'.
+ *
+ * @return string
+ *
+ * @ingroup themeable
+ */
+function theme_form_error_message($variables) {
+  $output = '<div class="form-error-message">';
+  $output .= '<strong>' . t('Error') . ': ' . $variables['element']['#errors'] . '</strong>';
+  $output .= '</div>';
+  return $output;
+}
+
+/**
  * Sets a form element's class attribute.
  *
  * Adds 'required' and 'error' classes as needed.
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index aa558b2..57bed6b 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -2705,6 +2705,9 @@ function drupal_common_theme() {
     'form_element_label' => array(
       'render element' => 'element',
     ),
+    'form_error_message' => array(
+      'render element' => 'element',
+    ),
     'vertical_tabs' => array(
       'render element' => 'element',
       'template' => 'vertical-tabs',
diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index 8e8a82a..ddcfbcc 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -860,6 +860,9 @@ public function validateForm($form_id, &$form, &$form_state) {
       }
       $form_state['values'] = $values;
     }
+    if (!$form_state['programmed']) {
+      $form['#errors'] = $this->getErrors($form_state);
+    }
   }
 
   /**
@@ -1170,6 +1173,16 @@ public function executeHandlers($type, &$form, &$form_state) {
    */
   public function setErrorByName($name, array &$form_state, $message = '') {
     if (!isset($form_state['errors'][$name])) {
+      // This is only used by errors set in submit handlers.
+      // @todo Unlike errors set during validation, these errors will not
+      //   directly correspond to their input element, and will not interrupt
+      //   submission. We should consider limiting usage of form errors to
+      //   validation only, and encourage usage of drupal_set_message() in
+      //   submit handlers.
+      if ($message && isset($form_state['build_info']['form_id']) && !empty($this->validatedForms[$form_state['build_info']['form_id']])) {
+        $this->drupalSetMessage($message, 'error');
+      }
+
       $record = TRUE;
       if (isset($form_state['limit_validation_errors'])) {
         // #limit_validation_errors is an array of "sections" within which user
@@ -1197,9 +1210,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');
-        }
       }
     }
 
diff --git a/core/modules/shortcut/shortcut.admin.inc b/core/modules/shortcut/shortcut.admin.inc
index 876c9f8..743b5cc 100644
--- a/core/modules/shortcut/shortcut.admin.inc
+++ b/core/modules/shortcut/shortcut.admin.inc
@@ -117,7 +117,7 @@ function shortcut_set_switch_validate($form, &$form_state) {
   if ($form_state['values']['set'] == 'new') {
     // Check to prevent creating a shortcut set with an empty title.
     if (trim($form_state['values']['label']) == '') {
-      form_set_error('new', $form_state, t('The new set label is required.'));
+      form_set_error('label', $form_state, t('The new set label is required.'));
     }
     // Check to prevent a duplicate title.
     if (shortcut_set_title_exists($form_state['values']['label'])) {
diff --git a/core/modules/system/css/system.theme.css b/core/modules/system/css/system.theme.css
index 9a0153d..b70e9e2 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: 10px;
+}
+.form-error-message {
+  margin-bottom: 10px;
+  min-height: 25px;
+}
 .form-item,
 .form-actions {
   margin-top: 1em;
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'),
+    );
+  }
+
+}
