diff --git a/core/includes/form.inc b/core/includes/form.inc
index 360155e..6e0d57b 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -191,10 +191,6 @@ function form_options_flatten($array) {
  *
  * 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.
@@ -207,7 +203,41 @@ function template_preprocess_select(&$variables) {
   Element\RenderElement::setAttributes($element, array('form-select'));
 
   $variables['attributes'] = $element['#attributes'];
-  $variables['options'] = form_select_options($element);
+  $variables['options'] = drupal_render_children($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 template_preprocess_optgroup(&$variables) {
+  $element = $variables['element'];
+
+  $variables['attributes'] = $element['#attributes'];
+  $variables['options'] = drupal_render_children($element);
+}
+
+/**
+ * Prepares variables for option element templates.
+ *
+ * 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 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 b349918..8b668b3 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -2396,6 +2396,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/Render/Element/Optgroup.php b/core/lib/Drupal/Core/Render/Element/Optgroup.php
new file mode 100644
index 0000000..03bc474
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Element/Optgroup.php
@@ -0,0 +1,105 @@
+<?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 FormElement {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    $class = get_class($this);
+    return array(
+      // Optgroup elements only ever appear inside of select elements and the
+      // select element handles the input for all optgroup elements.
+      '#input' => FALSE,
+      '#process' => array(
+        array($class, 'processOptgroup'),
+        array($class, 'processAjaxForm'),
+      ),
+      '#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';
+        $is_default = $value_valid && ((!$value_is_array && (string) $element['#value'] === $key || ($value_is_array && in_array($key, $element['#value']))) || $empty_choice);
+
+        $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;
+  }
+
+}
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..9877d69
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Element/Option.php
@@ -0,0 +1,54 @@
+<?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 single option of a select element.
+ *
+ * @FormElement("option")
+ *
+ * @see \Drupal\Core\Render\Element\Select
+ * @see \Drupal\Core\Render\Element\Optgroup
+ */
+class Option extends FormElement {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    $class = get_class($this);
+    return array(
+      // Option elements only ever appear inside of select elements and the
+      // select element handles the input for all option elements.
+      '#input' => FALSE,
+      '#process' => array(
+        array($class, 'processAjaxForm'),
+      ),
+      '#pre_render' => array(
+        array($class, 'preRenderOption'),
+      ),
+      '#theme' => 'option',
+      '#selected' => FALSE,
+    );
+  }
+
+  /**
+   * Prepares a select 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..08e3716 100644
--- a/core/lib/Drupal/Core/Render/Element/Select.php
+++ b/core/lib/Drupal/Core/Render/Element/Select.php
@@ -17,8 +17,11 @@
  * 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 {
+class Select extends Optgroup {
 
   /**
    * {@inheritdoc}
@@ -113,6 +116,20 @@ 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 the '#options' element in the same way that option groups do.
+    static::processOptgroup($element, $form_state, $complete_form);
+
     return $element;
   }
 
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>
