';
+  $output .= $prefix . '
\n";
   return $output;
 }
 
@@ -1241,11 +1252,15 @@ function theme_checkboxes($variables) {
 }
 
 /**
- * Adds form element theming to an element if its title or description is set.
+ * Adds theming to composite form elements such as checkboxes and radios.
  *
- * This is used as a pre render function for checkboxes and radios.
+ * If either the title or description is set for the composite element as a * whole, then this adds either a 'fieldset' or 'form_element' theme wrapper to
+ * render them. A fieldset is generally preferred to assist screen readers
+ * (http://webaim.org/techniques/forms/screen_reader#group), but is not used
+ * when #title_display is set to an option impossible to render as a fieldset
+ * legend.
  */
-function form_pre_render_conditional_form_element($element) {
+function form_pre_render_composite_form_element($element) {
   // Set the element's title attribute to show #title as a tooltip, if needed.
   if (isset($element['#title']) && $element['#title_display'] == 'attribute') {
     $element['#attributes']['title'] = $element['#title'];
@@ -1256,7 +1271,16 @@ function form_pre_render_conditional_form_element($element) {
   }
 
   if (isset($element['#title']) || isset($element['#description'])) {
-    $element['#theme_wrappers'][] = 'form_element';
+    // To assist screen readers, use a fieldset wrapper when possible, but
+    // fieldset legends are only compatible with the 'before' and 'invisible'
+    // options for #title_display.
+    if (!isset($element['#title_display']) || in_array($element['#title_display'], array('before', 'invisible'))) {
+      $wrapper = 'fieldset';
+    }
+    else {
+      $wrapper = 'form_element';
+    }
+    $element['#theme_wrappers'][] = $wrapper;
   }
   return $element;
 }
@@ -2713,7 +2737,7 @@ function form_process_file($element) {
  *   property hides the label for everyone except screen readers.
  * - attribute: Set the title attribute on the element to create a tooltip
  *   but output no label element. This is supported only for checkboxes
- *   and radios in form_pre_render_conditional_form_element(). It is used
+ *   and radios in form_pre_render_composite_form_element(). It is used
  *   where a visual label is not needed, such as a table of checkboxes where
  *   the row and column provide the context. The tooltip will include the
  *   title and required marker.
diff --git a/core/modules/system/lib/Drupal/system/Tests/Form/ElementsLabelsTest.php b/core/modules/system/lib/Drupal/system/Tests/Form/ElementsLabelsTest.php
index 6c51a04..02bc1d9 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Form/ElementsLabelsTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Form/ElementsLabelsTest.php
@@ -78,10 +78,10 @@ function testFormLabels() {
     $this->assertFalse(isset($elements[0]), 'No label tag when title set not to display.');
 
     // Check #field_prefix and #field_suffix placement.
-    $elements = $this->xpath('//span[@class="field-prefix"]/following-sibling::div[@id="edit-form-radios-test"]');
+    $elements = $this->xpath('//span[@class="field-prefix"]/following-sibling::fieldset[@id="edit-form-radios-test"]');
     $this->assertTrue(isset($elements[0]), 'Properly placed the #field_prefix element after the label and before the field.');
 
-    $elements = $this->xpath('//span[@class="field-suffix"]/preceding-sibling::div[@id="edit-form-radios-test"]');
+    $elements = $this->xpath('//span[@class="field-suffix"]/preceding-sibling::fieldset[@id="edit-form-radios-test"]');
     $this->assertTrue(isset($elements[0]), 'Properly places the #field_suffix element immediately after the form field.');
 
     // Check #prefix and #suffix placement.
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 5424f66..172b2a1 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Form/FormTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Form/FormTest.php
@@ -94,7 +94,7 @@ function testRequiredFields() {
     $elements['file']['empty_values'] = $empty_strings;
 
     // Regular expression to find the expected marker on required elements.
-    $required_marker_preg = '@
\*@';
+    $required_marker_preg = '@<(label|legend).*\*(label|legend)>@';
 
     // Go through all the elements and all the empty values for them.
     foreach ($elements as $type => $data) {
@@ -520,7 +520,7 @@ function testDisabledElements() {
 
     // All the elements should be marked as disabled, including the ones below
     // the disabled container.
-    $this->assertEqual(count($disabled_elements), 39, 'The correct elements have the disabled property in the HTML code.');
+    $this->assertEqual(count($disabled_elements), 41, 'The correct elements have the disabled property in the HTML code.');
 
     $this->drupalPostForm(NULL, $edit, t('Submit'));
     $returned_values['hijacked'] = drupal_json_decode($this->content);
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 1657f9f..ec91d05 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -449,7 +449,7 @@ function system_element_info() {
     '#input' => TRUE,
     '#process' => array('form_process_radios'),
     '#theme_wrappers' => array('radios'),
-    '#pre_render' => array('form_pre_render_conditional_form_element'),
+    '#pre_render' => array('form_pre_render_composite_form_element'),
   );
   $types['radio'] = array(
     '#input' => TRUE,
@@ -463,7 +463,7 @@ function system_element_info() {
   $types['checkboxes'] = array(
     '#input' => TRUE,
     '#process' => array('form_process_checkboxes'),
-    '#pre_render' => array('form_pre_render_conditional_form_element'),
+    '#pre_render' => array('form_pre_render_composite_form_element'),
     '#theme_wrappers' => array('checkboxes'),
   );
   $types['checkbox'] = array(