diff --git a/core/includes/form.inc b/core/includes/form.inc
index 260ffbe..210c7bf 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -12,7 +12,6 @@
 use Drupal\Component\Utility\Xss;
 use Drupal\Core\Database\Database;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Form\OptGroup;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Template\Attribute;
 use Symfony\Component\HttpFoundation\RedirectResponse;
@@ -119,34 +118,10 @@ function form_set_value($element, $value, FormStateInterface $form_state) {
 }
 
 /**
- * Allows PHP array processing of multiple select options with the same value.
- *
- * Used for form select elements which need to validate HTML option groups
- * and multiple options which may return the same value. Associative PHP arrays
- * cannot handle these structures, since they share a common key.
- *
- * @param $array
- *   The form options array to process.
- *
- * @return
- *   An array with all hierarchical elements flattened to a single array.
- *
- * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
- *   Use \Drupal\Core\Form\OptGroup::flattenOptions().
- */
-function form_options_flatten($array) {
-  return OptGroup::flattenOptions($array);
-}
-
-/**
  * Prepares variables for select element templates.
  *
  * Default template: select.html.twig.
  *
- * It is possible to group options together; to do this, change the format of
- * $options to an associative array in which the keys are group labels, and the
- * values are associative arrays in the normal $options format.
- *
  * @param $variables
  *   An associative array containing:
  *   - element: An associative array containing the properties of the element.
@@ -155,138 +130,43 @@ function form_options_flatten($array) {
  */
 function template_preprocess_select(&$variables) {
   $element = $variables['element'];
-  Element::setAttributes($element, array('id', 'name', 'size'));
-  Element\RenderElement::setAttributes($element, array('form-select'));
 
   $variables['attributes'] = $element['#attributes'];
-  $variables['options'] = form_select_options($element);
+  $variables['options'] = drupal_render_children($element);
 }
 
 /**
- * Converts an array of options into HTML, for use in select list form elements.
- *
- * This function calls itself recursively to obtain the values for each optgroup
- * within the list of options and when the function encounters an object with
- * an 'options' property inside $element['#options'].
- *
- * @param array $element
- *   An associative array containing the following key-value pairs:
- *   - #multiple: Optional Boolean indicating if the user may select more than
- *     one item.
- *   - #options: An associative array of options to render as HTML. Each array
- *     value can be a string, an array, or an object with an 'option' property:
- *     - A string or integer key whose value is a translated string is
- *       interpreted as a single HTML option element. Do not use placeholders
- *       that sanitize data: doing so will lead to double-escaping. Note that
- *       the key will be visible in the HTML and could be modified by malicious
- *       users, so don't put sensitive information in it.
- *     - A translated string key whose value is an array indicates a group of
- *       options. The translated string is used as the label attribute for the
- *       optgroup. Do not use placeholders to sanitize data: doing so will lead
- *       to double-escaping. The array should contain the options you wish to
- *       group and should follow the syntax of $element['#options'].
- *     - If the function encounters a string or integer key whose value is an
- *       object with an 'option' property, the key is ignored, the contents of
- *       the option property are interpreted as $element['#options'], and the
- *       resulting HTML is added to the output.
- *   - #value: Optional integer, string, or array representing which option(s)
- *     to pre-select when the list is first displayed. The integer or string
- *     must match the key of an option in the '#options' list. If '#multiple' is
- *     TRUE, this can be an array of integers or strings.
- * @param array|null $choices
- *   (optional) Either an associative array of options in the same format as
- *   $element['#options'] above, or NULL. This parameter is only used internally
- *   and is not intended to be passed in to the initial function call.
- *
- * @return string
- *   An HTML string of options and optgroups for use in a select form element.
+ * Prepares variables for option group element templates.
+ *
+ * Default template: optgroup.html.twig.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #title, #value, #options, #multiple, #attributes.
  */
