diff --git a/core/modules/field_ui/css/field_ui.admin.css b/core/modules/field_ui/css/field_ui.admin.css
index f26c86a24bb34bf7846327ea8344dd415143f868..ec20c062133cd59338b691a7db1f4de9f515d7ff 100644
--- a/core/modules/field_ui/css/field_ui.admin.css
+++ b/core/modules/field_ui/css/field_ui.admin.css
@@ -22,6 +22,10 @@
   font-size: 1em;
 }
 
+.allowed-values-table .form-item:where(:not(.hidden)) {
+  display: inline-table;
+}
+
 /* 'Manage form display' and 'Manage display' overview */
 .field-ui-overview .field-plugin-summary-cell {
   line-height: 1em;
diff --git a/core/modules/options/options.module b/core/modules/options/options.module
index 30aefdd33cadbdaea59dec819eb4afd74f51f122..608f4bcc6586553976e12ab1fdbf416a931f0315 100644
--- a/core/modules/options/options.module
+++ b/core/modules/options/options.module
@@ -5,10 +5,12 @@
  * Defines selection, check box and radio button widgets for text and numeric fields.
  */
 
+use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Url;
 use Drupal\Core\Entity\FieldableEntityInterface;
 use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\field\FieldStorageConfigInterface;
 
@@ -136,3 +138,20 @@ function _options_values_in_use($entity_type, $field_name, $values) {
 
   return FALSE;
 }
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * Add additional classes to enable styling for list field types.
+ *
+ * @see \Drupal\options\Plugin\Field\FieldType\ListItemBase::storageSettingsForm
+ */
+function options_form_field_storage_config_edit_form_alter(&$form, FormStateInterface $form_state) {
+  $table = &NestedArray::getValue($form, ['settings', 'allowed_values', 'table']);
+  if (!$table) {
+    return;
+  }
+
+  $form['#attached']['library'][] = 'field_ui/drupal.field_ui';
+  $table['#attributes']['class'][] = 'allowed-values-table';
+}
diff --git a/core/modules/options/src/Plugin/Field/FieldType/ListFloatItem.php b/core/modules/options/src/Plugin/Field/FieldType/ListFloatItem.php
index 0799b5fa6aad1fa477ff91c3828b04b17a686da9..10006d6f0aecb4e39641407a8792b83b82fc2566 100644
--- a/core/modules/options/src/Plugin/Field/FieldType/ListFloatItem.php
+++ b/core/modules/options/src/Plugin/Field/FieldType/ListFloatItem.php
@@ -4,6 +4,8 @@
 
 use Drupal\Core\Field\FieldFilteredMarkup;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\TypedData\DataDefinition;
 
@@ -52,11 +54,7 @@ public static function schema(FieldStorageDefinitionInterface $field_definition)
    * {@inheritdoc}
    */
   protected function allowedValuesDescription() {
-    $description = '<p>' . $this->t('The possible values this field can contain. Enter one value per line, in the format key|label.');
-    $description .= '<br/>' . $this->t('The key is the stored value, and must be numeric. The label will be used in displayed values and edit forms.');
-    $description .= '<br/>' . $this->t('The label is optional: if a line contains a single number, it will be used as key and label.');
-    $description .= '<br/>' . $this->t('Lists of labels are also accepted (one label per line), only if the field does not hold any values yet. Numeric keys will be automatically generated from the positions in the list.');
-    $description .= '</p>';
+    $description = '<p>' . $this->t('The name will be used in displayed options and edit forms. The value is the stored value, and must be numeric.') . '</p>';
     $description .= '<p>' . $this->t('Allowed HTML tags in labels: @tags', ['@tags' => FieldFilteredMarkup::displayAllowedTags()]) . '</p>';
     return $description;
   }
@@ -115,4 +113,20 @@ protected static function castAllowedValue($value) {
     return (float) $value;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
+    $element = parent::storageSettingsForm($form, $form_state, $has_data);
+
+    foreach (Element::children($element['allowed_values']['table']) as $delta => $row) {
+      // @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/number
+      // @see \Drupal\Core\Field\Plugin\Field\FieldWidget\NumberWidget::formElement()
+      $element['allowed_values']['table'][$delta]['item']['key']['#step'] = 'any';
+      $element['allowed_values']['table'][$delta]['item']['key']['#type'] = 'number';
+    }
+
+    return $element;
+  }
+
 }
diff --git a/core/modules/options/src/Plugin/Field/FieldType/ListIntegerItem.php b/core/modules/options/src/Plugin/Field/FieldType/ListIntegerItem.php
index eabec6f0222b95a9110dd155cc7b2896b1b26514..ce9bef1927700575640c0c38862cdf506beecefc 100644
--- a/core/modules/options/src/Plugin/Field/FieldType/ListIntegerItem.php
+++ b/core/modules/options/src/Plugin/Field/FieldType/ListIntegerItem.php
@@ -4,6 +4,8 @@
 
 use Drupal\Core\Field\FieldFilteredMarkup;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\TypedData\DataDefinition;
 
@@ -52,11 +54,7 @@ public static function schema(FieldStorageDefinitionInterface $field_definition)
    * {@inheritdoc}
    */
   protected function allowedValuesDescription() {
-    $description = '<p>' . $this->t('The possible values this field can contain. Enter one value per line, in the format key|label.');
-    $description .= '<br/>' . $this->t('The key is the stored value, and must be numeric. The label will be used in displayed values and edit forms.');
-    $description .= '<br/>' . $this->t('The label is optional: if a line contains a single number, it will be used as key and label.');
-    $description .= '<br/>' . $this->t('Lists of labels are also accepted (one label per line), only if the field does not hold any values yet. Numeric keys will be automatically generated from the positions in the list.');
-    $description .= '</p>';
+    $description = '<p>' . $this->t('The name will be used in displayed options and edit forms. The value is the stored value, and must be numeric.') . '</p>';
     $description .= '<p>' . $this->t('Allowed HTML tags in labels: @tags', ['@tags' => FieldFilteredMarkup::displayAllowedTags()]) . '</p>';
     return $description;
   }
@@ -77,4 +75,19 @@ protected static function castAllowedValue($value) {
     return (int) $value;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
+    $element = parent::storageSettingsForm($form, $form_state, $has_data);
+
+    foreach (Element::children($element['allowed_values']['table']) as $delta => $row) {
+      // @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/number
+      // @see \Drupal\Core\Field\Plugin\Field\FieldWidget\NumberWidget::formElement()
+      $element['allowed_values']['table'][$delta]['item']['key']['#type'] = 'number';
+    }
+
+    return $element;
+  }
+
 }
