diff --git a/core/includes/form.inc b/core/includes/form.inc
index bd256ad..10ef614 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -18,14 +18,15 @@
  * 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.
+ * the #options property 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.
  *     Properties used: #title, #value, #options, #description, #extra,
- *     #multiple, #required, #name, #attributes, #size.
+ *     #multiple, #required, #name, #attributes, #size, #sort_options,
+ *     #sort_start.
  */
 function template_preprocess_select(&$variables) {
   $element = $variables['element'];
@@ -37,36 +38,17 @@ function template_preprocess_select(&$variables) {
 }
 
 /**
- * Converts an options form element into a structured array for output.
+ * Converts the options in a select element into a structured array for output.
  *
  * 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.
+ *   An associative array containing properties of the select element. See
+ *   \Drupal\Core\Render\Element\Select for details, but note that the
+ *   #empty_option and #empty_value properties are processed, and the
+ *   #value property is set, before reaching this function.
  * @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
@@ -90,7 +72,17 @@ function form_select_options($element, $choices = NULL) {
       return [];
     }
     $choices = $element['#options'];
+    $sort_options = isset($element['#sort_options']) && $element['#sort_options'];
+    $sort_start = isset($element['#sort_start']) ? $element['#sort_start'] : 0;
   }
+  else {
+    // We are within an option group.
+    $sort_options = isset($choices['#sort_options']) && $choices['#sort_options'];
+    $sort_start = isset($choices['#sort_start']) ? $choices['#sort_start'] : 0;
+    unset($choices['#sort_options']);
+    unset($choices['#sort_start']);
+  }
+
   // 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);
@@ -125,6 +117,14 @@ function form_select_options($element, $choices = NULL) {
       $options[] = $option;
     }
   }
+  if ($sort_options) {
+    $unsorted = array_slice($options, 0, $sort_start);
+    $sorted = array_slice($options, $sort_start);
+    uasort($sorted, function ($a, $b) {
+      return strcmp((string) $a['label'], (string) $b['label']);
+    });
+    $options = array_merge($unsorted, $sorted);
+  }
   return $options;
 }
 
diff --git a/core/lib/Drupal/Core/Render/Element/Select.php b/core/lib/Drupal/Core/Render/Element/Select.php
index 300c244..342b763 100644
--- a/core/lib/Drupal/Core/Render/Element/Select.php
+++ b/core/lib/Drupal/Core/Render/Element/Select.php
@@ -9,11 +9,32 @@
  * Provides a form element for a drop-down menu or scrolling selection box.
  *
  * Properties:
- * - #options: An associative array, where the keys are the values for each
- *   option, and the values are the option labels to be shown in the drop-down
- *   list. If a value is an array, it will be rendered similarly, but as an
- *   optgroup. The key of the sub-array will be used as the label for the
- *   optgroup. Nesting optgroups is not allowed.
+ * - #options: An associative array of options for the select. Do not use
+ *   placeholders that sanitize data in any labels, as doing so will lead to
+ *   double-escaping. Each array value can be:
+ *   - A single translated string representing an HTML option element, where
+ *     the outer array key is the option value and the translated string array
+ *     value is the option label. The option value will be visible in the HTML
+ *     and can be modified by malicious users, so it should not contain
+ *     sensitive information and should be treated as possibly malicious data in
+ *     processing.
+ *   - An array representing an HTML optgroup element. The outer array key
+ *     should be a translated string, and is used as the label for the group.
+ *     The inner array contains the options for the group (with the keys as
+ *     option values, and translated string values as option labels). Nesting
+ *     option groups is not supported.
+ *   - An object with an 'option' property. In this case, the outer array key
+ *     is ignored, and the contents of the 'option' property are interpreted as
+ *     an array of options to be merged with any other regular options and
+ *     option groups found in the outer array.
+ * - #sort_options: (optional) If set to TRUE (default is FALSE), sort the
+ *   options by their labels, after rendering and translation is complete.
+ *   Can be set within an option group to sort that group.
+ * - #sort_start: (optional) Option index to start sorting at, where 0 is the
+ *   first option (default is 0, and this only applies if #sort_options is
+ *   TRUE). Can be used within an option group. If an empty option is being
+ *   added automatically (see #empty_option and #empty_value properties), and
+ *   you want it to stay at the top of the list, be sure to start sorting at 1.
  * - #empty_option: (optional) The label to show for the first default option.
  *   By default, the label is automatically set to "- Select -" for a required
  *   field and "- None -" for an optional field.
@@ -68,6 +89,8 @@ public function getInfo() {
     return [
       '#input' => TRUE,
       '#multiple' => FALSE,
+      '#sort_options' => FALSE,
+      '#sort_start' => 0,
       '#process' => [
         [$class, 'processSelect'],
         [$class, 'processAjaxForm'],
diff --git a/core/modules/system/tests/modules/form_test/src/Form/FormTestSelectForm.php b/core/modules/system/tests/modules/form_test/src/Form/FormTestSelectForm.php
index d1b14d8..5c1f1d6 100644
--- a/core/modules/system/tests/modules/form_test/src/Form/FormTestSelectForm.php
+++ b/core/modules/system/tests/modules/form_test/src/Form/FormTestSelectForm.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Symfony\Component\HttpFoundation\JsonResponse;
 
 /**
@@ -125,11 +126,72 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       ],
     ];
 
+    // Add a select that should have its options left alone.
+    $form['unsorted'] = [
+      '#type' => 'select',
+      '#options' => $this->makeSortableOptions('uso'),
+    ];
+
+    // Add a select to test sorting at the top level, and with some of the
+    // option groups sorted and some left alone.
+    $sortable_options = $this->makeSortableOptions('sso');
+    $sortable_options['sso_zzgroup']['#sort_options'] = TRUE;
+    $form['sorted'] = [
+      '#type' => 'select',
+      '#options' => $sortable_options,
+      '#sort_options' => TRUE,
+      '#sort_start' => 3,
+    ];
+
+    // Add a select to test sorting with a -NONE- option included.
+    $sortable_none_options = $this->makeSortableOptions('sno');
+    $sortable_none_options['sno_zzgroup']['#sort_options'] = TRUE;
+    $form['sorted_none'] = [
+      '#type' => 'select',
+      '#options' => $sortable_none_options,
+      '#sort_options' => TRUE,
+      '#sort_start' => 4,
+      '#empty_value' => 'sno_empty',
+    ];
+
     $form['submit'] = ['#type' => 'submit', '#value' => 'Submit'];
     return $form;
   }
 
   /**
+   * Makes and returns a set of options to test sorting on.
+   *
+   * @param string $prefix
+   *   Prefix for the keys of the options.
+   *
+   * @return array
+   *   Options array, including option groups, for testing.
+   */
+  protected function makeSortableOptions($prefix) {
+    return [
+      // Don't use $this->t() here, because we really don't want these
+      // to be translated or added to localize.d.o. Using TranslatableMarkup
+      // in places tests that casting to string is working, however.
+      $prefix . '_first_element' => new TranslatableMarkup('first element'),
+      $prefix . '_second' => new TranslatableMarkup('second element'),
+      $prefix . '_zzgroup' => [
+        $prefix . '_gc' => new TranslatableMarkup('group c'),
+        $prefix . '_ga' => new TranslatableMarkup('group a'),
+        $prefix . '_gb' => 'group b',
+      ],
+      $prefix . '_yygroup' => [
+        $prefix . '_ge' => new TranslatableMarkup('group e'),
+        $prefix . '_gd' => new TranslatableMarkup('group d'),
+        $prefix . '_gf' => new TranslatableMarkup('group f'),
+      ],
+      $prefix . '_d' => 'd',
+      $prefix . '_c' => new TranslatableMarkup('main c'),
+      $prefix . '_b' => new TranslatableMarkup('main b'),
+      $prefix . '_a' => 'a',
+    ];
+  }
+
+  /**
    * {@inheritdoc}
    */
   public function submitForm(array &$form, FormStateInterface $form_state) {
diff --git a/core/modules/system/tests/src/Functional/Form/FormTest.php b/core/modules/system/tests/src/Functional/Form/FormTest.php
index bd45bbc..43b9bf3 100644
--- a/core/modules/system/tests/src/Functional/Form/FormTest.php
+++ b/core/modules/system/tests/src/Functional/Form/FormTest.php
@@ -464,6 +464,73 @@ public function testEmptySelect() {
   }
 
   /**
+   * Tests sorting and not sorting of options in a select element.
+   */
+  public function testSelectSorting() {
+    $this->drupalGet('form-test/select');
+    $content = $this->getSession()->getPage()->getContent();
+
+    // Verify the expected order of the options in the unsorted and
+    // sorted elements.
+    $expected_order = [
+      'uso_first',
+      'uso_second',
+      'uso_zzgroup',
+      'uso_gc',
+      'uso_ga',
+      'uso_gb',
+      'uso_yygroup',
+      'uso_ge',
+      'uso_gd',
+      'uso_gf',
+      'uso_d',
+      'uso_c',
+      'uso_b',
+      'uso_a',
+      'sso_first',
+      'sso_second',
+      'sso_zzgroup',
+      'sso_ga',
+      'sso_gb',
+      'sso_gc',
+      'sso_a',
+      'sso_d',
+      'sso_b',
+      'sso_c',
+      'sso_yygroup',
+      'sso_ge',
+      'sso_gd',
+      'sso_gf',
+      'sno_empty',
+      'sno_first',
+      'sno_second',
+      'sno_zzgroup',
+      'sno_ga',
+      'sno_gb',
+      'sno_gc',
+      'sno_a',
+      'sno_d',
+      'sno_b',
+      'sno_c',
+      'sno_yygroup',
+      'sno_ge',
+      'sno_gd',
+      'sno_gf',
+    ];
+
+    $prev_position = 0;
+    foreach ($expected_order as $string) {
+      $position = strpos($content, $string);
+      $this->assertTrue($position > $prev_position, 'Item ' . $string . ' is in correct order');
+      $prev_position = $position;
+    }
+
+    // Verify that #sort_order and #sort_start are not in the page.
+    $this->assertFalse(strpos($content, '#sort_order'), 'Sort order property has been removed');
+    $this->assertFalse(strpos($content, '#sort_start'), 'Sort start property has been removed');
+  }
+
+  /**
    * Tests validation of #type 'number' and 'range' elements.
    */
   public function testNumber() {