-function form_select_options($element, $choices = NULL) {
-  if (!isset($choices)) {
-    if (empty($element['#options'])) {
-      return '';
-    }
-    $choices = $element['#options'];
-  }
-  // array_key_exists() accommodates the rare event where $element['#value'] is NULL.
-  // isset() fails in this situation.
-  $value_valid = isset($element['#value']) || array_key_exists('#value', $element);
-  $value_is_array = $value_valid && is_array($element['#value']);
-  // Check if the element is multiple select and no value has been selected.
-  $empty_value = (empty($element['#value']) && !empty($element['#multiple']));
-  $options = '';
-  foreach ($choices as $key => $choice) {
-    if (is_array($choice)) {
-      $options .= '<optgroup label="' . String::checkPlain($key) . '">';
-      $options .= form_select_options($element, $choice);
-      $options .= '</optgroup>';
-    }
-    elseif (is_object($choice) && isset($choice->option)) {
-      $options .= form_select_options($element, $choice->option);
-    }
-    else {
-      $key = (string) $key;
-      $empty_choice = $empty_value && $key == '_none';
-      if ($value_valid && ((!$value_is_array && (string) $element['#value'] === $key || ($value_is_array && in_array($key, $element['#value']))) || $empty_choice)) {
-        $selected = ' selected="selected"';
-      }
-      else {
-        $selected = '';
-      }
-      $options .= '<option value="' . String::checkPlain($key) . '"' . $selected . '>' . String::checkPlain($choice) . '</option>';
-    }
-  }
-  return SafeMarkup::set($options);
+function template_preprocess_optgroup(&$variables) {
+  $element = $variables['element'];
+
+  $variables['attributes'] = $element['#attributes'];
+  $variables['options'] = drupal_render_children($element);
 }
 
 /**
- * Returns the indexes of a select element's options matching a given key.
- *
- * This function is useful if you need to modify the options that are
- * already in a form element; for example, to remove choices which are
- * not valid because of additional filters imposed by another module.
- * One example might be altering the choices in a taxonomy selector.
- * To correctly handle the case of a multiple hierarchy taxonomy,
- * #options arrays can now hold an array of objects, instead of a
- * direct mapping of keys to labels, so that multiple choices in the
- * selector can have the same key (and label). This makes it difficult
- * to manipulate directly, which is why this helper function exists.
- *
- * This function does not support optgroups (when the elements of the
- * #options array are themselves arrays), and will return FALSE if
- * arrays are found. The caller must either flatten/restore or
- * manually do their manipulations in this case, since returning the
- * index is not sufficient, and supporting this would make the
- * "helper" too complicated and cumbersome to be of any help.
- *
- * As usual with functions that can return array() or FALSE, do not
- * forget to use === and !== if needed.
- *
- * @param $element
- *   The select element to search.
- * @param $key
- *   The key to look for.
+ * Prepares variables for option element templates.
  *
- * @return
- *   An array of indexes that match the given $key. Array will be
- *   empty if no elements were found. FALSE if optgroups were found.
+ * Default template: option.html.twig.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #title, #value, #attributes.
  */
-function form_get_options($element, $key) {
-  $keys = array();
-  foreach ($element['#options'] as $index => $choice) {
-    if (is_array($choice)) {
-      return FALSE;
-    }
-    elseif (is_object($choice)) {
-      if (isset($choice->option[$key])) {
-        $keys[] = $index;
-      }
-    }
-    elseif ($index == $key) {
-      $keys[] = $index;
-    }
-  }
-  return $keys;
+function template_preprocess_option(&$variables) {
+  $element = $variables['element'];
+
+  $variables['attributes'] = $element['#attributes'];
+  $variables['label'] = $element['#title'];
 }
 
 /**
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index d7387a2..c6cd375 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -2355,6 +2355,14 @@ function drupal_common_theme() {
       'render element' => 'element',
       'template' => 'select',
     ),
+    'option' => array(
+      'render element' => 'element',
+      'template' => 'option',
+    ),
+    'optgroup' => array(
+      'render element' => 'element',
+      'template' => 'optgroup',
+    ),
     'fieldset' => array(
       'render element' => 'element',
       'template' => 'fieldset',
diff --git a/core/lib/Drupal/Core/Form/FormValidator.php b/core/lib/Drupal/Core/Form/FormValidator.php
index ee2da65..51dd3fc 100644
--- a/core/lib/Drupal/Core/Form/FormValidator.php
+++ b/core/lib/Drupal/Core/Form/FormValidator.php
@@ -316,7 +316,7 @@ protected function performRequiredValidation(&$elements, FormStateInterface &$fo
 
     if (isset($elements['#options']) && isset($elements['#value'])) {
       if ($elements['#type'] == 'select') {
-        $options = OptGroup::flattenOptions($elements['#options']);
+        $options = Element\Select::getFlattenedOptions($elements);
       }
       else {
         $options = $elements['#options'];
diff --git a/core/lib/Drupal/Core/Form/OptGroup.php b/core/lib/Drupal/Core/Form/OptGroup.php
deleted file mode 100644
index 21e9855..0000000
--- a/core/lib/Drupal/Core/Form/OptGroup.php
+++ /dev/null
@@ -1,58 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\Core\Form\OptGroup.
- */
-
-namespace Drupal\Core\Form;
-
-/**
- * Provides helpers for HTML option groups.
- */
-class OptGroup {
-
-  /**
-   * Allows PHP array processing of multiple select options with the same value.
-   *
-   * Used for form select elements which need to validate HTML option groups
-   * and multiple options which may return the same value. Associative PHP
-   * arrays cannot handle these structures, since they share a common key.
-   *
-   * @param array $array
-   *   The form options array to process.
-   *
-   * @return array
-   *   An array with all hierarchical elements flattened to a single array.
-   */
-  public static function flattenOptions(array $array) {
-    $options = array();
-    static::doFlattenOptions($array, $options);
-    return $options;
-  }
-
-  /**
-   * Iterates over an array building a flat array with duplicate keys removed.
-   *
-   * This function also handles cases where objects are passed as array values.
-   *
-   * @param array $array
-   *   The form options array to process.
-   * @param array $options
-   *   The array of flattened options.
-   */
-  protected static function doFlattenOptions(array $array, array &$options) {
-    foreach ($array as $key => $value) {
-      if (is_object($value) && isset($value->option)) {
-        static::doFlattenOptions($value->option, $options);
-      }
-      elseif (is_array($value)) {
-        static::doFlattenOptions($value, $options);
-      }
-      else {
-        $options[$key] = 1;
-      }
-    }
-  }
-
-}
diff --git a/core/lib/Drupal/Core/Render/Element/OptGroup.php b/core/lib/Drupal/Core/Render/Element/OptGroup.php
new file mode 100644
index 0000000..a46e2a8
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Element/OptGroup.php
@@ -0,0 +1,157 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\Element\Option.
+ */
+
+namespace Drupal\Core\Render\Element;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element;
+
+/**
+ * Provides a form element for a group of options inside of a select element.
+ *
+ * @FormElement("optgroup")
+ *
+ * @see \Drupal\Core\Render\Element\Select
+ * @see \Drupal\Core\Render\Element\Option
+ */
+class OptGroup extends RenderElement {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    $class = get_class($this);
+    return array(
+      '#process' => array(
+        array($class, 'processOptGroup'),
+        array($class, 'processAjaxForm'),
+      ),
+      '#pre_render' => array(
+        array($class, 'preRenderOptGroup'),
+      ),
+      '#theme' => 'optgroup',
+      '#options' => array(),
+    );
+  }
+
+  /**
+   * Processes an option group form element.
+   *
+   * This expands the #options property of option groups into proper option
+   * elements.
+   *
+   * @param array $element
+   *   The form element to process. Properties used:
+   *   - #options: (optional) An array of options for this option group where
+   *     the keys are the option values and the values are the respective
+   *     labels. Instead of specifying the options in this way, respective
+   *     option elements can be created as child elements of an option group
+   *     element.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   * @param array $complete_form
+   *   The complete form structure.
+   *
+   * @return array
+   *   The processed element.
+   *
+   * @see _form_validate()
+   */
+  public static function processOptGroup(&$element, FormStateInterface $form_state, &$complete_form) {
+    // @TODO This check is taken over from \Drupal\Core\Render\Element\Radios
+    //   and \Drupal\Core\Render\Element\Checkboxes, but I do not understand why
+    //   it is necessary.
+    if (count($element['#options']) > 0) {
+      // @TODO This is ported from form_select_options(). But instead of
+      //   checking #value, #default_value should be used here and below, right?
+      $value_valid = isset($element['#value']) || array_key_exists('#value', $element);
+      $value_is_array = $value_valid && is_array($element['#value']);
+      // Check if the element is multiple select and no value has been selected.
+      $empty_value = (empty($element['#value']) && !empty($element['#multiple']));
+
+      $weight = 0;
+      foreach ($element['#options'] as $key => $choice) {
+        $key = (string) $key;
+
+        // Maintain order of options as defined in #options, in case the element
+        // defines custom option sub-elements, but does not define all option
+        // sub-elements.
+        $weight += 0.001;
+
+        $empty_choice = $empty_value && $key == '_none';
+
+        $element += array($key => array());
+        $element[$key] += array(
+          '#type' => 'option',
+          '#title' => $choice,
+          // The key is sanitized in Drupal\Core\Template\Attribute during output
+          // from the theme function.
+          '#return_value' => $key,
+          '#selected' => $value_valid && ((!$value_is_array && (string) $element['#value'] === $key || ($value_is_array && in_array($key, $element['#value']))) || $empty_choice),
+          '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
+          '#weight' => $weight,
+        );
+      }
+    }
+
+    return $element;
+  }
+
+  /**
+   * Prepares an option group render element.
+   */
+  public static function preRenderOptGroup($element) {
+    $element['#attributes']['label'] = $element['#title'];
+    return $element;
+  }
+
+  /**
+   * Returns a list of options for an option group element.
+   *
+   * This function assumes that the passed element has been processed by
+   * OptGroup::processOptgroup() prior to calling this function.
+   */
+  public static function getOptions($element) {
+    $options = [];
+
+    // @TODO This does not respect weights.
+    foreach (Element::children($element) as $child) {
+      if ($element[$child]['#type'] == 'option') {
+        $options[$child] = $element[$child]['#title'];
+      }
+    }
+
+    return $options;
+  }
+
+  /**
+   * Iterates over an array building a flat array with duplicate keys removed.
+   *
+   * Used for form select elements which need to validate HTML option groups
+   * and multiple options which may return the same value. Associative PHP
+   * arrays cannot handle these structures, since they share a common key.
+   *
+   * @param array $array
+   *   The form options array to process.
+   *
+   * @return array
+   *   An array with all hierarchical elements flattened to a single array.
+   */
+  public static function flattenOptions(array $array) {
+    $options = [];
+    foreach ($array as $key => $value) {
+      if (is_array($value)) {
+        $options += static::flattenOptions($value);
+      }
+      else {
+        $options[$key] = TRUE;
+      }
+    }
+    return $options;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Render/Element/Option.php b/core/lib/Drupal/Core/Render/Element/Option.php
new file mode 100644
index 0000000..036a10e
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Element/Option.php
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\Element\Option.
+ */
+
+namespace Drupal\Core\Render\Element;
+
+use Drupal\Core\Render\Element;
+
+/**
+ * Provides a form element for a single option of a select element.
+ *
+ * @FormElement("option")
+ *
+ * @see \Drupal\Core\Render\Element\Select
+ * @see \Drupal\Core\Render\Element\Optgroup
+ */
+class Option extends RenderElement {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    $class = get_class($this);
+    return array(
+      '#process' => array(
+        array($class, 'processAjaxForm'),
+      ),
+      '#pre_render' => array(
+        array($class, 'preRenderOption'),
+      ),
+      '#theme' => 'option',
+      '#selected' => FALSE,
+    );
+  }
+
+  /**
+   * Prepares an option render element.
+   */
+  public static function preRenderOption($element) {
+    $element['#attributes']['value'] = $element['#return_value'];
+    if ($element['#selected']) {
+      $element['#attributes']['selected'] = 'selected';
+    }
+    return $element;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Render/Element/Select.php b/core/lib/Drupal/Core/Render/Element/Select.php
index 70f14d6..ad3f13d 100644
--- a/core/lib/Drupal/Core/Render/Element/Select.php
+++ b/core/lib/Drupal/Core/Render/Element/Select.php
@@ -17,6 +17,9 @@
  * a select element, including behavior if #required is TRUE or FALSE.
  *
  * @FormElement("select")
+ *
+ * @see \Drupal\Core\Render\Element\Option
+ * @see \Drupal\Core\Render\Element\Optgroup
  */
 class Select extends FormElement {
 
@@ -113,6 +116,49 @@ public static function processSelect(&$element, FormStateInterface $form_state,
         $element['#options'] = $empty_option + $element['#options'];
       }
     }
+
+    // If there are option group elements, pass the '#value' and '#multiple'
+    // keys down so they can properly set the selected option.
+    // @see \Drupal\Core\Render\Element\OptGroup::processOptgroup()
+    foreach (Element::children($element) as $child) {
+      if ($element[$child]['#type'] == 'optgroup') {
+        $element[$child]['#value'] = $element['#value'];
+        $element[$child]['#multiple'] = $element['#multiple'];
+      }
+    }
+
+    // Process options and option groups.
+    $weight = 0;
+    foreach ($element['#options'] as $key => $choice) {
+      $key = (string) $key;
+
+      // Maintain order of options as defined in #options, in case both
+      // options and option groups are defined or the element defines custom
+      // option sub-elements, but does not define all option sub-elements.
+      $weight += 0.001;
+      $element[$key]['#weight'] = $weight;
+
+      // Expand option groups into proper elements.
+      if (is_array($choice)) {
+        $element += array($key => array());
+        $element[$key] += array(
+          '#type' => 'optgroup',
+          '#title' => $key,
+          // The key is sanitized in Drupal\Core\Template\Attribute during output
+          // from the theme function.
+          '#options' => $choice,
+          '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
+          '#value' => $element['#value'],
+        );
+        OptGroup::processOptGroup($element[$key], $form_state, $complete_form);
+
+        unset($element['#options'][$key]);
+      }
+    }
+
+    // Process the '#options' element in the same way that option groups do.
+    OptGroup::processOptGroup($element, $form_state, $complete_form);
+
     return $element;
   }
 
@@ -156,4 +202,25 @@ public static function preRenderSelect($element) {
     return $element;
   }
 
+  /**
+   * Returns a list of flattened options for a select element.
+   *
+   * This function assumes that the passed element has been processed by
+   * Select::processSelect() prior to calling this function.
+   */
+  public static function getFlattenedOptions($element) {
+    $options = [];
+
+    // @TODO This does not respect weights.
+    foreach (Element::children($element) as $child) {
+      if ($element[$child]['#type'] == 'optgroup') {
+        $options += OptGroup::getOptions($element[$child]);
+        unset($element[$child]);
+      }
+    }
+    $options += OptGroup::getOptions($element);
+
+    return $options;
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Render/Element/Weight.php b/core/lib/Drupal/Core/Render/Element/Weight.php
index ffcda6b..7b4d088 100644
--- a/core/lib/Drupal/Core/Render/Element/Weight.php
+++ b/core/lib/Drupal/Core/Render/Element/Weight.php
@@ -52,6 +52,7 @@ public static function processWeight(&$element, FormStateInterface $form_state,
       }
       $element['#options'] = $weights;
       $element += $element_info_manager->getInfo('select');
+      Select::processSelect($element, $form_state, $complete_form);
     }
     // Otherwise, use a text field.
     else {
diff --git a/core/modules/entity_reference/src/ConfigurableEntityReferenceItem.php b/core/modules/entity_reference/src/ConfigurableEntityReferenceItem.php
index 98ddf9b..633b647 100644
--- a/core/modules/entity_reference/src/ConfigurableEntityReferenceItem.php
+++ b/core/modules/entity_reference/src/ConfigurableEntityReferenceItem.php
@@ -11,7 +11,7 @@
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Form\OptGroup;
+use Drupal\Core\Render\Element\OptGroup;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\TypedData\OptionsProviderInterface;
 use Drupal\Core\TypedData\DataDefinition;
diff --git a/core/modules/options/src/Plugin/Field/FieldType/ListItemBase.php b/core/modules/options/src/Plugin/Field/FieldType/ListItemBase.php
index 05ec40d..0f11fd6 100644
--- a/core/modules/options/src/Plugin/Field/FieldType/ListItemBase.php
+++ b/core/modules/options/src/Plugin/Field/FieldType/ListItemBase.php
@@ -11,7 +11,7 @@
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldItemBase;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Form\OptGroup;
+use Drupal\Core\Render\Element\OptGroup;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\TypedData\OptionsProviderInterface;
 
diff --git a/core/modules/system/src/Tests/Render/Element/SelectTest.php b/core/modules/system/src/Tests/Render/Element/SelectTest.php
new file mode 100644
index 0000000..d2e57a6
--- /dev/null
+++ b/core/modules/system/src/Tests/Render/Element/SelectTest.php
@@ -0,0 +1,377 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Render\Element\SelectTest
+ */
+
+namespace Drupal\system\Tests\Render\Element;
+
+use Drupal\Core\Form\FormState;
+use Drupal\simpletest\KernelTestBase;
+
+/**
+ * Tests the rendering of a select element.
+ *
+ * @group system
+ * @group Render
+ *
+ * @see \Drupal\Core\Render\Element\Select
+ * @see \Drupal\Core\Render\Element\Optgroup
+ * @see \Drupal\Core\Render\Element\Option
+ */
+class SelectTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   *
+   * The theme functions are registered in system_theme().
+   */
+  public static $modules = array('system');
+
+  /**
+   * The form builder used in this test.
+   *
+   * @var \Drupal\Core\Form\FormBuilderInterface
+   */
+  protected $formBuilder;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->formBuilder = $this->container->get('form_builder');
+
+    /** @var \Drupal\Core\Extension\ThemeHandler $theme_handler */
+    $theme_handler = $this->container->get('theme_handler');
+    $theme_handler->enable(['stark']);
+    $theme_handler->setDefault('stark');
+  }
+
+  /**
+   * Tests the rendering of a select element.
+   */
+  public function testSelect() {
+    foreach ($this->providerTestSelect() as $test_case) {
+      $element = $test_case['element'];
+      // In order to avoid calling FormBuilderInterface::prepareForm() we have
+      // to supply some default values.
+      $element += [
+        '#id' => 'edit-select',
+        '#parents' => [],
+        '#array_parents' => [],
+        '#tree' => FALSE,
+        // Avoid the form builder trying to handle the (non-existent) input.
+        '#input' => FALSE,
+      ];
+
+      $output = $test_case['output'];
+      // The wrapper element never changes, so avoid having to specify it every
+      // time.
+      $output = '<div class="form-item form-type-select">' . $output . '</div>';
+
+      $form_id = 'select';
+      $form_state = new FormState();
+      $this->formBuilder->processForm($form_id, $element, $form_state);
+      $this->assertMarkupEquals($output, drupal_render($element));
+    }
+  }
+
+  /**
+   * Provides test data for SelectTest::testSelect().
+   *
+   * @return array
+   *   An array of test cases where each test case is an array with the
+   *   following keys:
+   *   - element: The select element to be rendered as part of the test. The
+   *     element may or may not contain additional option or option group
+   *     sub-elements.
+   *   - output: The markup that the rendering of the element is expected to
+   *     yield.
+   */
+  public function providerTestSelect() {
+    $test_case = [];
+    $test_cases = [];
+
+    // Test an empty select element.
+    $test_case['element'] = [
+      '#type' => 'select',
+      '#title' => 'Test select',
+    ];
+    $test_case['output'] = <<<HTML
+<label for="edit-select">Test select</label>
+<select id="edit-select" class="form-select">
+</select>
+HTML;
+    $test_cases[] = $test_case;
+
+    // Test a simple select element.
+    $test_case['element'] = [
+      '#type' => 'select',
+      '#title' => 'Test select',
+      '#options' => [
+        'spring' => 'Spring',
+        'summer' => 'Summer',
+        'fall' => 'Fall',
+        'winter' => 'Winter',
+      ],
+      '#value' => NULL,
+    ];
+    $test_case['output'] = <<<HTML
+<label for="edit-select">Test select</label>
+<select id="edit-select" class="form-select">
+  <option value="spring">Spring</option>
+  <option value="summer">Summer</option>
+  <option value="fall">Fall</option>
+  <option value="winter">Winter</option>
+</select>
+HTML;
+    $test_cases[] = $test_case;
+
+    // Test a select element with a default value.
+    // Re-use the information from the previous test case.
+    $test_case['element']['#value'] = 'summer';
+    $test_case['output'] = str_replace(
+      '<option value="summer">',
+      '<option value="summer" selected="selected">',
+      $test_case['output']
+    );
+    $test_cases[] = $test_case;
+
+    // Test a required select element.
+    $test_case['element'] = [
+      '#type' => 'select',
+      '#title' => 'Test select',
+      '#options' => [
+        'spring' => 'Spring',
+        'summer' => 'Summer',
+        'fall' => 'Fall',
+        'winter' => 'Winter',
+      ],
+      '#required' => TRUE,
+      '#value' => NULL,
+    ];
+    $test_case['output'] = <<<HTML
+<label for="edit-select" class="form-required">Test select</label>
+<select id="edit-select" class="form-select required" required="required" aria-required="true">
+  <option value="" selected="selected">- Select -</option>
+  <option value="spring">Spring</option>
+  <option value="summer">Summer</option>
+  <option value="fall">Fall</option>
+  <option value="winter">Winter</option>
+</select>
+HTML;
+    $test_cases[] = $test_case;
+
+    // Test a required select element with a default value.
+    // Re-use the information from the previous test case.
+    $test_case['element']['#value'] = 'summer';
+    $test_case['output'] = str_replace(
+      ['<option value="" selected="selected">', '<option value="summer">'],
+      ['<option value="">', '<option value="summer" selected="selected">'],
+      $test_case['output']
+    );
+    $test_cases[] = $test_case;
+
+    // Test a select element with option groups.
+    $test_case['element'] = [
+      '#type' => 'select',
+      '#title' => 'Test select',
+      '#options' => [
+        'Seasons' => [
+          'spring' => 'Spring',
+          'summer' => 'Summer',
+          'fall' => 'Fall',
+          'winter' => 'Winter',
+        ],
+        'Sports' => [
+          'football' => 'Football',
+          'baseball' => 'Baseball',
+          'basketball' => 'Basketball',
+          'hockey' => 'Hockey',
+        ],
+      ],
+      '#value' => NULL,
+    ];
+    $test_case['output'] = <<<HTML
+<label for="edit-select">Test select</label>
+<select id="edit-select" class="form-select">
+  <optgroup label="Seasons">
+    <option value="spring">Spring</option>
+    <option value="summer">Summer</option>
+    <option value="fall">Fall</option>
+    <option value="winter">Winter</option>
+  </optgroup>
+  <optgroup label="Sports">
+    <option value="football">Football</option>
+    <option value="baseball">Baseball</option>
+    <option value="basketball">Basketball</option>
+    <option value="hockey">Hockey</option>
+  </optgroup>
+
+</select>
+HTML;
+    $test_cases[] = $test_case;
+
+    // Test a select element with option groups and a default value.
+    // Re-use the information from the previous test case.
+    $test_case['element']['#value'] = 'summer';
+    $test_case['output'] = str_replace(
+      '<option value="summer">',
+      '<option value="summer" selected="selected">',
+      $test_case['output']
+    );
+    $test_cases[] = $test_case;
+
+    // Test a select element with attributes on the option elements.
+    $test_case['element'] = [
+      '#type' => 'select',
+      '#title' => 'Test select',
+      '#options' => [
+        'football' => 'Football',
+        'baseball' => 'Baseball',
+        'basketball' => 'Basketball',
+        'hockey' => 'Hockey',
+      ],
+      '#value' => NULL,
+      'football' => [
+        '#type' => 'option',
+        '#attributes' => ['data-league' => 'nfl'],
+      ],
+      'baseball' => [
+        '#type' => 'option',
+        '#attributes' => ['data-league' => 'mlb'],
+      ],
+      'basketball' => [
+        '#type' => 'option',
+        '#attributes' => ['data-league' => 'nba'],
+      ],
+      'hockey' => [
+        '#type' => 'option',
+        '#attributes' => ['data-league' => 'nhl'],
+      ],
+    ];
+    $test_case['output'] = <<<HTML
+<label for="edit-select">Test select</label>
+<select id="edit-select" class="form-select">
+  <option data-league="nfl" value="football">Football</option>
+  <option data-league="mlb" value="baseball">Baseball</option>
+  <option data-league="nba" value="basketball">Basketball</option>
+  <option data-league="nhl" value="hockey">Hockey</option>
+</select>
+HTML;
+    $test_cases[] = $test_case;
+
+    // Test a select element with option groups that have attributes.
+    $test_case['element'] = [
+      '#type' => 'select',
+      '#title' => 'Test select',
+      '#options' => [
+        'Seasons' => [
+          'spring' => 'Spring',
+          'summer' => 'Summer',
+          'fall' => 'Fall',
+          'winter' => 'Winter',
+        ],
+        'Sports' => [
+          'football' => 'Football',
+          'baseball' => 'Baseball',
+          'basketball' => 'Basketball',
+          'hockey' => 'Hockey',
+        ],
+      ],
+      '#value' => NULL,
+      'Seasons' => [
+        '#type' => 'optgroup',
+        '#attributes' => ['data-option-count' => 4]
+      ],
+      'Sports' => [
+        '#type' => 'optgroup',
+        '#attributes' => ['data-option-count' => 4]
+      ],
+    ];
+    $test_case['output'] = <<<HTML
+<label for="edit-select">Test select</label>
+<select id="edit-select" class="form-select">
+  <optgroup data-option-count="4" label="Seasons">
+    <option value="spring">Spring</option>
+    <option value="summer">Summer</option>
+    <option value="fall">Fall</option>
+    <option value="winter">Winter</option>
+  </optgroup>
+  <optgroup data-option-count="4" label="Sports">
+    <option value="football">Football</option>
+    <option value="baseball">Baseball</option>
+    <option value="basketball">Basketball</option>
+    <option value="hockey">Hockey</option>
+  </optgroup>
+</select>
+HTML;
+    $test_cases[] = $test_case;
+
+    // Test a select element with both options and option groups.
+    $test_case['element'] = [
+      '#type' => 'select',
+      '#title' => 'Test select',
+      '#options' => [
+        'football' => 'Football',
+        'baseball' => 'Baseball',
+        'Seasons' => [
+          'spring' => 'Spring',
+          'summer' => 'Summer',
+          'fall' => 'Fall',
+          'winter' => 'Winter',
+        ],
+        'basketball' => 'Basketball',
+        'hockey' => 'Hockey',
+      ],
+      '#value' => NULL,
+    ];
+    $test_case['output'] = <<<HTML
+<label for="edit-select">Test select</label>
+<select id="edit-select" class="form-select">
+  <option value="football">Football</option>
+  <option value="baseball">Baseball</option>
+  <optgroup label="Seasons">
+    <option value="spring">Spring</option>
+    <option value="summer">Summer</option>
+    <option value="fall">Fall</option>
+    <option value="winter">Winter</option>
+  </optgroup>
+  <option value="basketball">Basketball</option>
+  <option value="hockey">Hockey</option>
+</select>
+HTML;
+    $test_cases[] = $test_case;
+
+    return $test_cases;
+  }
+
+  /**
+   * Asserts that two strings of markup are equal.
+   *
+   * This method removes whitespace between HTML elements before comparing the
+   * two strings so that '<a> <b>' and '<a><b>' are considered equal.
+   *
+   * @param string $expected
+   *   The expected markup string.
+   * @param string $actual
+   *   The actual markup string.
+   * @param string $message
+   *   (optional) The assertion message. If omitted, a message will be generated
+   *   from the input parameters.
+   * @param string $group
+   *   (optional) The assertion group. Defaults to 'Other'.
+   *
+   * @return bool
+   *   TRUE if the assertion succeeded; FALSE otherwise.
+   */
+  protected function assertMarkupEquals($expected, $actual, $message = '', $group = 'Other') {
+    $subject = [trim($expected), trim($actual)];
+    list($expected, $actual) = preg_replace('/>\s+</', '><', $subject);
+    $this->assertIdentical($expected, $actual, $message, $group);
+  }
+
+}
diff --git a/core/modules/system/templates/optgroup.html.twig b/core/modules/system/templates/optgroup.html.twig
new file mode 100644
index 0000000..1efe368
--- /dev/null
+++ b/core/modules/system/templates/optgroup.html.twig
@@ -0,0 +1,13 @@
+{#
+/**
+ * @file
+ * Default theme implementation for an optgroup element.
+ *
+ * Available variables:
+ * - attributes: HTML attributes for the optgroup tag.
+ * - options: The option element children.
+ *
+ * @ingroup themeable
+ */
+#}
+<optgroup{{ attributes }}>{{ options }}</optgroup>
diff --git a/core/modules/system/templates/option.html.twig b/core/modules/system/templates/option.html.twig
new file mode 100644
index 0000000..d795da9
--- /dev/null
+++ b/core/modules/system/templates/option.html.twig
@@ -0,0 +1,13 @@
+{#
+/**
+ * @file
+ * Default theme implementation for an option element.
+ *
+ * Available variables:
+ * - attributes: HTML attributes for the option tag.
+ * - label: The option label.
+ *
+ * @ingroup themeable
+ */
+#}
+<option{{ attributes }}>{{ label }}</option>
diff --git a/core/modules/taxonomy/src/Plugin/Field/FieldType/TaxonomyTermReferenceItem.php b/core/modules/taxonomy/src/Plugin/Field/FieldType/TaxonomyTermReferenceItem.php
index a2965c9..17f6900 100644
--- a/core/modules/taxonomy/src/Plugin/Field/FieldType/TaxonomyTermReferenceItem.php
+++ b/core/modules/taxonomy/src/Plugin/Field/FieldType/TaxonomyTermReferenceItem.php
@@ -10,7 +10,7 @@
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Form\OptGroup;
+use Drupal\Core\Render\Element\OptGroup;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\TypedData\OptionsProviderInterface;
 
diff --git a/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php b/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php
index 439c8e6..07591eb 100644
--- a/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php
+++ b/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php
@@ -137,9 +137,7 @@ protected function valueForm(&$form, FormStateInterface $form_state) {
 
         if ($tree) {
           foreach ($tree as $term) {
-            $choice = new \stdClass();
-            $choice->option = array($term->id() => str_repeat('-', $term->depth) . String::checkPlain(\Drupal::entityManager()->getTranslationFromContext($term)->label()));
-            $options[] = $choice;
+            $options[$term->id()] = str_repeat('-', $term->depth) . String::checkPlain(\Drupal::entityManager()->getTranslationFromContext($term)->label());
           }
         }
       }
diff --git a/core/modules/views/src/Plugin/views/filter/InOperator.php b/core/modules/views/src/Plugin/views/filter/InOperator.php
index 4d442b8..a6a3f4a 100644
--- a/core/modules/views/src/Plugin/views/filter/InOperator.php
+++ b/core/modules/views/src/Plugin/views/filter/InOperator.php
@@ -10,6 +10,7 @@
 use Drupal\Component\Utility\String as UtilityString;
 use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element\OptGroup;
 use Drupal\views\Plugin\views\display\DisplayPluginBase;
 use Drupal\views\ViewExecutable;
 
@@ -428,7 +429,7 @@ public function validate() {
       }
 
       // Some filter_in_operator usage uses optgroups forms, so flatten it.
-      $flat_options = form_options_flatten($this->value_options, TRUE);
+      $flat_options = OptGroup::flattenOptions($this->value_options);
 
       // Remove every element which is not known.
       foreach ($this->value as $value) {
diff --git a/core/tests/Drupal/Tests/Core/Form/OptGroupTest.php b/core/tests/Drupal/Tests/Core/Render/Element/OptGroupTest.php
similarity index 48%
rename from core/tests/Drupal/Tests/Core/Form/OptGroupTest.php
rename to core/tests/Drupal/Tests/Core/Render/Element/OptGroupTest.php
index 6f353b9..5553f29 100644
--- a/core/tests/Drupal/Tests/Core/Form/OptGroupTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/Element/OptGroupTest.php
@@ -2,13 +2,13 @@
 
 /**
  * @file
- * Contains \Drupal\Tests\Core\Form\OptGroupTest.
+ * Contains \Drupal\Tests\Core\Render\Element\OptGroupTest.
  */
 
-namespace Drupal\Tests\Core\Form;
+namespace Drupal\Tests\Core\Render\Element;
 
+use Drupal\Core\Render\Element\OptGroup;
 use Drupal\Tests\UnitTestCase;
-use Drupal\Core\Form\OptGroup;
 
 /**
  * @coversDefaultClass \Drupal\Core\Form\OptGroup
@@ -22,7 +22,7 @@ class OptGroupTest extends UnitTestCase {
    * @dataProvider providerTestFlattenOptions
    */
   public function testFlattenOptions($options) {
-    $this->assertSame(array('foo' => 1), OptGroup::flattenOptions($options));
+    $this->assertSame(array('foo' => TRUE), OptGroup::flattenOptions($options));
   }
 
   /**
@@ -31,19 +31,12 @@ public function testFlattenOptions($options) {
    * @return array
    */
   public function providerTestFlattenOptions() {
-    $object1 = new \stdClass();
-    $object1->option = array('foo' => 'foo');
-    $object2 = new \stdClass();
-    $object2->option = array(array('foo' => 'foo'), array('foo' => 'foo'));
-    $object3 = new \stdClass();
+    $object = new \stdClass();
     return array(
       array(array('foo' => 'foo')),
       array(array(array('foo' => 'foo'))),
-      array(array($object1)),
-      array(array($object2)),
-      array(array($object1, $object2)),
-      array(array('foo' => $object3)),
-      array(array('foo' => $object3, $object1, array('foo' => 'foo'))),
+      array(array('foo' => $object)),
+      array(array('foo' => $object, array('foo' => 'foo'))),
     );
   }
 