diff --git a/core/modules/options/src/Plugin/Field/FieldType/ListItemBase.php b/core/modules/options/src/Plugin/Field/FieldType/ListItemBase.php
index db91355540c32300018f46b915eb4b6f1fa357d1..c4d4ef997ec86ed0c94d7eb402921962bb45097e 100644
--- a/core/modules/options/src/Plugin/Field/FieldType/ListItemBase.php
+++ b/core/modules/options/src/Plugin/Field/FieldType/ListItemBase.php
@@ -2,10 +2,13 @@
 
 namespace Drupal\options\Plugin\Field\FieldType;
 
+use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\NestedArray;
 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;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\TypedData\OptionsProviderInterface;
@@ -84,24 +87,131 @@ public function isEmpty() {
    * {@inheritdoc}
    */
   public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
-    $allowed_values = $this->getSetting('allowed_values');
+    if (!array_key_exists('allowed_values', $form_state->getStorage())) {
+      $form_state->set('allowed_values', $this->getFieldDefinition()->getSetting('allowed_values'));
+    }
+
+    $allowed_values = $form_state->getStorage()['allowed_values'];
     $allowed_values_function = $this->getSetting('allowed_values_function');
 
+    if (!$form_state->get('items_count')) {
+      $form_state->set('items_count', max(count($allowed_values), 0));
+    }
+
+    $wrapper_id = Html::getUniqueId('allowed-values-wrapper');
     $element['allowed_values'] = [
-      '#type' => 'textarea',
-      '#title' => $this->t('Allowed values list'),
-      '#default_value' => $this->allowedValuesString($allowed_values),
-      '#rows' => 10,
-      '#access' => empty($allowed_values_function),
       '#element_validate' => [[static::class, 'validateAllowedValues']],
       '#field_has_data' => $has_data,
-      '#field_name' => $this->getFieldDefinition()->getName(),
-      '#entity_type' => $this->getEntity()->getEntityTypeId(),
       '#allowed_values' => $allowed_values,
       '#required' => TRUE,
+      '#prefix' => '<div id="' . $wrapper_id . '">',
+      '#suffix' => '</div>',
+      '#access' => empty($allowed_values_function),
+      'help_text' => ['#markup' => $this->allowedValuesDescription()],
+    ];
+    $element['allowed_values']['table'] = [
+      '#type' => 'table',
+      '#header' => [
+        $this->t('Allowed values'),
+        $this->t('Delete'),
+        $this->t('Weight'),
+      ],
+      '#attributes' => [
+        'id' => 'allowed-values-order',
+      ],
+      '#tabledrag' => [
+        [
+          'action' => 'order',
+          'relationship' => 'sibling',
+          'group' => 'weight',
+        ],
+      ],
     ];
 
-    $element['allowed_values']['#description'] = $this->allowedValuesDescription();
+    $max = $form_state->get('items_count');
+    $entity_type_id = $this->getFieldDefinition()->getTargetEntityTypeId();
+    $field_name = $this->getFieldDefinition()->getName();
+    $current_keys = array_keys($allowed_values);
+    for ($delta = 0; $delta <= $max; $delta++) {
+      $element['allowed_values']['table'][$delta] = [
+        '#attributes' => [
+          'class' => ['draggable'],
+        ],
+        '#weight' => $delta,
+      ];
+      $element['allowed_values']['table'][$delta]['item'] = [
+        'label' => [
+          '#type' => 'textfield',
+          '#title' => $this->t('Name'),
+          '#weight' => -30,
+          '#default_value' => isset($current_keys[$delta]) ? $allowed_values[$current_keys[$delta]] : '',
+          '#required' => $delta === 0,
+        ],
+        'key' => [
+          '#type' => 'textfield',
+          '#maxlength' => 255,
+          '#title' => $this->t('Value'),
+          '#default_value' => $current_keys[$delta] ?? '',
+          '#weight' => -20,
+          '#required' => $delta === 0,
+        ],
+      ];
+      $element['allowed_values']['table'][$delta]['delete'] = [
+        '#type' => 'submit',
+        '#value' => $this->t('Remove'),
+        '#name' => "remove_row_button__$delta",
+        '#id' => "remove_row_button__$delta",
+        '#delta' => $delta,
+        '#submit' => [[static::class, 'deleteSubmit']],
+        '#limit_validation_errors' => [],
+        '#ajax' => [
+          'callback' => [static::class, 'deleteAjax'],
+          'wrapper' => $wrapper_id,
+          'effect' => 'fade',
+        ],
+      ];
+      $element['allowed_values']['table'][$delta]['weight'] = [
+        '#type' => 'weight',
+        '#title' => $this->t('Weight for row @number', ['@number' => $delta + 1]),
+        '#title_display' => 'invisible',
+        '#delta' => 50,
+        '#default_value' => 0,
+        '#attributes' => ['class' => ['weight']],
+      ];
+      if ($delta < count($allowed_values)) {
+        $query = \Drupal::entityQuery($entity_type_id)
+          ->accessCheck(FALSE)
+          ->condition($field_name, $current_keys[$delta]);
+        $entity_ids = $query->execute();
+        if (!empty($entity_ids)) {
+          $element['allowed_values']['table'][$delta]['item']['key']['#attributes']['disabled'] = 'disabled';
+          $element['allowed_values']['table'][$delta]['delete']['#attributes']['disabled'] = 'disabled';
+          $element['allowed_values']['table'][$delta]['delete'] += [
+            'message' => [
+              '#type' => 'item',
+              '#markup' => $this->t('Cannot be removed: option in use.'),
+            ],
+          ];
+        }
+      }
+    }
+    $element['allowed_values']['table']['#max_delta'] = $max;
+
+    $element['allowed_values']['add_more_allowed_values'] = [
+      '#type' => 'submit',
+      '#name' => 'add_more_allowed_values',
+      '#value' => $this->t('Add another item'),
+      '#attributes' => ['class' => ['field-add-more-submit']],
+      // Allow users to add another row without requiring existing rows to have
+      // values.
+      '#limit_validation_errors' => [],
+      '#submit' => [[static::class, 'addMoreSubmit']],
+      '#ajax' => [
+        'callback' => [static::class, 'addMoreAjax'],
+        'wrapper' => $wrapper_id,
+        'effect' => 'fade',
+      ],
+    ];
 
     $element['allowed_values_function'] = [
       '#type' => 'item',
@@ -114,6 +224,71 @@ public function storageSettingsForm(array &$form, FormStateInterface $form_state
     return $element;
   }
 
+  /**
+   * Adds a new option.
+   *
+   * @param array $form
+   *   The form array to add elements to.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public static function addMoreSubmit(array $form, FormStateInterface $form_state) {
+    $form_state->set('items_count', $form_state->get('items_count') + 1);
+    $form_state->setRebuild();
+  }
+
+  /**
+   * Ajax callback for the "Add another item" button.
+   */
+  public static function addMoreAjax(array $form, FormStateInterface $form_state) {
+    $button = $form_state->getTriggeringElement();
+
+    // Go one level up in the form.
+    $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
+    $delta = $element['table']['#max_delta'];
+    $element['table'][$delta]['item']['#prefix'] = '<div class="ajax-new-content">' . ($element['table'][$delta]['item']['#prefix'] ?? '');
+    $element['table'][$delta]['item']['#suffix'] = ($element['table'][$delta]['item']['#suffix'] ?? '') . '</div>';
+
+    return $element;
+  }
+
+  /**
+   * Deletes a row/option.
+   *
+   * @param array $form
+   *   The form array to add elements to.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public static function deleteSubmit(array $form, FormStateInterface $form_state) {
+    $allowed_values = $form_state->getStorage()['allowed_values'];
+    $button = $form_state->getTriggeringElement();
+    $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
+    $item_to_be_removed = $element['item']['label']['#default_value'];
+    $remaining_allowed_values = array_diff($allowed_values, [$item_to_be_removed]);
+    $form_state->set('allowed_values', $remaining_allowed_values);
+
+    $delta = $button['#delta'];
+    $user_input = $form_state->getUserInput();
+    // The user input is directly modified to preserve the rest of the data on
+    // the page as it cannot be rebuilt from a fresh form state.
+    unset($user_input['settings']['allowed_values']['table'][$delta]);
+    $user_input['settings']['allowed_values']['table'] = array_values($user_input['settings']['allowed_values']['table']);
+    $form_state->setUserInput($user_input);
+    $form_state->set('items_count', $form_state->get('items_count') - 1);
+
+    $form_state->setRebuild();
+  }
+
+  /**
+   * Ajax callback for per row delete button.
+   */
+  public static function deleteAjax(array $form, FormStateInterface $form_state) {
+    $button = $form_state->getTriggeringElement();
+
+    return NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -3));
+  }
+
   /**
    * Provides the field type specific allowed values form element #description.
    *
@@ -134,7 +309,30 @@ abstract protected function allowedValuesDescription();
    * @see \Drupal\Core\Render\Element\FormElement::processPattern()
    */
   public static function validateAllowedValues($element, FormStateInterface $form_state) {
-    $values = static::extractAllowedValues($element['#value'], $element['#field_has_data']);
+    $items = array_filter(array_map(function ($item) use ($element) {
+      $current_element = $element['table'][$item];
+      if ($current_element['item']['key']['#value'] !== NULL && $current_element['item']['label']['#value']) {
+        return $current_element['item']['key']['#value'] . '|' . $current_element['item']['label']['#value'];
+      }
+      elseif ($current_element['item']['key']['#value']) {
+        return $current_element['item']['key']['#value'];
+      }
+      elseif ($current_element['item']['label']['#value']) {
+        return $current_element['item']['label']['#value'];
+      }
+
+      return NULL;
+    }, Element::children($element['table'])), function ($item) {
+      return $item;
+    });
+    if ($reordered_items = $form_state->getValue(['settings', 'allowed_values', 'table'])) {
+      uksort($items, function ($a, $b) use ($reordered_items) {
+        $a_weight = $reordered_items[$a]['weight'] ?? 0;
+        $b_weight = $reordered_items[$b]['weight'] ?? 0;
+        return $a_weight <=> $b_weight;
+      });
+    }
+    $values = static::extractAllowedValues($items, $element['#field_has_data']);
 
     if (!is_array($values)) {
       $form_state->setError($element, new TranslatableMarkup('Allowed values list: invalid input.'));
@@ -148,14 +346,6 @@ public static function validateAllowedValues($element, FormStateInterface $form_
         }
       }
 
-      // Prevent removing values currently in use.
-      if ($element['#field_has_data']) {
-        $lost_keys = array_keys(array_diff_key($element['#allowed_values'], $values));
-        if (_options_values_in_use($element['#entity_type'], $element['#field_name'], $lost_keys)) {
-          $form_state->setError($element, new TranslatableMarkup('Allowed values list: some values are being removed while currently in use.'));
-        }
-      }
-
       $form_state->setValueForElement($element, $values);
     }
   }
@@ -163,8 +353,8 @@ public static function validateAllowedValues($element, FormStateInterface $form_
   /**
    * Extracts the allowed values array from the allowed_values element.
    *
-   * @param string $string
-   *   The raw string to extract values from.
+   * @param string|array $list
+   *   The raw string or array to extract values from.
    * @param bool $has_data
    *   The current field already has data inserted or not.
    *
@@ -173,12 +363,15 @@ public static function validateAllowedValues($element, FormStateInterface $form_
    *
    * @see \Drupal\options\Plugin\Field\FieldType\ListItemBase::allowedValuesString()
    */
-  protected static function extractAllowedValues($string, $has_data) {
+  protected static function extractAllowedValues($list, $has_data) {
     $values = [];
 
-    $list = explode("\n", $string);
-    $list = array_map('trim', $list);
-    $list = array_filter($list, 'strlen');
+    if (is_string($list)) {
+      trigger_error('Passing a string to ' . __METHOD__ . '() is deprecated in drupal:10.2.0 and will be removed from drupal:11.0.0. Please use an array instead.', E_USER_DEPRECATED);
+      $list = explode("\n", $list);
+      $list = array_map('trim', $list);
+      $list = array_filter($list, 'strlen');
+    }
 
     $generated_keys = $explicit_keys = FALSE;
     foreach ($list as $position => $text) {
diff --git a/core/modules/options/src/Plugin/Field/FieldType/ListStringItem.php b/core/modules/options/src/Plugin/Field/FieldType/ListStringItem.php
index acd86c0fa201d053f8a1c08f5e42fdd4a55bb109..0fdba2b0c32441ee8bdad4a3c1a6427e42ebc8d7 100644
--- a/core/modules/options/src/Plugin/Field/FieldType/ListStringItem.php
+++ b/core/modules/options/src/Plugin/Field/FieldType/ListStringItem.php
@@ -4,6 +4,8 @@
 
 use Drupal\Core\Field\FieldFilteredMarkup;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\TypedData\DataDefinition;
 
@@ -54,9 +56,8 @@ public static function schema(FieldStorageDefinitionInterface $field_definition)
    * {@inheritdoc}
    */
   protected function allowedValuesDescription() {
-    $description = '<p>' . $this->t('The possible values this field can contain. Enter one value per line, in the format key|label.');
-    $description .= '<br/>' . $this->t('The key is the stored value. The label will be used in displayed values and edit forms.');
-    $description .= '<br/>' . $this->t('The label is optional: if a line contains a single string, it will be used as key and label.');
+    $description = '<p>' . $this->t('The name will be used in displayed options and edit forms.');
+    $description .= '<br/>' . $this->t('The value is automatically generated machine name of the name provided and will be the stored value.');
     $description .= '</p>';
     $description .= '<p>' . $this->t('Allowed HTML tags in labels: @tags', ['@tags' => FieldFilteredMarkup::displayAllowedTags()]) . '</p>';
     return $description;
@@ -78,4 +79,46 @@ protected static function castAllowedValue($value) {
     return (string) $value;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
+    $element = parent::storageSettingsForm($form, $form_state, $has_data);
+
+    // Improve user experience by using an automatically generated machine name.
+    foreach (Element::children($element['allowed_values']['table']) as $delta => $row) {
+      $element['allowed_values']['table'][$delta]['item']['key']['#type'] = 'machine_name';
+      $element['allowed_values']['table'][$delta]['item']['key']['#machine_name'] = [
+        'exists' => [static::class, 'exists'],
+      ];
+      $element['allowed_values']['table'][$delta]['item']['key']['#process'] = array_merge(
+        [[static::class, 'processAllowedValuesKey']],
+        // Workaround for https://drupal.org/i/1300290#comment-12873635.
+        \Drupal::service('plugin.manager.element_info')->getInfoProperty('machine_name', '#process', []),
+      );
+    }
+
+    return $element;
+  }
+
+  /**
+   * Sets the machine name source to be the label.
+   */
+  public static function processAllowedValuesKey(array &$element): array {
+    $parents = $element['#parents'];
+    array_pop($parents);
+    $parents[] = 'label';
+    $element['#machine_name']['source'] = $parents;
+    return $element;
+  }
+
+  /**
+   * Checks for existing keys for allowed values.
+   */
+  public static function exists(): bool {
+    // Without access to the current form state, we cannot know if a given key
+    // is in use. Return FALSE in all cases.
+    return FALSE;
+  }
+
 }
diff --git a/core/modules/options/tests/src/Functional/OptionsFieldUITest.php b/core/modules/options/tests/src/Functional/OptionsFieldUITest.php
index 3ab68bf635f391dc9c1550f72103d5c1e6ccc8f3..184225f52a4e48cc12d83f98ceca16d2ff136b8a 100644
--- a/core/modules/options/tests/src/Functional/OptionsFieldUITest.php
+++ b/core/modules/options/tests/src/Functional/OptionsFieldUITest.php
@@ -91,25 +91,39 @@ protected function setUp(): void {
   public function testOptionsAllowedValuesInteger() {
     $this->fieldName = 'field_options_integer';
     $this->createOptionsField('list_integer');
+    $page = $this->getSession()->getPage();
+    $assert_session = $this->assertSession();
 
-    // Flat list of textual values.
-    $string = "Zero\nOne";
-    $array = ['0' => 'Zero', '1' => 'One'];
-    $this->assertAllowedValuesInput($string, $array, 'Unkeyed lists are accepted.');
     // Explicit integer keys.
-    $string = "0|Zero\n2|Two";
-    $array = ['0' => 'Zero', '2' => 'Two'];
-    $this->assertAllowedValuesInput($string, $array, 'Integer keys are accepted.');
-    // Check that values can be added and removed.
-    $string = "0|Zero\n1|One";
-    $array = ['0' => 'Zero', '1' => 'One'];
-    $this->assertAllowedValuesInput($string, $array, 'Values can be added and removed.');
+    $input = [
+      'settings[allowed_values][table][0][item][key]' => 0,
+      'settings[allowed_values][table][0][item][label]' => 'Zero',
+      'settings[allowed_values][table][1][item][key]' => 2,
+      'settings[allowed_values][table][1][item][label]' => 'Two',
+    ];
+    $array = [0 => 'Zero', 2 => 'Two'];
+    $this->assertAllowedValuesInput($input, $array, 'Integer keys are accepted.');
+
     // Non-integer keys.
-    $this->assertAllowedValuesInput("1.1|One", 'keys must be integers', 'Non integer keys are rejected.');
-    $this->assertAllowedValuesInput("abc|abc", 'keys must be integers', 'Non integer keys are rejected.');
-    // Mixed list of keyed and unkeyed values.
-    $this->assertAllowedValuesInput("Zero\n1|One", 'invalid input', 'Mixed lists are rejected.');
+    $input = [
+      'settings[allowed_values][table][0][item][key]' => 1.1,
+      'settings[allowed_values][table][0][item][label]' => 'One',
+    ];
+    $this->assertAllowedValuesInput($input, 'keys must be integers', 'Non integer keys are rejected.');
+    $input = [
+      'settings[allowed_values][table][0][item][key]' => 'abc',
+      'settings[allowed_values][table][0][item][label]' => 'abc',
+    ];
+    $this->assertAllowedValuesInput($input, 'keys must be integers', 'Non integer keys are rejected.');
 
+    $input = [
+      'settings[allowed_values][table][0][item][key]' => 0,
+      'settings[allowed_values][table][0][item][label]' => 'Zero',
+      'settings[allowed_values][table][1][item][key]' => 1,
+      'settings[allowed_values][table][1][item][label]' => 'One',
+    ];
+    $array = [0 => 'Zero', 1 => 'One'];
+    $this->assertAllowedValuesInput($input, $array, '');
     // Create a node with actual data for the field.
     $settings = [
       'type' => $this->type,
@@ -117,28 +131,30 @@ public function testOptionsAllowedValuesInteger() {
     ];
     $node = $this->drupalCreateNode($settings);
 
-    // Check that a flat list of values is rejected once the field has data.
-    $this->assertAllowedValuesInput("Zero\nOne", 'invalid input', 'Unkeyed lists are rejected once the field has data.');
-
-    // Check that values can be added but values in use cannot be removed.
-    $string = "0|Zero\n1|One\n2|Two";
-    $array = ['0' => 'Zero', '1' => 'One', '2' => 'Two'];
-    $this->assertAllowedValuesInput($string, $array, 'Values can be added.');
-    $string = "0|Zero\n1|One";
-    $array = ['0' => 'Zero', '1' => 'One'];
-    $this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.');
-    $this->assertAllowedValuesInput("0|Zero", 'some values are being removed while currently in use', 'Values in use cannot be removed.');
+    // Check that the values in use cannot be removed.
+    $this->drupalGet($this->adminPath);
+    $assert_session->elementExists('css', '#remove_row_button__1');
+    $delete_button_1 = $page->findById('remove_row_button__1');
+    $this->assertTrue($delete_button_1->hasAttribute('disabled'), 'Button is disabled');
 
     // Delete the node, remove the value.
     $node->delete();
-    $string = "0|Zero";
-    $array = ['0' => 'Zero'];
-    $this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.');
+    $this->drupalGet($this->adminPath);
+    $delete_button_1->click();
+    $assert_session->pageTextNotContains('Please wait');
+    $page->findById('edit-submit')->click();
+    $field_storage = FieldStorageConfig::loadByName('node', $this->fieldName);
+    $this->assertSame($field_storage->getSetting('allowed_values'), [0 => 'Zero']);
 
     // Check that the same key can only be used once.
-    $string = "0|Zero\n0|One";
+    $input = [
+      'settings[allowed_values][table][0][item][key]' => 0,
+      'settings[allowed_values][table][0][item][label]' => 'Zero',
+      'settings[allowed_values][table][1][item][key]' => 0,
+      'settings[allowed_values][table][1][item][label]' => 'One',
+    ];
     $array = ['0' => 'One'];
-    $this->assertAllowedValuesInput($string, $array, 'Same value cannot be used multiple times.');
+    $this->assertAllowedValuesInput($input, $array, 'Same value cannot be used multiple times.');
   }
 
   /**
@@ -147,24 +163,47 @@ public function testOptionsAllowedValuesInteger() {
   public function testOptionsAllowedValuesFloat() {
     $this->fieldName = 'field_options_float';
     $this->createOptionsField('list_float');
+    $page = $this->getSession()->getPage();
+    $assert_session = $this->assertSession();
 
-    // Flat list of textual values.
-    $string = "Zero\nOne";
-    $array = ['0' => 'Zero', '1' => 'One'];
-    $this->assertAllowedValuesInput($string, $array, 'Unkeyed lists are accepted.');
     // Explicit numeric keys.
-    $string = "0|Zero\n.5|Point five";
+    $input = [
+      'settings[allowed_values][table][0][item][key]' => 0,
+      'settings[allowed_values][table][0][item][label]' => 'Zero',
+      'settings[allowed_values][table][1][item][key]' => .5,
+      'settings[allowed_values][table][1][item][label]' => 'Point five',
+    ];
     $array = ['0' => 'Zero', '0.5' => 'Point five'];
-    $this->assertAllowedValuesInput($string, $array, 'Integer keys are accepted.');
-    // Check that values can be added and removed.
-    $string = "0|Zero\n.5|Point five\n1.0|One";
+    $this->assertAllowedValuesInput($input, $array, 'Integer keys are accepted.');
+
+    // Check that values can be added.
+    $input = [
+      'settings[allowed_values][table][0][item][key]' => 0,
+      'settings[allowed_values][table][0][item][label]' => 'Zero',
+      'settings[allowed_values][table][1][item][key]' => .5,
+      'settings[allowed_values][table][1][item][label]' => 'Point five',
+      'settings[allowed_values][table][2][item][key]' => 1,
+      'settings[allowed_values][table][2][item][label]' => 'One',
+    ];
     $array = ['0' => 'Zero', '0.5' => 'Point five', '1' => 'One'];
-    $this->assertAllowedValuesInput($string, $array, 'Values can be added and removed.');
+    $this->assertAllowedValuesInput($input, $array, 'Values can be added.');
     // Non-numeric keys.
-    $this->assertAllowedValuesInput("abc|abc\n", 'each key must be a valid integer or decimal', 'Non numeric keys are rejected.');
-    // Mixed list of keyed and unkeyed values.
-    $this->assertAllowedValuesInput("Zero\n1|One\n", 'invalid input', 'Mixed lists are rejected.');
-
+    $input = [
+      'settings[allowed_values][table][0][item][key]' => 'abc',
+      'settings[allowed_values][table][0][item][label]' => 'abc',
+    ];
+    $this->assertAllowedValuesInput($input, 'each key must be a valid integer or decimal', 'Non numeric keys are rejected.');
+
+    $input = [
+      'settings[allowed_values][table][0][item][key]' => 0,
+      'settings[allowed_values][table][0][item][label]' => 'Zero',
+      'settings[allowed_values][table][1][item][key]' => .5,
+      'settings[allowed_values][table][1][item][label]' => 'Point five',
+      'settings[allowed_values][table][2][item][key]' => 2,
+      'settings[allowed_values][table][2][item][label]' => 'Two',
+    ];
+    $array = ['0' => 'Zero', '0.5' => 'Point five', '2' => 'Two'];
+    $this->assertAllowedValuesInput($input, $array, '');
     // Create a node with actual data for the field.
     $settings = [
       'type' => $this->type,
@@ -172,33 +211,39 @@ public function testOptionsAllowedValuesFloat() {
     ];
     $node = $this->drupalCreateNode($settings);
 
-    // Check that a flat list of values is rejected once the field has data.
-    $this->assertAllowedValuesInput("Zero\nOne", 'invalid input', 'Unkeyed lists are rejected once the field has data.');
-
-    // Check that values can be added but values in use cannot be removed.
-    $string = "0|Zero\n.5|Point five\n2|Two";
-    $array = ['0' => 'Zero', '0.5' => 'Point five', '2' => 'Two'];
-    $this->assertAllowedValuesInput($string, $array, 'Values can be added.');
-    $string = "0|Zero\n.5|Point five";
-    $array = ['0' => 'Zero', '0.5' => 'Point five'];
-    $this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.');
-    $this->assertAllowedValuesInput("0|Zero", 'some values are being removed while currently in use', 'Values in use cannot be removed.');
+    // Check that the values in use cannot be removed.
+    $this->drupalGet($this->adminPath);
+    $assert_session->elementExists('css', '#remove_row_button__1');
+    $delete_button_1 = $page->findById('remove_row_button__1');
+    $this->assertTrue($delete_button_1->hasAttribute('disabled'), 'Button is disabled');
 
     // Delete the node, remove the value.
     $node->delete();
-    $string = "0|Zero";
-    $array = ['0' => 'Zero'];
-    $this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.');
-
-    // Check that the same key can only be used once.
-    $string = "0.5|Point five\n0.5|Half";
+    $this->drupalGet($this->adminPath);
+    $delete_button_1->click();
+    $assert_session->pageTextNotContains('Please wait');
+    $page->findById('edit-submit')->click();
+    $field_storage = FieldStorageConfig::loadByName('node', $this->fieldName);
+    $this->assertSame($field_storage->getSetting('allowed_values'), [0 => 'Zero', 2 => 'Two']);
+
+    $input = [
+      'settings[allowed_values][table][0][item][key]' => .5,
+      'settings[allowed_values][table][0][item][label]' => 'Point five',
+      'settings[allowed_values][table][1][item][key]' => .5,
+      'settings[allowed_values][table][1][item][label]' => 'Half',
+    ];
     $array = ['0.5' => 'Half'];
-    $this->assertAllowedValuesInput($string, $array, 'Same value cannot be used multiple times.');
+    $this->assertAllowedValuesInput($input, $array, 'Same value cannot be used multiple times.');
 
     // Check that different forms of the same float value cannot be used.
-    $string = "0|Zero\n.5|Point five\n0.5|Half";
-    $array = ['0' => 'Zero', '0.5' => 'Half'];
-    $this->assertAllowedValuesInput($string, $array, 'Different forms of the same value cannot be used.');
+    $input = [
+      'settings[allowed_values][table][0][item][key]' => .5,
+      'settings[allowed_values][table][0][item][label]' => 'Point five',
+      'settings[allowed_values][table][1][item][key]' => 0.5,
+      'settings[allowed_values][table][1][item][label]' => 'Half',
+    ];
+    $array = ['0.5' => 'Half'];
+    $this->assertAllowedValuesInput($input, $array, 'Different forms of the same value cannot be used.');
   }
 
   /**
@@ -207,76 +252,78 @@ public function testOptionsAllowedValuesFloat() {
   public function testOptionsAllowedValuesText() {
     $this->fieldName = 'field_options_text';
     $this->createOptionsField('list_string');
+    $page = $this->getSession()->getPage();
+    $assert_session = $this->assertSession();
 
-    // Flat list of textual values.
-    $string = "Zero\nOne";
-    $array = ['Zero' => 'Zero', 'One' => 'One'];
-    $this->assertAllowedValuesInput($string, $array, 'Unkeyed lists are accepted.');
     // Explicit keys.
-    $string = "zero|Zero\none|One";
-    $array = ['zero' => 'Zero', 'one' => 'One'];
-    $this->assertAllowedValuesInput($string, $array, 'Explicit keys are accepted.');
-    // Check that values can be added and removed.
-    $string = "zero|Zero\ntwo|Two";
-    $array = ['zero' => 'Zero', 'two' => 'Two'];
-    $this->assertAllowedValuesInput($string, $array, 'Values can be added and removed.');
-    // Mixed list of keyed and unkeyed values.
-    $string = "zero|Zero\nOne\n";
-    $array = ['zero' => 'Zero', 'One' => 'One'];
-    $this->assertAllowedValuesInput($string, $array, 'Mixed lists are accepted.');
+    $input = [
+      'settings[allowed_values][table][0][item][key]' => '_zero',
+      'settings[allowed_values][table][0][item][label]' => 'Zero',
+      'settings[allowed_values][table][1][item][key]' => '_one',
+      'settings[allowed_values][table][1][item][label]' => 'One',
+    ];
+    $array = ['_zero' => 'Zero', '_one' => 'One'];
+    $this->assertAllowedValuesInput($input, $array, 'Explicit keys are accepted.');
+
     // Overly long keys.
-    $this->assertAllowedValuesInput("zero|Zero\n" . $this->randomMachineName(256) . "|One", 'each key must be a string at most 255 characters long', 'Overly long keys are rejected.');
+    $input = [
+      'settings[allowed_values][table][0][item][key]' => 'zero',
+      'settings[allowed_values][table][0][item][label]' => 'Zero',
+      'settings[allowed_values][table][1][item][key]' => $this->randomMachineName(256),
+      'settings[allowed_values][table][1][item][label]' => 'One',
+    ];
+    $this->assertAllowedValuesInput($input, 'each key must be a string at most 255 characters long', 'Overly long keys are rejected.');
 
+    $input = [
+      'settings[allowed_values][table][0][item][key]' => 'zero',
+      'settings[allowed_values][table][0][item][label]' => 'Zero',
+      'settings[allowed_values][table][1][item][key]' => 'one',
+      'settings[allowed_values][table][1][item][label]' => 'One',
+    ];
+    $array = ['zero' => 'Zero', 'one' => 'One'];
+    $this->assertAllowedValuesInput($input, $array, '');
     // Create a node with actual data for the field.
     $settings = [
       'type' => $this->type,
-      $this->fieldName => [['value' => 'One']],
+      $this->fieldName => [['value' => 'one']],
     ];
     $node = $this->drupalCreateNode($settings);
 
-    // Check that flat lists of values are still accepted once the field has
-    // data.
-    $string = "Zero\nOne";
-    $array = ['Zero' => 'Zero', 'One' => 'One'];
-    $this->assertAllowedValuesInput($string, $array, 'Unkeyed lists are still accepted once the field has data.');
-
-    // Check that values can be added but values in use cannot be removed.
-    $string = "Zero\nOne\nTwo";
-    $array = ['Zero' => 'Zero', 'One' => 'One', 'Two' => 'Two'];
-    $this->assertAllowedValuesInput($string, $array, 'Values can be added.');
-    $string = "Zero\nOne";
-    $array = ['Zero' => 'Zero', 'One' => 'One'];
-    $this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.');
-    $this->assertAllowedValuesInput("Zero", 'some values are being removed while currently in use', 'Values in use cannot be removed.');
+    // Check that the values in use cannot be removed.
+    $this->drupalGet($this->adminPath);
+    $assert_session->elementExists('css', '#remove_row_button__1');
+    $delete_button_1 = $page->findById('remove_row_button__1');
+    $value_field_1 = $page->findField('settings[allowed_values][table][1][item][key]');
+    $this->assertTrue($delete_button_1->hasAttribute('disabled'), 'Button is disabled');
+    $this->assertTrue($value_field_1->hasAttribute('disabled'), 'Button is disabled');
 
     // Delete the node, remove the value.
     $node->delete();
-    $string = "Zero";
-    $array = ['Zero' => 'Zero'];
-    $this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.');
-
-    // Check that string values with dots can be used.
-    $string = "Zero\nexample.com|Example";
-    $array = ['Zero' => 'Zero', 'example.com' => 'Example'];
-    $this->assertAllowedValuesInput($string, $array, 'String value with dot is supported.');
+    $this->drupalGet($this->adminPath);
+    $delete_button_1->click();
+    $assert_session->pageTextNotContains('Please wait');
+    $page->findById('edit-submit')->click();
+    $field_storage = FieldStorageConfig::loadByName('node', $this->fieldName);
+    $this->assertSame($field_storage->getSetting('allowed_values'), ['zero' => 'Zero']);
+
+    // Check that string values with dots can not be used.
+    $input = [
+      'settings[allowed_values][table][0][item][key]' => 'zero',
+      'settings[allowed_values][table][0][item][label]' => 'Zero',
+      'settings[allowed_values][table][1][item][key]' => 'example.com',
+      'settings[allowed_values][table][1][item][label]' => 'Example',
+    ];
+    $this->assertAllowedValuesInput($input, 'The machine-readable name must contain only lowercase letters, numbers, and underscores.', 'String value with dot is not supported.');
 
     // Check that the same key can only be used once.
-    $string = "zero|Zero\nzero|One";
+    $input = [
+      'settings[allowed_values][table][0][item][key]' => 'zero',
+      'settings[allowed_values][table][0][item][label]' => 'Zero',
+      'settings[allowed_values][table][1][item][key]' => 'zero',
+      'settings[allowed_values][table][1][item][label]' => 'One',
+    ];
     $array = ['zero' => 'One'];
-    $this->assertAllowedValuesInput($string, $array, 'Same value cannot be used multiple times.');
-  }
-
-  /**
-   * Options (text) : test 'trimmed values' input.
-   */
-  public function testOptionsTrimmedValuesText() {
-    $this->fieldName = 'field_options_trimmed_text';
-    $this->createOptionsField('list_string');
-
-    // Explicit keys.
-    $string = "zero |Zero\none | One";
-    $array = ['zero' => 'Zero', 'one' => 'One'];
-    $this->assertAllowedValuesInput($string, $array, 'Explicit keys are accepted and trimmed.');
+    $this->assertAllowedValuesInput($input, $array, 'Same value cannot be used multiple times.');
   }
 
   /**
@@ -307,11 +354,10 @@ protected function createOptionsField($type) {
   }
 
   /**
-   * Tests a string input for the 'allowed values' form element.
+   * Tests an input array for the 'allowed values' form element.
    *
-   * @param string $input_string
-   *   The input string, in the pipe-linefeed format expected by the form
-   *   element.
+   * @param array $input
+   *   The input array.
    * @param array|string $result
    *   Either an expected resulting array in
    *   $field->getSetting('allowed_values'), or an expected error message.
@@ -320,10 +366,14 @@ protected function createOptionsField($type) {
    *
    * @internal
    */
-  public function assertAllowedValuesInput(string $input_string, $result, string $message): void {
-    $edit = ['settings[allowed_values]' => $input_string];
+  public function assertAllowedValuesInput(array $input, $result, string $message): void {
     $this->drupalGet($this->adminPath);
-    $this->submitForm($edit, 'Save field settings');
+    $page = $this->getSession()->getPage();
+    $add_button = $page->findButton('Add another item');
+    $add_button->click();
+    $add_button->click();
+
+    $this->submitForm($input, 'Save field settings');
     // Verify that the page does not have double escaped HTML tags.
     $this->assertSession()->responseNotContains('&amp;lt;');
 
@@ -347,10 +397,15 @@ public function testNodeDisplay() {
     $on = $this->randomMachineName();
     $off = $this->randomMachineName();
     $edit = [
-      'settings[allowed_values]' => "1|$on" . PHP_EOL . "0|$off",
+      'settings[allowed_values][table][0][item][key]' => 1,
+      'settings[allowed_values][table][0][item][label]' => $on,
+      'settings[allowed_values][table][1][item][key]' => 0,
+      'settings[allowed_values][table][1][item][label]' => $off,
     ];
 
     $this->drupalGet($this->adminPath);
+    $page = $this->getSession()->getPage();
+    $page->findButton('Add another item')->click();
     $this->submitForm($edit, 'Save field settings');
     $this->assertSession()->pageTextContains('Updated field ' . $this->fieldName . ' field settings.');
 
@@ -397,13 +452,22 @@ public function testRequiredPropertyForAllowedValuesList() {
     foreach ($field_types as $field_type) {
       $this->fieldName = "field_options_$field_type";
       $this->createOptionsField($field_type);
+      $page = $this->getSession()->getPage();
 
       // Try to proceed without entering any value.
       $this->drupalGet($this->adminPath);
-      $this->submitForm([], 'Save field settings');
+      $page->findButton('Save field settings')->click();
 
-      // Confirmation message that this is a required field.
-      $this->assertSession()->pageTextContains('Allowed values list field is required.');
+      if ($field_type == 'list_string') {
+        // Asserting only name field as there is no value field for list_string.
+        $this->assertSession()->pageTextContains('Name field is required.');
+      }
+      else {
+        // Confirmation message that name and value are required fields for
+        // list_float and list_integer.
+        $this->assertSession()->pageTextContains('Name field is required.');
+        $this->assertSession()->pageTextContains('Value field is required.');
+      }
     }
   }
 
diff --git a/core/modules/options/tests/src/Functional/OptionsFloatFieldImportTest.php b/core/modules/options/tests/src/Functional/OptionsFloatFieldImportTest.php
index 8365d3f1648af187b01bc7843e06548e0b609e85..dfd5ad0645e639b344466547b41dbca05fda950d 100644
--- a/core/modules/options/tests/src/Functional/OptionsFloatFieldImportTest.php
+++ b/core/modules/options/tests/src/Functional/OptionsFloatFieldImportTest.php
@@ -71,7 +71,12 @@ public function testImport() {
     $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync'));
 
     // Set the active to not use dots in the allowed values key names.
-    $edit = ['settings[allowed_values]' => "0|Zero\n1|One"];
+    $edit = [
+      'settings[allowed_values][table][0][item][key]' => 0,
+      'settings[allowed_values][table][0][item][label]' => 'Zero',
+      'settings[allowed_values][table][1][item][key]' => 1,
+      'settings[allowed_values][table][1][item][label]' => 'One',
+    ];
     $this->drupalGet($admin_path);
     $this->submitForm($edit, 'Save field settings');
     $field_storage = FieldStorageConfig::loadByName('node', $field_name);
diff --git a/core/modules/options/tests/src/FunctionalJavascript/OptionsFieldUITest.php b/core/modules/options/tests/src/FunctionalJavascript/OptionsFieldUITest.php
new file mode 100644
index 0000000000000000000000000000000000000000..9119628ac36d7395f90f8cd89b84b9ee15a2a6ce
--- /dev/null
+++ b/core/modules/options/tests/src/FunctionalJavascript/OptionsFieldUITest.php
@@ -0,0 +1,232 @@
+<?php
+
+namespace Drupal\Tests\options\FunctionalJavascript;
+
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+
+/**
+ * Tests the Options field UI functionality.
+ *
+ * @group options
+ */
+class OptionsFieldUITest extends WebDriverTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'node',
+    'options',
+    'field_ui',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * Machine name of the created content type.
+   *
+   * @var string
+   */
+  protected $type;
+
+  /**
+   * Name of the option field.
+   *
+   * @var string
+   */
+  protected $fieldName;
+
+  /**
+   * Admin path to manage field storage settings.
+   *
+   * @var string
+   */
+  protected $adminPath;
+
+  /**
+   * Node form path for created content type.
+   *
+   * @var string
+   */
+  protected $nodeFormPath;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    // Create test user.
+    $admin_user = $this->drupalCreateUser([
+      'bypass node access',
+      'administer node fields',
+      'administer node display',
+    ]);
+    $this->drupalLogin($admin_user);
+
+    $type = $this->drupalCreateContentType(['type' => 'plan']);
+    $this->type = $type->id();
+    $this->nodeFormPath = 'node/add/' . $this->type;
+  }
+
+  /**
+   * Tests option types allowed values.
+   *
+   * @dataProvider providerTestOptionsAllowedValues
+   */
+  public function testOptionsAllowedValues($option_type, $options, $is_string_option) {
+    $this->fieldName = 'field_options_text';
+    $this->createOptionsField($option_type);
+    $page = $this->getSession()->getPage();
+
+    $this->drupalGet($this->adminPath);
+
+    $i = 0;
+    foreach ($options as $option_key => $option_label) {
+      $page->fillField("settings[allowed_values][table][$i][item][label]", $option_label);
+      // Add keys if not string option list.
+      if (!$is_string_option) {
+        $page->fillField("settings[allowed_values][table][$i][item][key]", $option_key);
+      }
+      $page->pressButton('Add another item');
+      $i++;
+      $this->assertSession()->waitForElementVisible('css', "[name='settings[allowed_values][table][$i][item][label]']");
+    }
+    $page->pressButton('Save field settings');
+
+    // Test the order of the option list on node form.
+    $this->drupalGet($this->nodeFormPath);
+    $this->assertNodeFormOrder(['- None -', 'First', 'Second', 'Third']);
+
+    // Test the order of the option list on admin path.
+    $this->drupalGet($this->adminPath);
+    $this->assertOrder(['First', 'Second', 'Third', ''], $is_string_option);
+    $drag_handle = $page->find('css', '[data-drupal-selector="edit-settings-allowed-values-table-0"] .tabledrag-handle');
+    $target = $page->find('css', '[data-drupal-selector="edit-settings-allowed-values-table-2"]');
+
+    // Change the order the items appear.
+    $drag_handle->dragTo($target);
+    $this->assertOrder(['Second', 'Third', 'First', ''], $is_string_option);
+    $page->pressButton('Save field settings');
+
+    $this->drupalGet($this->nodeFormPath);
+    $this->assertNodeFormOrder(['- None -', 'Second', 'Third', 'First']);
+
+    $this->drupalGet($this->adminPath);
+
+    // Confirm the change in order was saved.
+    $this->assertOrder(['Second', 'Third', 'First', ''], $is_string_option);
+
+    // Delete an item.
+    $page->pressButton('remove_row_button__1');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertOrder(['Second', 'First', ''], $is_string_option);
+    $page->pressButton('Save field settings');
+
+    $this->drupalGet($this->nodeFormPath);
+    $this->assertNodeFormOrder(['- None -', 'Second', 'First']);
+
+    $this->drupalGet($this->adminPath);
+
+    // Confirm the item removal was saved.
+    $this->assertOrder(['Second', 'First', ''], $is_string_option);
+  }
+
+  /**
+   * Asserts the order of provided option list on admin path.
+   *
+   * @param array $expected
+   *   Expected order.
+   * @param bool $is_string_option
+   *   Whether the request is for string option list.
+   */
+  protected function assertOrder($expected, $is_string_option) {
+    $page = $this->getSession()->getPage();
+    if ($is_string_option) {
+      $inputs = $page->findAll('css', '.draggable .form-text.machine-name-source');
+    }
+    else {
+      $inputs = $page->findAll('css', '.draggable .form-text');
+    }
+    foreach ($expected as $step => $expected_input_value) {
+      $value = $inputs[$step]->getValue();
+      $this->assertSame($expected_input_value, $value, "Item $step should be $expected_input_value, but got $value");
+    }
+  }
+
+  /**
+   * Asserts the order of provided option list on node form.
+   *
+   * @param array $expected
+   *   Expected order.
+   */
+  protected function assertNodeFormOrder($expected) {
+    $elements = $this->assertSession()->selectExists('field_options_text')->findAll('css', 'option');
+    $elements = array_map(function ($element) {
+      return $element->getText();
+    }, $elements);
+    $this->assertSame($expected, $elements);
+  }
+
+  /**
+   * Helper function to create list field of a given type.
+   *
+   * @param string $type
+   *   One of 'list_integer', 'list_float' or 'list_string'.
+   */
+  protected function createOptionsField($type) {
+    // Create a field.
+    FieldStorageConfig::create([
+      'field_name' => $this->fieldName,
+      'entity_type' => 'node',
+      'type' => $type,
+    ])->save();
+    FieldConfig::create([
+      'field_name' => $this->fieldName,
+      'entity_type' => 'node',
+      'bundle' => $this->type,
+    ])->save();
+
+    \Drupal::service('entity_display.repository')
+      ->getFormDisplay('node', $this->type)
+      ->setComponent($this->fieldName)
+      ->save();
+
+    $this->adminPath = 'admin/structure/types/manage/' . $this->type . '/fields/node.' . $this->type . '.' . $this->fieldName . '/storage';
+  }
+
+  /**
+   * Data provider for testOptionsAllowedValues().
+   *
+   * @return array
+   *   Array of arrays with the following elements:
+   *   - Option type.
+   *   - Array of option type values.
+   *   - Whether option type is string type or not.
+   */
+  public function providerTestOptionsAllowedValues() {
+    return [
+      'List integer' => [
+        'list_integer',
+        [1 => 'First', 2 => 'Second', 3 => 'Third'],
+        FALSE,
+      ],
+      'List float' => [
+        'list_float',
+        ['0.1' => 'First', '0.2' => 'Second', '0.3' => 'Third'],
+        FALSE,
+      ],
+      'List string' => [
+        'list_string',
+        ['first' => 'First', 'second' => 'Second', 'third' => 'Third'],
+        TRUE,
+      ],
+    ];
+  }
+
+}
diff --git a/core/themes/claro/css/theme/field-ui.admin.css b/core/themes/claro/css/theme/field-ui.admin.css
index bdda5e632a1be8b33f0e53efb9b96c0eb7a4a2eb..c2f601c20c68edd5caa263039e2888961136b116 100644
--- a/core/themes/claro/css/theme/field-ui.admin.css
+++ b/core/themes/claro/css/theme/field-ui.admin.css
@@ -94,3 +94,7 @@
 .field-settings-summary-cell li:first-child {
   font-size: 1em;
 }
+
+.allowed-values-table .form-item:where(:not(.hidden)) {
+  display: inline-table;
+}
diff --git a/core/themes/claro/css/theme/field-ui.admin.pcss.css b/core/themes/claro/css/theme/field-ui.admin.pcss.css
index 20fc4dc0b46b68ad34b8cb07ab2542ca7936fcc5..eb8ab5cfb7647482417e07d2d7e760d75edce0f4 100644
--- a/core/themes/claro/css/theme/field-ui.admin.pcss.css
+++ b/core/themes/claro/css/theme/field-ui.admin.pcss.css
@@ -75,3 +75,7 @@
 .field-settings-summary-cell li:first-child {
   font-size: 1em;
 }
+
+.allowed-values-table .form-item:where(:not(.hidden)) {
+  display: inline-table;
+}
diff --git a/core/themes/stable9/css/field_ui/field_ui.admin.css b/core/themes/stable9/css/field_ui/field_ui.admin.css
index f26c86a24bb34bf7846327ea8344dd415143f868..ec20c062133cd59338b691a7db1f4de9f515d7ff 100644
--- a/core/themes/stable9/css/field_ui/field_ui.admin.css
+++ b/core/themes/stable9/css/field_ui/field_ui.admin.css
@@ -22,6 +22,10 @@
   font-size: 1em;
 }
 
+.allowed-values-table .form-item:where(:not(.hidden)) {
+  display: inline-table;
+}
+
 /* 'Manage form display' and 'Manage display' overview */
 .field-ui-overview .field-plugin-summary-cell {
   line-height: 1em;