diff --git a/core/core.libraries.yml b/core/core.libraries.yml
index 0ff5fb09ec..0a83517cd5 100644
--- a/core/core.libraries.yml
+++ b/core/core.libraries.yml
@@ -214,6 +214,20 @@ drupal.dropbutton:
     - core/drupalSettings
     - core/jquery.once
 
+drupal.splitbutton:
+  version: VERSION
+  js:
+    misc/splitbutton/splitbutton.js: {}
+  css:
+    component:
+      misc/splitbutton/splitbutton.css: {}
+  dependencies:
+    - core/jquery
+    - core/drupal
+    - core/drupalSettings
+    - core/jquery.once
+    - core/jquery.ui
+
 drupal.entity-form:
   version: VERSION
   js:
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index d2d54979b7..57224905ea 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -1178,6 +1178,124 @@ function template_preprocess_container(&$variables) {
   $variables['attributes'] = $element['#attributes'];
 }
 
+/**
+ * Prepares variables for splitbutton templates.
+ *
+ * Default template: splitbutton.html.twig.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #id, #attributes.
+ */
+function template_preprocess_splitbutton(array &$variables) {
+  $element = $variables['element'];
+  $element += [
+    '#attributes' => [],
+    '#content_attributes' => [],
+    '#splitbutton_type' => [],
+  ];
+
+  // Attach the library and the required CSS classes.
+  $variables['#attached']['library'][] = 'core/drupal.splitbutton';
+  $variables['#attached']['drupalSettings']['splitbutton']['triggerLabelDefault'] = t('List additional actions');
+  $element['#attributes']['class'][] = 'js-splitbutton';
+  $element['#content_attributes']['class'][] = 'js-splitbutton-list';
+
+  if (empty($element['#content_attributes']['id'])) {
+    $base = isset($variables['element']['#id']) ? $variables['element']['#id'] . '-' : '';
+    $element['#content_attributes']['id'] = Html::getUniqueId($base . 'splitbutton-list');
+  }
+
+  if (!is_array($element['#splitbutton_type'])) {
+    $element['#splitbutton_type'] = [$element['#splitbutton_type']];
+  }
+  foreach ($element['#splitbutton_type'] as $type) {
+    $element['#attributes']['class'][] = Html::getClass('splitbutton--' . $type);
+  }
+
+  $variables += [
+    'main_items' => [],
+    'items' => [],
+  ];
+
+  foreach (Element::children($element) as $key) {
+    $group = !isset($variables['main_items']) || empty($variables['main_items']) ? 'main_items' : 'items';
+    if (isset($variables['element'][$key]['#splitbutton_main_item'])) {
+      $group = 'main_items';
+    }
+    if (isset($element['#main_items']) && empty($element['#main_items'])) {
+      $group = 'items';
+    }
+    $variables[$group][$key]['value'] = $variables['element'][$key];
+    $variables[$group][$key]['attributes'] = new Attribute(['role' => 'none']);
+
+    // If the current element is a link, we have to add the required attributes
+    // to  $link['#options']['attributes'].
+    // @see \Drupal\Core\Render\Element\Link::preRenderLink
+    if ($variables[$group][$key]['value']['#type'] === 'link' && isset($variables[$group][$key]['value']['#options']['attributes']) && !empty($variables[$group][$key]['value']['#options']['attributes'])) {
+      $variables[$group][$key]['value']['#options']['attributes']['class'][] = 'splitbutton__action';
+    }
+    else {
+      $variables[$group][$key]['value']['#attributes']['class'][] = 'splitbutton__action';
+    }
+
+    unset($variables[$group][$key]['value']['#markup']);
+    unset($variables[$group][$key]['value']['#children']);
+    unset($variables[$group][$key]['value']['#printed']);
+  }
+
+  foreach ($variables['main_items'] as &$main_item) {
+    if ($main_item['value']['#type'] === 'link' && isset($main_item['value']['#options']['attributes'])) {
+      $main_item['value']['#options']['attributes']['class'][] = 'splitbutton__action--main';
+    }
+    else {
+      $main_item['value']['#attributes']['class'][] = 'splitbutton__action--main';
+    }
+  }
+
+  foreach ($variables['items'] as &$item) {
+    if ($item['value']['#type'] === 'link' && isset($item['value']['#options']['attributes'])) {
+      $item['value']['#options']['attributes']['role'] = $element['#use_aria_menu_role'] ? 'menuitem' : NULL;
+      $item['value']['#options']['attributes']['class'][] = 'splitbutton__action--secondary';
+      $item['value']['#options']['attributes']['class'][] = 'js-splitbutton-action-secondary';
+    }
+    else {
+      $item['value']['#attributes']['role'] = $element['#use_aria_menu_role'] ? 'menuitem' : NULL;
+      $item['value']['#attributes']['class'][] = 'splitbutton__action--secondary';
+      $item['value']['#attributes']['class'][] = 'js-splitbutton-action-secondary';
+    }
+    $item['attributes']->setAttribute('role', $element['#use_aria_menu_role'] ? 'none' : NULL);
+  }
+
+  $variables['multiple'] = count($variables['items']) > 0;
+
+  // Special handling for form elements.
+  if (isset($element['#array_parents'])) {
+    // Assign an html ID.
+    if (!isset($element['#attributes']['id'])) {
+      $element['#attributes']['id'] = $element['#id'];
+    }
+  }
+
+  $list_id = $element['#content_attributes']['id'];
+  $button_id = Html::getUniqueId($list_id . '-toggle');
+  $instance_settings = [
+    'buttonId' => $button_id,
+    'noMainItems' => empty($element['#main_items']) && empty($variables['main_items']),
+    'triggerLabel' => !empty($element['#trigger_label']) ? (string) $element['#trigger_label'] : NULL,
+    'triggerLabelVisible' => !empty($element['#trigger_label_visible']),
+  ];
+  $variables['#attached']['drupalSettings']['splitbutton']['instances'][$list_id] = array_filter($instance_settings);
+
+  $variables['attributes'] = $element['#attributes'];
+  $variables['content_attributes'] = [
+    'role' => $element['#use_aria_menu_role'] ? 'menu' : NULL,
+    'aria-labelledby' => $button_id,
+  ] + $element['#content_attributes'];
+
+}
+
 /**
  * Prepares variables for maintenance task list templates.
  *
@@ -1882,6 +2000,9 @@ function drupal_common_theme() {
     'container' => [
       'render element' => 'element',
     ],
+    'splitbutton' => [
+      'render element' => 'element',
+    ],
     // From field system.
     'field' => [
       'render element' => 'element',
diff --git a/core/lib/Drupal/Core/Entity/EntityListBuilder.php b/core/lib/Drupal/Core/Entity/EntityListBuilder.php
index d07e82c3a8..12cb63e68f 100644
--- a/core/lib/Drupal/Core/Entity/EntityListBuilder.php
+++ b/core/lib/Drupal/Core/Entity/EntityListBuilder.php
@@ -205,7 +205,7 @@ public function buildRow(EntityInterface $entity) {
    */
   public function buildOperations(EntityInterface $entity) {
     $build = [
-      '#type' => 'operations',
+      '#type' => 'splitbutton_operations',
       '#links' => $this->getOperations($entity),
     ];
 
diff --git a/core/lib/Drupal/Core/Render/Element/OperationsSplitbutton.php b/core/lib/Drupal/Core/Render/Element/OperationsSplitbutton.php
new file mode 100644
index 0000000000..f9221d18c7
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Element/OperationsSplitbutton.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Drupal\Core\Render\Element;
+
+/**
+ * Provides am alternative element for entity operations.
+ *
+ * @todo Class comment.
+ *
+ * @see \Drupal\Core\Render\Element\Splitbutton.
+ *
+ * @RenderElement("splitbutton_operations")
+ */
+class OperationsSplitbutton extends Splitbutton {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function preRenderSplitbutton($element) {
+    $element = parent::preRenderSplitbutton($element);
+
+    if (($splitbutton_theme_wrapper_key = array_search('splitbutton', $element['#theme_wrappers'])) !== FALSE) {
+      unset($element['#theme_wrappers'][$splitbutton_theme_wrapper_key]);
+    }
+    array_unshift($element['#theme_wrappers'], 'splitbutton__operations');
+
+    return $element;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Render/Element/Splitbutton.php b/core/lib/Drupal/Core/Render/Element/Splitbutton.php
new file mode 100644
index 0000000000..855b7cb425
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Element/Splitbutton.php
@@ -0,0 +1,179 @@
+<?php
+
+namespace Drupal\Core\Render\Element;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element;
+
+/**
+ * Provides a render element for a set of links rendered as a drop-down button.
+ *
+ * By default, this element sets #theme so that the 'links' theme hook is used
+ * for rendering, with suffixes so that themes can override this specifically
+ * without overriding all links theming. If the #subtype property is provided in
+ * your render array with value 'foo', #theme is set to links__dropbutton__foo;
+ * if not, it's links__dropbutton; both of these can be overridden by setting
+ * the #theme property in your render array. See template_preprocess_links()
+ * for documentation on the other properties used in theming; for instance, use
+ * element property #links to provide $variables['links'] for theming.
+ *
+ * Properties:
+ * - #links: An array of links to actions. See template_preprocess_links() for
+ *   documentation the properties of links in this array.
+ * - #use_aria_menu_role: TRUE (if no buttons).
+ * - #splitbutton_type: A string ot an array of strings defining types of
+ *   splitbutton variant for styling proposes. Renders as class
+ *  `splitbutton--#splitbutton_type`.
+ * - #trigger_label: The label for the trigger button. Defaults to
+ *   'List additional actions'. This label is visible if #main_items is set to
+ *   FALSE.
+ * - #trigger_label_visible: FALSE.
+ * - #main_items: TRUE
+ *
+ * @see \Drupal\Core\Render\Element\Operations
+ *
+ * @RenderElement("splitbutton")
+ */
+class Splitbutton extends RenderElement {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    $class = get_class($this);
+    return [
+      '#process' => [
+        [$class, 'processButtons'],
+      ],
+      '#pre_render' => [
+        [$class, 'massageElements'],
+        [$class, 'preRenderLinksElements'],
+        [$class, 'preRenderSplitbutton'],
+      ],
+    ];
+  }
+
+  /**
+   * Processes button elements.
+   */
+  public static function processButtons(&$element, FormStateInterface $form_state, &$complete_form) {
+    $children = Element::children($element);
+    foreach ($children as $key) {
+      if (
+        !isset($element[$key]['#type']) ||
+        !in_array($element[$key]['#type'], ['submit', 'button'])
+      ) {
+        continue;
+      }
+
+      // If this is a button intentionally allowing incomplete form submission
+      // (e.g., a "Previous" or "Add another item" button), then also skip
+      // client-side validation.
+      if (
+        isset($element[$key]['#limit_validation_errors']) &&
+        $element[$key]['#limit_validation_errors'] !== FALSE
+      ) {
+        $element[$key]['#attributes']['formnovalidate'] = 'formnovalidate';
+      }
+
+      // Add missing id.
+      if (
+        empty($element[$key]['#id']) &&
+        !empty($element[$key]['#name'])
+      ) {
+        $element[$key]['#id'] = $element[$key]['#name'];
+      }
+
+      if (isset($element['#array_parents'])) {
+        $element[$key]['#parents'] = $element['#parents'];
+        $element[$key]['#parents'][] = $key;
+        $element[$key]['#array_parents'] = $element['#array_parents'];
+        $element[$key]['#array_parents'][] = $key;
+        $element[$key] = self::processAjaxForm($element[$key], $form_state, $complete_form);
+
+        $buttons = $form_state->getButtons();
+        $buttons[] = $element[$key];
+        $form_state->setButtons($buttons);
+      }
+    }
+
+    return $element;
+  }
+
+  /**
+   * Pre-renders Splitbutton items.
+   */
+  public static function massageElements($element) {
+    if (isset($element['#links']) && is_array($element['#links'])) {
+      foreach ($element['#links'] as $key => $value) {
+        $element[$key] = $value;
+      }
+    }
+    unset($element['#links']);
+
+    $child_keys = Element::children($element);
+    foreach ($child_keys as $key) {
+      $supported_button_types = ['submit', 'button'];
+      $supported_types = array_merge($supported_button_types, ['link']);
+      $child_has_supported_type = isset($element[$key]['#type']) && in_array($element[$key]['#type'], $supported_types);
+      $child_is_legacy_link = isset($element[$key]['title']);
+      $child_is_button = isset($element[$key]['#type']) && in_array($element[$key]['#type'], $supported_button_types);
+
+      if (!$child_has_supported_type && !$child_is_legacy_link) {
+        unset($element[$key]);
+      }
+
+      if ($child_is_button) {
+        $element['#use_aria_menu_role'] = FALSE;
+      }
+    }
+
+    return $element;
+  }
+
+  /**
+   * Pre-renders Splitbutton link items.
+   */
+  public static function preRenderLinksElements($element) {
+    foreach (Element::children($element) as $key) {
+      if (isset($element[$key]['title'])) {
+        // This was an old-scool and properly used dropbutton list item.
+        $links_item = $element[$key];
+        $links_item += [
+          'ajax' => NULL,
+          'url' => NULL,
+        ];
+
+        $keys = ['title', 'url'];
+        $link_element = [
+          '#type' => 'link',
+          '#title' => $links_item['title'],
+          '#options' => array_diff_key($links_item, array_combine($keys, $keys)),
+          '#url' => $links_item['url'],
+          '#ajax' => $links_item['ajax'],
+        ];
+
+        // Add the item to the list of links.
+        $element[$key] = !empty($links_item['url']) ? $link_element : ['#markup' => $links_item['title']];
+      }
+    }
+
+    return $element;
+  }
+
+  /**
+   * Pre-render callback: Adds splitbutton theme wrapper.
+   */
+  public static function preRenderSplitbutton($element) {
+    $element += [
+      '#theme_wrappers' => [],
+      '#main_items' => TRUE,
+      '#use_aria_menu_role' => TRUE,
+      '#trigger_label_visible' => FALSE,
+    ];
+    array_unshift($element['#theme_wrappers'], 'splitbutton');
+
+    return $element;
+  }
+
+}
diff --git a/core/misc/splitbutton/splitbutton.css b/core/misc/splitbutton/splitbutton.css
new file mode 100644
index 0000000000..411301b33b
--- /dev/null
+++ b/core/misc/splitbutton/splitbutton.css
@@ -0,0 +1,124 @@
+
+/**
+ * @file
+ * Base styles for splitbuttons.
+ */
+
+/**
+ * When a splitbutton has only one item, it is simply a button.
+ */
+.splitbutton {
+  display: inline-block;
+  box-sizing: border-box;
+  max-width: 100%;
+}
+
+.splitbutton--multiple {
+  padding-right: 2em; /* LTR */
+}
+
+[dir="rtl"] .splitbutton--multiple {
+  padding-right: 0;
+  padding-left: 2em;
+}
+
+.js .splitbutton--multiple {
+  position: relative;
+}
+
+.splitbutton__action.splitbutton__action {
+  width: 100%;
+  margin: 0;
+  text-align: left; /* LTR */
+}
+[dir="rtl"] .splitbutton__action.splitbutton__action {
+  text-align: right;
+}
+
+@media screen and (max-width: 600px) {
+  .js .splitbutton {
+    width: 100%;
+  }
+}
+
+.js td .splitbutton-multiple .splitbutton-action a,
+.js td .splitbutton-multiple .splitbutton-action input,
+.js td .splitbutton-multiple .splitbutton-action button {
+  width: auto;
+}
+
+/* UL styles are over-scoped in core, so this selector needs weight parity. */
+.splitbutton__list.splitbutton__list {
+  margin: 0;
+  padding: 0;
+  list-style: none;
+}
+
+.js .splitbutton__list .splitbutton__action {
+  display: none;
+}
+
+.splitbutton__list.open .splitbutton__action {
+  display: block;
+}
+
+/**
+ * The splitbutton styling.
+ *
+ * A splitbutton is a widget that displays a list of action links as a button
+ * with a primary action. Secondary actions are hidden behind a click on a
+ * twisty arrow.
+ *
+ * The arrow is created using border on a zero-width, zero-height span.
+ * The arrow inherits the link color, but can be overridden with border colors.
+ */
+.splitbutton.open {
+  z-index: 100;
+  max-width: none;
+}
+
+.splitbutton__toggle.splitbutton__toggle {
+  position: absolute;
+  top: 0;
+  right: 0; /* LTR */
+  bottom: 0;
+  width: 2em;
+  margin: 0;
+  padding: 0;
+}
+[dir="rtl"] .splitbutton__toggle.splitbutton__toggle {
+  right: auto;
+  left: 0;
+}
+
+.splitbutton__toggle-arrow {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  width: 0;
+  height: 0;
+  transform: translate(-50%, -50%);
+  border-width: 0.3333em 0.3333em 0;
+  border-style: solid;
+  border-right-color: transparent;
+  border-bottom-color: transparent;
+  border-left-color: transparent;
+  line-height: 0;
+}
+
+.splitbutton.open .splitbutton__toggle-arrow {
+  border-top-color: transparent;
+  border-bottom: 0.3333em solid;
+}
+
+.splitbutton .ajax-progress-throbber {
+  position: absolute;
+  top: 100%;
+  left: 0;
+  display: flex;
+  width: auto;
+}
+
+.splitbutton .ajax-progress-throbber .message {
+  flex: 1 0 auto;
+}
diff --git a/core/misc/splitbutton/splitbutton.es6.js b/core/misc/splitbutton/splitbutton.es6.js
new file mode 100644
index 0000000000..181169bcaa
--- /dev/null
+++ b/core/misc/splitbutton/splitbutton.es6.js
@@ -0,0 +1,412 @@
+/**
+ * @file
+ * SplitButton feature.
+ */
+
+(($, Drupal) => {
+  /**
+   * A SplitButton is an HTML list and at least one item as primary action.
+   *
+   * All secondary actions beyond the first in the list are presented in a
+   * dropdown list accessible through a toggle arrow associated with the button.
+   *
+   * @constructor Drupal.SplitButton
+   *
+   * @param {HTMLElement} splitbuttonList
+   *   A DOM element.
+   * @param {object} options
+   *   A list of options including:
+   * @param {string} options.triggerLabel
+   *   The text inside the toggle link element. This text may be hidden
+   *   from visual UAs.
+   */
+  function SplitButton(splitbuttonList, options) {
+    // Merge defaults with settings.
+    const _self = this;
+    const $splitbuttonList = $(splitbuttonList);
+
+    this.expanded = false;
+
+    /**
+     * @type {jQuery}
+     */
+    this.$list = $splitbuttonList;
+
+    /**
+     * @type {jQuery}
+     */
+    this.$splitbutton = $splitbuttonList.closest('.js-splitbutton');
+
+    /**
+     * Secondary actions.
+     *
+     * @type {jQuery}
+     */
+    this.$actions = this.$list.children();
+
+    // Add toggle link.
+    this.$toggle = $(Drupal.theme('splitbuttonToggle', options))
+      .addClass('js-splitbutton-toggle')
+      .attr('aria-haspopup', 'true')
+      .attr('aria-controls', splitbuttonList.id)
+      .attr('id', options.buttonId)
+      .on({
+        click: {
+          data: _self,
+          handler: _self.toggleClickHandler,
+        },
+        keydown: {
+          data: _self,
+          handler: _self.toggleKeyHandler,
+        },
+      });
+
+    this.$list.on('keydown', this, this.listKeyHandler);
+
+    if (
+      this.$splitbutton.find('.js-splitbutton-toggle-placeholder').length === 1
+    ) {
+      this.$splitbutton
+        .find('.js-splitbutton-toggle-placeholder')
+        .replaceWith(this.$toggle);
+    } else {
+      this.$list.before(this.$toggle);
+    }
+
+    // Bind mouse events.
+    this.$splitbutton.on({
+      /**
+       * Adds a timeout to close the dropdown on mouseleave.
+       *
+       * @ignore
+       */
+      'mouseleave.splitbutton': $.proxy(this.hoverOut, this),
+
+      /**
+       * Clears timeout when mouseout of the dropdown.
+       *
+       * @ignore
+       */
+      'mouseenter.splitbutton': $.proxy(this.hoverIn, this),
+
+      /**
+       * Similar to mouseleave/mouseenter, but for keyboard navigation.
+       *
+       * @ignore
+       */
+      'focusout.splitbutton': $.proxy(this.focusOut, this),
+
+      /**
+       * @ignore
+       */
+      'focusin.splitbutton': $.proxy(this.focusIn, this),
+    });
+
+    this.$list.find('.js-splitbutton-action-secondary').attr('tabindex', -1);
+  }
+
+  /**
+   * Process elements with the .js-splitbutton class on page load.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches splitButton behaviors.
+   */
+  Drupal.behaviors.splitButton = {
+    attach(context, settings) {
+      const $splitbuttonLists = $(context)
+        .find('.js-splitbutton-list')
+        .once('splitbutton');
+
+      if ($splitbuttonLists.length) {
+        // Initialize all buttons.
+        const il = $splitbuttonLists.length;
+        for (let i = 0; i < il; i++) {
+          const splitButtonId = $splitbuttonLists[i].id;
+          const options = $.extend(
+            {
+              triggerLabel: settings.splitbutton.triggerLabelDefault,
+              triggerLabelVisible: false,
+            },
+            settings.splitbutton.instances[splitButtonId],
+          );
+
+          SplitButton.splitbuttons.push(
+            new SplitButton($splitbuttonLists[i], options),
+          );
+        }
+      }
+    },
+  };
+
+  /**
+   * Extend the SplitButton constructor.
+   */
+  $.extend(
+    SplitButton,
+    /** @lends Drupal.SplitButton */ {
+      /**
+       * Store all processed SplitButtons.
+       *
+       * @type {Array.<Drupal.SplitButton>}
+       */
+      splitbuttons: [],
+    },
+  );
+
+  /**
+   * Extend the SplitButton prototype.
+   */
+  $.extend(
+    SplitButton.prototype,
+    /** @lends Drupal.SplitButton# */ {
+      /**
+       * Toggle the splitbutton open and closed.
+       *
+       * @param {bool} [show]
+       *   Force the splitbutton to open by passing true or to close by
+       *   passing false.
+       *
+       * @return {Drupal.SplitButton}
+       *   The SplitButton instance.
+       */
+      toggle(show) {
+        const isBool = typeof show === 'boolean';
+        show = isBool ? show : !this.expanded;
+        this.expanded = show;
+        this.$list.toggleClass('open', show);
+        this.$splitbutton.toggleClass('open', show);
+
+        if (show) {
+          this.$toggle.attr('aria-expanded', 'true');
+          this.$list
+            .find('.js-splitbutton-action-secondary')
+            .removeAttr('tabindex');
+        } else {
+          this.$toggle.removeAttr('aria-expanded');
+          this.$list
+            .find('.js-splitbutton-action-secondary')
+            .attr('tabindex', -1);
+        }
+
+        return this;
+      },
+
+      /**
+       * Move action focus to the specified direction.
+       *
+       * @param {string} direction
+       *   Can be 'previous', 'next', 'first' or 'last'.
+       */
+      focusAction(direction) {
+        const $listItemFocused = this.$actions.has(':focus');
+        const $listItemFirst = this.$actions.first();
+        const $listItemLast = this.$actions.last();
+        const $nextlistItem = $listItemFocused.next().length
+          ? $listItemFocused.next()
+          : $listItemLast;
+        const $prevlistItem = $listItemFocused.prev().length
+          ? $listItemFocused.prev()
+          : $listItemFirst;
+
+        this.toggle(true);
+
+        /* eslint-disable default-case */
+        switch (direction) {
+          case 'next':
+            $nextlistItem
+              .find(':focusable')
+              .first()
+              .trigger('focus');
+            break;
+
+          case 'previous':
+            $prevlistItem
+              .find(':focusable')
+              .first()
+              .trigger('focus');
+            break;
+
+          case 'first':
+            $listItemFirst
+              .find(':focusable')
+              .first()
+              .trigger('focus');
+            break;
+
+          case 'last':
+            $listItemLast
+              .find(':focusable')
+              .first()
+              .trigger('focus');
+            break;
+        }
+        /* eslint-enable default-case */
+      },
+
+      /**
+       * Delegated callback for toggling secondary splitbutton actions.
+       *
+       * This is an event handler attached to the toggle button.
+       *
+       * @param {jQuery.Event} event
+       *   The event triggered.
+       * @param {Drupal.SplitButton} event.data
+       *   The actual SplitButton instance.
+       */
+      toggleClickHandler(event) {
+        event.data.toggle();
+        event.preventDefault();
+      },
+
+      /**
+       * Delegated callback for toggling secondary splitbutton actions.
+       *
+       * This is an event handler attached to the toggle button.
+       *
+       * @param {jQuery.Event} event
+       *   The event triggered.
+       * @param {Drupal.SplitButton} event.data
+       *   The actual SplitButton instance.
+       */
+      toggleKeyHandler(event) {
+        /* eslint-disable default-case */
+        switch (event.key) {
+          case 'ArrowUp':
+            event.data.focusAction('last');
+            event.preventDefault();
+            break;
+
+          case ' ':
+          case 'Enter':
+            event.data.toggle();
+            event.preventDefault();
+            break;
+
+          case 'ArrowDown':
+            event.data.focusAction('first');
+            event.preventDefault();
+            break;
+        }
+        /* eslint-enable default-case */
+      },
+
+      /**
+       * Delegated callback for toggling secondary splitbutton actions.
+       *
+       * This is an event handler attached to the toggle button.
+       *
+       * @param {jQuery.Event} event
+       *   The event triggered.
+       * @param {Drupal.SplitButton} event.data
+       *   The actual SplitButton instance.
+       */
+      listKeyHandler(event) {
+        /* eslint-disable default-case */
+        switch (event.key) {
+          case 'ArrowUp':
+            event.data.focusAction('previous');
+            event.preventDefault();
+            break;
+
+          case 'ArrowDown':
+            event.data.focusAction('next');
+            event.preventDefault();
+            break;
+
+          case 'Home':
+            event.data.focusAction('first');
+            event.preventDefault();
+            break;
+
+          case 'End':
+            event.data.focusAction('last');
+            event.preventDefault();
+            break;
+
+          case 'Escape':
+            event.data.toggle(false).$toggle.trigger('focus');
+            event.preventDefault();
+            break;
+        }
+        /* eslint-enable default-case */
+      },
+
+      /**
+       * @method
+       */
+      hoverIn() {
+        // Clear any previous timer we were using.
+        if (this.timerID) {
+          window.clearTimeout(this.timerID);
+        }
+      },
+
+      /**
+       * @method
+       */
+      hoverOut() {
+        // Wait half a second before closing.
+        this.timerID = window.setTimeout($.proxy(this, 'close'), 500);
+      },
+
+      /**
+       * @method
+       */
+      open() {
+        this.toggle(true);
+      },
+
+      /**
+       * @method
+       */
+      close() {
+        this.toggle(false);
+      },
+
+      /**
+       * @param {jQuery.Event} e
+       *   The event triggered.
+       */
+      focusOut(e) {
+        this.hoverOut.call(this, e);
+      },
+
+      /**
+       * @param {jQuery.Event} e
+       *   The event triggered.
+       */
+      focusIn(e) {
+        this.hoverIn.call(this, e);
+      },
+    },
+  );
+
+  $.extend(
+    Drupal.theme,
+    /** @lends Drupal.theme */ {
+      /**
+       * A toggle is an interactive element often bound to a click handler.
+       *
+       * @param {object} options
+       *   Options object.
+       * @param {string} [options.triggerLabel]
+       *   The button text.
+       *
+       * @return {string}
+       *   A string representing a DOM fragment.
+       */
+      splitbuttonToggle(options) {
+        return `<button type="button" class="button splitbutton__toggle"><span class="splitbutton__toggle-label${
+          options.triggerLabelVisible ? `` : ` visually-hidden`
+        }">${
+          options.triggerLabel
+        }</span>&nbsp;<span class="splitbutton__toggle-arrow"></span></button>`;
+      },
+    },
+  );
+
+  // Expose constructor in the public space.
+  Drupal.SplitButton = SplitButton;
+})(jQuery, Drupal);
diff --git a/core/misc/splitbutton/splitbutton.js b/core/misc/splitbutton/splitbutton.js
new file mode 100644
index 0000000000..ddebc906aa
--- /dev/null
+++ b/core/misc/splitbutton/splitbutton.js
@@ -0,0 +1,201 @@
+/**
+* DO NOT EDIT THIS FILE.
+* See the following change record for more information,
+* https://www.drupal.org/node/2815083
+* @preserve
+**/
+
+(function ($, Drupal) {
+  function SplitButton(splitbuttonList, options) {
+    var _self = this;
+    var $splitbuttonList = $(splitbuttonList);
+
+    this.expanded = false;
+
+    this.$list = $splitbuttonList;
+
+    this.$splitbutton = $splitbuttonList.closest('.js-splitbutton');
+
+    this.$actions = this.$list.children();
+
+    this.$toggle = $(Drupal.theme('splitbuttonToggle', options)).addClass('js-splitbutton-toggle').attr('aria-haspopup', 'true').attr('aria-controls', splitbuttonList.id).attr('id', options.buttonId).on({
+      click: {
+        data: _self,
+        handler: _self.toggleClickHandler
+      },
+      keydown: {
+        data: _self,
+        handler: _self.toggleKeyHandler
+      }
+    });
+
+    this.$list.on('keydown', this, this.listKeyHandler);
+
+    if (this.$splitbutton.find('.js-splitbutton-toggle-placeholder').length === 1) {
+      this.$splitbutton.find('.js-splitbutton-toggle-placeholder').replaceWith(this.$toggle);
+    } else {
+      this.$list.before(this.$toggle);
+    }
+
+    this.$splitbutton.on({
+      'mouseleave.splitbutton': $.proxy(this.hoverOut, this),
+
+      'mouseenter.splitbutton': $.proxy(this.hoverIn, this),
+
+      'focusout.splitbutton': $.proxy(this.focusOut, this),
+
+      'focusin.splitbutton': $.proxy(this.focusIn, this)
+    });
+
+    this.$list.find('.js-splitbutton-action-secondary').attr('tabindex', -1);
+  }
+
+  Drupal.behaviors.splitButton = {
+    attach: function attach(context, settings) {
+      var $splitbuttonLists = $(context).find('.js-splitbutton-list').once('splitbutton');
+
+      if ($splitbuttonLists.length) {
+        var il = $splitbuttonLists.length;
+        for (var i = 0; i < il; i++) {
+          var splitButtonId = $splitbuttonLists[i].id;
+          var options = $.extend({
+            triggerLabel: settings.splitbutton.triggerLabelDefault,
+            triggerLabelVisible: false
+          }, settings.splitbutton.instances[splitButtonId]);
+
+          SplitButton.splitbuttons.push(new SplitButton($splitbuttonLists[i], options));
+        }
+      }
+    }
+  };
+
+  $.extend(SplitButton, {
+    splitbuttons: []
+  });
+
+  $.extend(SplitButton.prototype, {
+    toggle: function toggle(show) {
+      var isBool = typeof show === 'boolean';
+      show = isBool ? show : !this.expanded;
+      this.expanded = show;
+      this.$list.toggleClass('open', show);
+      this.$splitbutton.toggleClass('open', show);
+
+      if (show) {
+        this.$toggle.attr('aria-expanded', 'true');
+        this.$list.find('.js-splitbutton-action-secondary').removeAttr('tabindex');
+      } else {
+        this.$toggle.removeAttr('aria-expanded');
+        this.$list.find('.js-splitbutton-action-secondary').attr('tabindex', -1);
+      }
+
+      return this;
+    },
+    focusAction: function focusAction(direction) {
+      var $listItemFocused = this.$actions.has(':focus');
+      var $listItemFirst = this.$actions.first();
+      var $listItemLast = this.$actions.last();
+      var $nextlistItem = $listItemFocused.next().length ? $listItemFocused.next() : $listItemLast;
+      var $prevlistItem = $listItemFocused.prev().length ? $listItemFocused.prev() : $listItemFirst;
+
+      this.toggle(true);
+
+      switch (direction) {
+        case 'next':
+          $nextlistItem.find(':focusable').first().trigger('focus');
+          break;
+
+        case 'previous':
+          $prevlistItem.find(':focusable').first().trigger('focus');
+          break;
+
+        case 'first':
+          $listItemFirst.find(':focusable').first().trigger('focus');
+          break;
+
+        case 'last':
+          $listItemLast.find(':focusable').first().trigger('focus');
+          break;
+      }
+    },
+    toggleClickHandler: function toggleClickHandler(event) {
+      event.data.toggle();
+      event.preventDefault();
+    },
+    toggleKeyHandler: function toggleKeyHandler(event) {
+      switch (event.key) {
+        case 'ArrowUp':
+          event.data.focusAction('last');
+          event.preventDefault();
+          break;
+
+        case ' ':
+        case 'Enter':
+          event.data.toggle();
+          event.preventDefault();
+          break;
+
+        case 'ArrowDown':
+          event.data.focusAction('first');
+          event.preventDefault();
+          break;
+      }
+    },
+    listKeyHandler: function listKeyHandler(event) {
+      switch (event.key) {
+        case 'ArrowUp':
+          event.data.focusAction('previous');
+          event.preventDefault();
+          break;
+
+        case 'ArrowDown':
+          event.data.focusAction('next');
+          event.preventDefault();
+          break;
+
+        case 'Home':
+          event.data.focusAction('first');
+          event.preventDefault();
+          break;
+
+        case 'End':
+          event.data.focusAction('last');
+          event.preventDefault();
+          break;
+
+        case 'Escape':
+          event.data.toggle(false).$toggle.trigger('focus');
+          event.preventDefault();
+          break;
+      }
+    },
+    hoverIn: function hoverIn() {
+      if (this.timerID) {
+        window.clearTimeout(this.timerID);
+      }
+    },
+    hoverOut: function hoverOut() {
+      this.timerID = window.setTimeout($.proxy(this, 'close'), 500);
+    },
+    open: function open() {
+      this.toggle(true);
+    },
+    close: function close() {
+      this.toggle(false);
+    },
+    focusOut: function focusOut(e) {
+      this.hoverOut.call(this, e);
+    },
+    focusIn: function focusIn(e) {
+      this.hoverIn.call(this, e);
+    }
+  });
+
+  $.extend(Drupal.theme, {
+    splitbuttonToggle: function splitbuttonToggle(options) {
+      return '<button type="button" class="button splitbutton__toggle"><span class="splitbutton__toggle-label' + (options.triggerLabelVisible ? '' : ' visually-hidden') + '">' + options.triggerLabel + '</span>&nbsp;<span class="splitbutton__toggle-arrow"></span></button>';
+    }
+  });
+
+  Drupal.SplitButton = SplitButton;
+})(jQuery, Drupal);
\ No newline at end of file
diff --git a/core/modules/aggregator/src/Controller/AggregatorController.php b/core/modules/aggregator/src/Controller/AggregatorController.php
index 5beb15441c..45b18b4615 100644
--- a/core/modules/aggregator/src/Controller/AggregatorController.php
+++ b/core/modules/aggregator/src/Controller/AggregatorController.php
@@ -149,7 +149,7 @@ public function adminOverview() {
       ];
       $row[] = [
         'data' => [
-          '#type' => 'operations',
+          '#type' => 'splitbutton_operations',
           '#links' => $links,
         ],
       ];
diff --git a/core/modules/ban/src/Form/BanAdmin.php b/core/modules/ban/src/Form/BanAdmin.php
index d71be8d402..038b447488 100644
--- a/core/modules/ban/src/Form/BanAdmin.php
+++ b/core/modules/ban/src/Form/BanAdmin.php
@@ -67,7 +67,7 @@ public function buildForm(array $form, FormStateInterface $form_state, $default_
       ];
       $row[] = [
         'data' => [
-          '#type' => 'operations',
+          '#type' => 'splitbutton_operations',
           '#links' => $links,
         ],
       ];
diff --git a/core/modules/block/src/Controller/BlockLibraryController.php b/core/modules/block/src/Controller/BlockLibraryController.php
index cf5f20a0ba..720467d9db 100644
--- a/core/modules/block/src/Controller/BlockLibraryController.php
+++ b/core/modules/block/src/Controller/BlockLibraryController.php
@@ -145,7 +145,7 @@ public function listBlocks(Request $request, $theme) {
         $links['add']['query']['weight'] = $weight;
       }
       $row['operations']['data'] = [
-        '#type' => 'operations',
+        '#type' => 'splitbutton_operations',
         '#links' => $links,
       ];
       $rows[] = $row;
diff --git a/core/modules/book/src/Controller/BookController.php b/core/modules/book/src/Controller/BookController.php
index bb09ae9719..6e88735c0a 100644
--- a/core/modules/book/src/Controller/BookController.php
+++ b/core/modules/book/src/Controller/BookController.php
@@ -94,7 +94,7 @@ public function adminOverview() {
       ];
       $row[] = [
         'data' => [
-          '#type' => 'operations',
+          '#type' => 'splitbutton_operations',
           '#links' => $links,
         ],
       ];
diff --git a/core/modules/book/src/Form/BookAdminEditForm.php b/core/modules/book/src/Form/BookAdminEditForm.php
index d5d29c1cc8..2053638461 100644
--- a/core/modules/book/src/Form/BookAdminEditForm.php
+++ b/core/modules/book/src/Form/BookAdminEditForm.php
@@ -268,7 +268,7 @@ protected function bookAdminTableTree(array $tree, array &$form) {
       ];
 
       $form[$id]['operations'] = [
-        '#type' => 'operations',
+        '#type' => 'splitbutton_operations',
       ];
       $form[$id]['operations']['#links']['view'] = [
         'title' => $this->t('View'),
diff --git a/core/modules/book/tests/src/Functional/BookTest.php b/core/modules/book/tests/src/Functional/BookTest.php
index e1e81955d2..9039da8be1 100644
--- a/core/modules/book/tests/src/Functional/BookTest.php
+++ b/core/modules/book/tests/src/Functional/BookTest.php
@@ -510,7 +510,7 @@ public function testAdminBookNodeListing() {
     $this->drupalGet('admin/structure/book/' . $this->book->id());
     $this->assertText($this->book->label(), 'The book title is displayed on the administrative book listing page.');
 
-    $elements = $this->xpath('//table//ul[@class="dropbutton"]/li/a');
+    $elements = $this->xpath("//table///div[contains(concat(' ', normalize-space(@class), ' '), ' splitbutton ')]/a");
     $this->assertEqual($elements[0]->getText(), 'View', 'View link is found from the list.');
   }
 
diff --git a/core/modules/comment/src/Form/CommentAdminOverview.php b/core/modules/comment/src/Form/CommentAdminOverview.php
index 42c76e485b..8d2e386618 100644
--- a/core/modules/comment/src/Form/CommentAdminOverview.php
+++ b/core/modules/comment/src/Form/CommentAdminOverview.php
@@ -233,7 +233,7 @@ public function buildForm(array $form, FormStateInterface $form_state, $type = '
         ];
       }
       $options[$comment->id()]['operations']['data'] = [
-        '#type' => 'operations',
+        '#type' => 'splitbutton_operations',
         '#links' => $links,
       ];
     }
diff --git a/core/modules/comment/tests/src/Functional/Views/CommentOperationsTest.php b/core/modules/comment/tests/src/Functional/Views/CommentOperationsTest.php
index ecd15d2a97..fce9d455fb 100644
--- a/core/modules/comment/tests/src/Functional/Views/CommentOperationsTest.php
+++ b/core/modules/comment/tests/src/Functional/Views/CommentOperationsTest.php
@@ -29,9 +29,9 @@ public function testCommentOperations() {
     $this->drupalLogin($admin_account);
     $this->drupalGet('test-comment-operations');
     $this->assertResponse(200);
-    $operation = $this->cssSelect('.views-field-operations li.edit a');
+    $operation = $this->cssSelect('.views-field-operations .splitbutton a[href*="/edit"]');
     $this->assertEqual(count($operation), 1, 'Found edit operation for comment.');
-    $operation = $this->cssSelect('.views-field-operations li.delete a');
+    $operation = $this->cssSelect('.views-field-operations .splitbutton a[href*="/delete"]');
     $this->assertEqual(count($operation), 1, 'Found delete operation for comment.');
   }
 
diff --git a/core/modules/config/src/Form/ConfigSync.php b/core/modules/config/src/Form/ConfigSync.php
index 71fb3681f1..dcdbba8109 100644
--- a/core/modules/config/src/Form/ConfigSync.php
+++ b/core/modules/config/src/Form/ConfigSync.php
@@ -333,7 +333,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
             'name' => $config_name,
             'operations' => [
               'data' => [
-                '#type' => 'operations',
+                '#type' => 'splitbutton_operations',
                 '#links' => $links,
               ],
             ],
diff --git a/core/modules/config_translation/src/Controller/ConfigTranslationController.php b/core/modules/config_translation/src/Controller/ConfigTranslationController.php
index 8c9dc480e8..67dab7ad9a 100644
--- a/core/modules/config_translation/src/Controller/ConfigTranslationController.php
+++ b/core/modules/config_translation/src/Controller/ConfigTranslationController.php
@@ -243,7 +243,7 @@ public function itemPage(Request $request, RouteMatchInterface $route_match, $pl
       ];
 
       $page['languages'][$langcode]['operations'] = [
-        '#type' => 'operations',
+        '#type' => 'splitbutton_operations',
         '#links' => $operations,
         // Even if the mapper contains multiple language codes, the source
         // configuration can still be edited.
diff --git a/core/modules/config_translation/src/Controller/ConfigTranslationMapperList.php b/core/modules/config_translation/src/Controller/ConfigTranslationMapperList.php
index 4e510aaf38..1ca29b83db 100644
--- a/core/modules/config_translation/src/Controller/ConfigTranslationMapperList.php
+++ b/core/modules/config_translation/src/Controller/ConfigTranslationMapperList.php
@@ -121,7 +121,7 @@ protected function buildOperations(ConfigMapperInterface $mapper) {
     $operations = $mapper->getOperations();
     uasort($operations, 'Drupal\Component\Utility\SortArray::sortByWeightElement');
     $build = [
-      '#type' => 'operations',
+      '#type' => 'splitbutton_operations',
       '#links' => $operations,
     ];
     return $build;
diff --git a/core/modules/content_moderation/src/Form/ContentModerationConfigureForm.php b/core/modules/content_moderation/src/Form/ContentModerationConfigureForm.php
index f43451ec63..62013865b9 100644
--- a/core/modules/content_moderation/src/Form/ContentModerationConfigureForm.php
+++ b/core/modules/content_moderation/src/Form/ContentModerationConfigureForm.php
@@ -112,7 +112,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
           ],
         ],
         'operations' => [
-          '#type' => 'operations',
+          '#type' => 'splitbutton_operations',
           '#links' => [
             'select' => [
               'title' => $this->t('Select'),
diff --git a/core/modules/content_translation/src/Controller/ContentTranslationController.php b/core/modules/content_translation/src/Controller/ContentTranslationController.php
index 5d48cc0533..ed1d12810c 100644
--- a/core/modules/content_translation/src/Controller/ContentTranslationController.php
+++ b/core/modules/content_translation/src/Controller/ContentTranslationController.php
@@ -176,7 +176,7 @@ public function overview(RouteMatchInterface $route_match, $entity_type_id = NUL
           ->setRouteParameter('language', $language->getId());
         $operations = [
           'data' => [
-            '#type' => 'operations',
+            '#type' => 'splitbutton_operations',
             '#links' => [],
           ],
         ];
diff --git a/core/modules/content_translation/tests/src/Functional/ContentTranslationUITestBase.php b/core/modules/content_translation/tests/src/Functional/ContentTranslationUITestBase.php
index 5e7954086f..9617128e76 100644
--- a/core/modules/content_translation/tests/src/Functional/ContentTranslationUITestBase.php
+++ b/core/modules/content_translation/tests/src/Functional/ContentTranslationUITestBase.php
@@ -226,7 +226,7 @@ protected function doTestTranslationOverview() {
         $elements = $this->xpath('//table//a[@href=:href]', [':href' => $view_url]);
         $this->assertEqual($elements[0]->getText(), $entity->getTranslation($langcode)->label(), new FormattableMarkup('Label correctly shown for %language translation.', ['%language' => $langcode]));
         $edit_path = $entity->toUrl('edit-form', ['language' => $language])->toString();
-        $elements = $this->xpath('//table//ul[@class="dropbutton"]/li/a[@href=:href]', [':href' => $edit_path]);
+        $elements = $this->xpath("//table//div[contains(concat(' ', normalize-space(@class), ' '), ' splitbutton ')]//a[@href=:href]", [':href' => $edit_path]);
         $this->assertEqual($elements[0]->getText(), t('Edit'), new FormattableMarkup('Edit link correct for %language translation.', ['%language' => $langcode]));
       }
     }
diff --git a/core/modules/field_ui/tests/src/Functional/ManageFieldsFunctionalTest.php b/core/modules/field_ui/tests/src/Functional/ManageFieldsFunctionalTest.php
index c9dcd3490c..1fbd593ebd 100644
--- a/core/modules/field_ui/tests/src/Functional/ManageFieldsFunctionalTest.php
+++ b/core/modules/field_ui/tests/src/Functional/ManageFieldsFunctionalTest.php
@@ -156,7 +156,7 @@ public function manageFieldsPage($type = '') {
     // Assert entity operations for all fields.
     $number_of_links = 3;
     $number_of_links_found = 0;
-    $operation_links = $this->xpath('//ul[@class = "dropbutton"]/li/a');
+    $operation_links = $this->xpath("//div[contains(concat(' ', normalize-space(@class), ' '), ' splitbutton ')]//a");
     $url = base_path() . "admin/structure/types/manage/$type/fields/node.$type.body";
 
     foreach ($operation_links as $link) {
diff --git a/core/modules/forum/src/Form/Overview.php b/core/modules/forum/src/Form/Overview.php
index 2158170da8..d3e557b6aa 100644
--- a/core/modules/forum/src/Form/Overview.php
+++ b/core/modules/forum/src/Form/Overview.php
@@ -54,7 +54,7 @@ public function buildForm(array $form, FormStateInterface $form_state, Vocabular
 
         // Re-create the operations column and add only the edit link.
         $form['terms'][$key]['operations'] = [
-          '#type' => 'operations',
+          '#type' => 'splitbutton_operations',
           '#links' => [
             'edit' => [
               'title' => $title,
diff --git a/core/modules/image/src/Form/ImageStyleEditForm.php b/core/modules/image/src/Form/ImageStyleEditForm.php
index f33c20b870..07397b1a3a 100644
--- a/core/modules/image/src/Form/ImageStyleEditForm.php
+++ b/core/modules/image/src/Form/ImageStyleEditForm.php
@@ -137,7 +137,7 @@ public function form(array $form, FormStateInterface $form_state) {
         ]),
       ];
       $form['effects'][$key]['operations'] = [
-        '#type' => 'operations',
+        '#type' => 'splitbutton_operations',
         '#links' => $links,
       ];
     }
diff --git a/core/modules/language/src/Form/NegotiationBrowserForm.php b/core/modules/language/src/Form/NegotiationBrowserForm.php
index 1ffd1a2c5c..df3ecf6fdd 100644
--- a/core/modules/language/src/Form/NegotiationBrowserForm.php
+++ b/core/modules/language/src/Form/NegotiationBrowserForm.php
@@ -115,7 +115,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       ];
       // Operations column.
       $form['mappings'][$browser_langcode]['operations'] = [
-        '#type' => 'operations',
+        '#type' => 'splitbutton_operations',
         '#links' => [],
       ];
       $form['mappings'][$browser_langcode]['operations']['#links']['delete'] = [
diff --git a/core/modules/language/src/Form/NegotiationConfigureForm.php b/core/modules/language/src/Form/NegotiationConfigureForm.php
index 0679a563d2..323d643991 100644
--- a/core/modules/language/src/Form/NegotiationConfigureForm.php
+++ b/core/modules/language/src/Form/NegotiationConfigureForm.php
@@ -308,7 +308,7 @@ protected function configureFormTable(array &$form, $type) {
           $table_form['#show_operations'] = TRUE;
         }
         $table_form['operation'][$method_id] = [
-         '#type' => 'operations',
+         '#type' => 'splitbutton_operations',
          '#links' => $config_op,
         ];
       }
diff --git a/core/modules/media/tests/src/Functional/MediaOverviewPageTest.php b/core/modules/media/tests/src/Functional/MediaOverviewPageTest.php
index d4f82eaea0..1b220151b7 100644
--- a/core/modules/media/tests/src/Functional/MediaOverviewPageTest.php
+++ b/core/modules/media/tests/src/Functional/MediaOverviewPageTest.php
@@ -133,10 +133,10 @@ public function testMediaOverviewPage() {
     $this->assertSame($expected, $changed_element1->getText());
 
     // Operations.
-    $edit_link1 = $assert_session->elementExists('css', 'td.views-field-operations li.edit a', $row1);
+    $edit_link1 = $assert_session->elementExists('css', 'td.views-field-operations .splitbutton a[href*="/edit"]', $row1);
     $this->assertSame('Edit', $edit_link1->getText());
     $assert_session->linkByHrefExists('/media/' . $media1->id() . '/edit');
-    $delete_link1 = $assert_session->elementExists('css', 'td.views-field-operations li.delete a', $row1);
+    $delete_link1 = $assert_session->elementExists('css', 'td.views-field-operations .splitbutton a[href*="/delete"]', $row1);
     $this->assertSame('Delete', $delete_link1->getText());
     $assert_session->linkByHrefExists('/media/' . $media1->id() . '/delete');
 
diff --git a/core/modules/menu_ui/src/MenuForm.php b/core/modules/menu_ui/src/MenuForm.php
index ccedad3133..7a0b77db89 100644
--- a/core/modules/menu_ui/src/MenuForm.php
+++ b/core/modules/menu_ui/src/MenuForm.php
@@ -469,7 +469,7 @@ protected function buildOverviewTreeForm($tree, $delta) {
           ];
         }
         $form[$id]['operations'] = [
-          '#type' => 'operations',
+          '#type' => 'splitbutton_operations',
           '#links' => $operations,
         ];
       }
diff --git a/core/modules/node/src/Controller/NodeController.php b/core/modules/node/src/Controller/NodeController.php
index 385d2bc5ac..25740efbfa 100644
--- a/core/modules/node/src/Controller/NodeController.php
+++ b/core/modules/node/src/Controller/NodeController.php
@@ -274,7 +274,7 @@ public function revisionOverview(NodeInterface $node) {
 
           $row[] = [
             'data' => [
-              '#type' => 'operations',
+              '#type' => 'splitbutton_operations',
               '#links' => $links,
             ],
           ];
diff --git a/core/modules/shortcut/src/Form/SetCustomize.php b/core/modules/shortcut/src/Form/SetCustomize.php
index 6f86a0e2a0..3155c27aa3 100644
--- a/core/modules/shortcut/src/Form/SetCustomize.php
+++ b/core/modules/shortcut/src/Form/SetCustomize.php
@@ -75,7 +75,7 @@ public function form(array $form, FormStateInterface $form_state) {
         'url' => $shortcut->toUrl('delete-form'),
       ];
       $form['shortcuts']['links'][$id]['operations'] = [
-        '#type' => 'operations',
+        '#type' => 'splitbutton_operations',
         '#links' => $links,
         '#access' => $url->access(),
       ];
diff --git a/core/modules/system/templates/splitbutton.html.twig b/core/modules/system/templates/splitbutton.html.twig
new file mode 100644
index 0000000000..4788bc77a1
--- /dev/null
+++ b/core/modules/system/templates/splitbutton.html.twig
@@ -0,0 +1,41 @@
+{#
+/**
+ * @file
+ * Default theme implementation of a splitbutton used to wrap child elements.
+ *
+ * Used for grouped buttons and link items.
+ *
+ * Available variables:
+ * - attributes: HTML attributes for the containing element.
+ * - content_attributes: HTML attributes for the list element.
+ * - item_attributes: HTML attributes for the list item.
+ * - main_items: The uncollapsed splitbutton elements.
+ * - items: Further child elements of the splitbutton.
+ * - multiple: Whether the splitbutton has more than one item.
+ *
+ * @see template_preprocess_splitbutton()
+ */
+#}
+{% if main_items or items %}
+{%
+  set classes = [
+    'splitbutton',
+    multiple ? 'splitbutton--multiple' : 'splitbutton--single'
+  ]
+%}
+<div{{ attributes.addClass(classes) }}>
+  <div class="splitbutton__main-items">
+  {% for main_item in main_items %}
+    {{ main_item.value }}
+  {% endfor %}
+  </div>
+
+  {% if items %}
+  <ul{{ content_attributes.addClass('splitbutton__list') }}>
+    {% for item in items %}
+      <li{{ item.attributes.addClass('splitbutton__list-item') }}>{{ item.value }}</li>
+    {% endfor %}
+  </ul>
+  {% endif %}
+</div>
+{% endif %}
diff --git a/core/modules/taxonomy/src/Form/OverviewTerms.php b/core/modules/taxonomy/src/Form/OverviewTerms.php
index 502900b87d..a249273a7f 100644
--- a/core/modules/taxonomy/src/Form/OverviewTerms.php
+++ b/core/modules/taxonomy/src/Form/OverviewTerms.php
@@ -420,7 +420,7 @@ public function buildForm(array $form, FormStateInterface $form_state, Vocabular
 
       if ($operations = $this->termListBuilder->getOperations($term)) {
         $form['terms'][$key]['operations'] = [
-          '#type' => 'operations',
+          '#type' => 'splitbutton_operations',
           '#links' => $operations,
         ];
       }
diff --git a/core/modules/views/src/EntityViewsData.php b/core/modules/views/src/EntityViewsData.php
index f8d8ed6301..53d7ecb092 100644
--- a/core/modules/views/src/EntityViewsData.php
+++ b/core/modules/views/src/EntityViewsData.php
@@ -217,6 +217,27 @@ public function getViewsData() {
       }
     }
 
+    // Entity types must implement a list_builder in order to use Views'
+    // entity splitbutton operations field.
+    if ($this->entityType->hasListBuilderClass()) {
+      $data[$base_table]['splitbutton_operations'] = [
+        'field' => [
+          'title' => $this->t('Splitbutton operations links'),
+          'help' => $this->t('Provides splitbutton to perform entity operations.'),
+          'id' => 'entity_splitbutton_operations',
+        ],
+      ];
+      if ($revision_table) {
+        $data[$revision_table]['splitbutton_operations'] = [
+          'field' => [
+            'title' => $this->t('Splitbutton operation links'),
+            'help' => $this->t('Provides splitbutton to perform entity operations.'),
+            'id' => 'entity_splitbutton_operations',
+          ],
+        ];
+      }
+    }
+
     if ($this->entityType->hasViewBuilderClass()) {
       $data[$base_table]['rendered_entity'] = [
         'field' => [
diff --git a/core/modules/views/src/Plugin/views/field/EntitySplitbuttonOperations.php b/core/modules/views/src/Plugin/views/field/EntitySplitbuttonOperations.php
new file mode 100644
index 0000000000..ee9b699ae9
--- /dev/null
+++ b/core/modules/views/src/Plugin/views/field/EntitySplitbuttonOperations.php
@@ -0,0 +1,222 @@
+<?php
+
+namespace Drupal\views\Plugin\views\field;
+
+use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;
+use Drupal\Core\Entity\EntityRepositoryInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\Routing\RedirectDestinationTrait;
+use Drupal\views\Entity\Render\EntityTranslationRenderTrait;
+use Drupal\views\ResultRow;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Renders all operations links for an entity.
+ *
+ * @ingroup views_field_handlers
+ *
+ * @ViewsField("entity_splitbutton_operations")
+ */
+class EntitySplitbuttonOperations extends FieldPluginBase {
+
+  use EntityTranslationRenderTrait;
+  use RedirectDestinationTrait;
+  use DeprecatedServicePropertyTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $deprecatedProperties = ['entityManager' => 'entity.manager'];
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The entity repository service.
+   *
+   * @var \Drupal\Core\Entity\EntityRepositoryInterface
+   */
+  protected $entityRepository;
+
+  /**
+   * The entity display repository.
+   *
+   * @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface
+   */
+  protected $entityDisplayRepository;
+
+  /**
+   * The language manager.
+   *
+   * @var \Drupal\Core\Language\LanguageManagerInterface
+   */
+  protected $languageManager;
+
+  /**
+   * Constructs a new EntityOperations object.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param array $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity manager.
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The language manager.
+   * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
+   *   The entity repository.
+   */
+  public function __construct(array $configuration, $plugin_id, array $plugin_definition, EntityTypeManagerInterface $entity_type_manager, LanguageManagerInterface $language_manager, EntityRepositoryInterface $entity_repository = NULL) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    $this->entityTypeManager = $entity_type_manager;
+    $this->languageManager = $language_manager;
+
+    if (!$entity_repository) {
+      @trigger_error('Calling EntityOperations::__construct() with the $entity_repository argument is supported in drupal:8.7.0 and will be required before drupal:9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED);
+      $entity_repository = \Drupal::service('entity.repository');
+    }
+    $this->entityRepository = $entity_repository;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('entity_type.manager'),
+      $container->get('language_manager'),
+      $container->get('entity.repository')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function usesGroupBy() {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defineOptions() {
+    $options = parent::defineOptions();
+
+    $options['destination'] = [
+      'default' => FALSE,
+    ];
+
+    return $options;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
+    parent::buildOptionsForm($form, $form_state);
+
+    $form['destination'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Include destination'),
+      '#description' => $this->t('Enforce a <code>destination</code> parameter in the link to return the user to the original view upon completing the link action. Most operations include a destination by default and this setting is no longer needed.'),
+      '#default_value' => $this->options['destination'],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render(ResultRow $values) {
+    $entity = $this->getEntityTranslation($this->getEntity($values), $values);
+    $operations = $this->entityTypeManager->getListBuilder($entity->getEntityTypeId())->getOperations($entity);
+    if ($this->options['destination']) {
+      foreach ($operations as &$operation) {
+        if (!isset($operation['query'])) {
+          $operation['query'] = [];
+        }
+        $operation['query'] += $this->getDestinationArray();
+      }
+    }
+    $build = $operations + [
+      '#type' => 'splitbutton_operations',
+    ];
+
+    return $build;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function query() {
+    // We purposefully do not call parent::query() because we do not want the
+    // default query behavior for Views fields. Instead, let the entity
+    // translation renderer provide the correct query behavior.
+    if ($this->languageManager->isMultilingual()) {
+      $this->getEntityTranslationRenderer()->query($this->query, $this->relationship);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEntityTypeId() {
+    return $this->getEntityType();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEntityManager() {
+    // This relies on DeprecatedServicePropertyTrait to trigger a deprecation
+    // message in case it is accessed.
+    return $this->entityManager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEntityTypeManager() {
+    return $this->entityTypeManager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEntityRepository() {
+    return $this->entityRepository;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getLanguageManager() {
+    return $this->languageManager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getView() {
+    return $this->view;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clickSortable() {
+    return FALSE;
+  }
+
+}
diff --git a/core/modules/views/src/Plugin/views/field/Splitbutton.php b/core/modules/views/src/Plugin/views/field/Splitbutton.php
new file mode 100644
index 0000000000..81188f5413
--- /dev/null
+++ b/core/modules/views/src/Plugin/views/field/Splitbutton.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Drupal\views\Plugin\views\field;
+
+use Drupal\views\ResultRow;
+
+/**
+ * Provides a handler that renders links as splitbutton.
+ *
+ * @ingroup views_field_handlers
+ *
+ * @ViewsField("splitbutton")
+ */
+class Splitbutton extends Links {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render(ResultRow $values) {
+    $links = $this->getLinks();
+
+    if (!empty($links)) {
+      return $links + [
+        '#type' => 'splitbutton',
+      ];
+    }
+    else {
+      return '';
+    }
+  }
+
+}
diff --git a/core/modules/views/tests/src/Unit/Plugin/views/field/EntityOperationsUnitTest.php b/core/modules/views/tests/src/Unit/Plugin/views/field/EntityOperationsUnitTest.php
index 5f07966475..e73672c9b8 100644
--- a/core/modules/views/tests/src/Unit/Plugin/views/field/EntityOperationsUnitTest.php
+++ b/core/modules/views/tests/src/Unit/Plugin/views/field/EntityOperationsUnitTest.php
@@ -125,7 +125,7 @@ public function testRenderWithDestination() {
     $result->_entity = $entity;
 
     $expected_build = [
-      '#type' => 'operations',
+      '#type' => 'splitbutton_operations',
       '#links' => $operations,
     ];
     $expected_build['#links']['foo']['query'] = ['destination' => 'foobar'];
@@ -167,7 +167,7 @@ public function testRenderWithoutDestination() {
     $result->_entity = $entity;
 
     $expected_build = [
-      '#type' => 'operations',
+      '#type' => 'splitbutton_operations',
       '#links' => $operations,
     ];
     $build = $this->plugin->render($result);
diff --git a/core/modules/views_ui/js/views-admin.es6.js b/core/modules/views_ui/js/views-admin.es6.js
index 289cfea15e..3f3c979054 100644
--- a/core/modules/views_ui/js/views-admin.es6.js
+++ b/core/modules/views_ui/js/views-admin.es6.js
@@ -368,31 +368,31 @@
         return;
       }
 
-      const $addDisplayDropdown = $(
-        `<li class="add"><a href="#"><span class="icon add"></span>${Drupal.t(
-          'Add',
-        )}</a><ul class="action-list" style="display:none;"></ul></li>`,
-      );
-      const $displayButtons = $menu.nextAll('input.add-display').detach();
-      $displayButtons
-        .appendTo($addDisplayDropdown.find('.action-list'))
-        .wrap('<li>')
-        .parent()
-        .eq(0)
-        .addClass('first')
-        .end()
-        .eq(-1)
-        .addClass('last');
-      // Remove the 'Add ' prefix from the button labels since they're being
-      // placed in an 'Add' dropdown. @todo This assumes English, but so does
-      // $addDisplayDropdown above. Add support for translation.
-      $displayButtons.each(function() {
-        const label = $(this).val();
-        if (label.substr(0, 4) === 'Add ') {
-          $(this).val(label.substr(4));
-        }
-      });
-      $addDisplayDropdown.appendTo($menu);
+      // const $addDisplayDropdown = $(
+      //   `<li class="add"><a href="#"><span class="icon add"></span>${Drupal.t(
+      //     'Add',
+      //   )}</a><ul class="action-list" style="display:none;"></ul></li>`,
+      // );
+      // const $displayButtons = $menu.nextAll('input.add-display').detach();
+      // $displayButtons
+      //   .appendTo($addDisplayDropdown.find('.action-list'))
+      //   .wrap('<li>')
+      //   .parent()
+      //   .eq(0)
+      //   .addClass('first')
+      //   .end()
+      //   .eq(-1)
+      //   .addClass('last');
+      // // Remove the 'Add ' prefix from the button labels since they're being
+      // // placed in an 'Add' dropdown. @todo This assumes English, but so does
+      // // $addDisplayDropdown above. Add support for translation.
+      // $displayButtons.each(function() {
+      //   const label = $(this).val();
+      //   if (label.substr(0, 4) === 'Add ') {
+      //     $(this).val(label.substr(4));
+      //   }
+      // });
+      // $addDisplayDropdown.appendTo($menu);
 
       // Add the click handler for the add display button.
       $menu.find('li.add > a').on('click', function(event) {
diff --git a/core/modules/views_ui/js/views-admin.js b/core/modules/views_ui/js/views-admin.js
index 5cc0ef29d3..066db4119c 100644
--- a/core/modules/views_ui/js/views-admin.js
+++ b/core/modules/views_ui/js/views-admin.js
@@ -171,17 +171,17 @@
         return;
       }
 
-      var $addDisplayDropdown = $('<li class="add"><a href="#"><span class="icon add"></span>' + Drupal.t('Add') + '</a><ul class="action-list" style="display:none;"></ul></li>');
-      var $displayButtons = $menu.nextAll('input.add-display').detach();
-      $displayButtons.appendTo($addDisplayDropdown.find('.action-list')).wrap('<li>').parent().eq(0).addClass('first').end().eq(-1).addClass('last');
-
-      $displayButtons.each(function () {
-        var label = $(this).val();
-        if (label.substr(0, 4) === 'Add ') {
-          $(this).val(label.substr(4));
-        }
-      });
-      $addDisplayDropdown.appendTo($menu);
+      // var $addDisplayDropdown = $('<li class="add"><a href="#"><span class="icon add"></span>' + Drupal.t('Add') + '</a><ul class="action-list" style="display:none;"></ul></li>');
+      // var $displayButtons = $menu.nextAll('input.add-display').detach();
+      // $displayButtons.appendTo($addDisplayDropdown.find('.action-list')).wrap('<li>').parent().eq(0).addClass('first').end().eq(-1).addClass('last');
+      //
+      // $displayButtons.each(function () {
+      //   var label = $(this).val();
+      //   if (label.substr(0, 4) === 'Add ') {
+      //     $(this).val(label.substr(4));
+      //   }
+      // });
+      // $addDisplayDropdown.appendTo($menu);
 
       $menu.find('li.add > a').on('click', function (event) {
         event.preventDefault();
@@ -628,4 +628,4 @@
       });
     }
   };
-})(jQuery, Drupal, drupalSettings);
\ No newline at end of file
+})(jQuery, Drupal, drupalSettings);
diff --git a/core/modules/views_ui/src/ViewEditForm.php b/core/modules/views_ui/src/ViewEditForm.php
index 8ca5612dbb..237485ea7d 100644
--- a/core/modules/views_ui/src/ViewEditForm.php
+++ b/core/modules/views_ui/src/ViewEditForm.php
@@ -402,13 +402,9 @@ public function getDisplayDetails($view, $display) {
 
       // The Delete, Duplicate and Undo Delete buttons.
       $build['top']['actions'] = [
-        '#theme_wrappers' => ['dropbutton_wrapper'],
+        '#type' => 'splitbutton',
+        '#splitbutton_type' => ['extrasmall'],
       ];
-
-      // Because some of the 'links' are actually submit buttons, we have to
-      // manually wrap each item in <li> and the whole list in <ul>.
-      $build['top']['actions']['prefix']['#markup'] = '<ul class="dropbutton">';
-
       if (!$is_display_deleted) {
         if (!$is_enabled) {
           $build['top']['actions']['enable'] = [
@@ -416,8 +412,6 @@ public function getDisplayDetails($view, $display) {
             '#value' => $this->t('Enable @display_title', ['@display_title' => $display_title]),
             '#limit_validation_errors' => [],
             '#submit' => ['::submitDisplayEnable', '::submitDelayDestination'],
-            '#prefix' => '<li class="enable">',
-            "#suffix" => '</li>',
           ];
         }
         // Add a link to view the page unless the view is disabled or has no
@@ -448,8 +442,6 @@ public function getDisplayDetails($view, $display) {
               '#title' => $this->t('View @display_title', ['@display_title' => $display_title]),
               '#options' => ['alt' => [$this->t("Go to the real page for this display")]],
               '#url' => $url,
-              '#prefix' => '<li class="view">',
-              "#suffix" => '</li>',
             ];
           }
         }
@@ -459,8 +451,6 @@ public function getDisplayDetails($view, $display) {
             '#value' => $this->t('Duplicate @display_title', ['@display_title' => $display_title]),
             '#limit_validation_errors' => [],
             '#submit' => ['::submitDisplayDuplicate', '::submitDelayDestination'],
-            '#prefix' => '<li class="duplicate">',
-            "#suffix" => '</li>',
           ];
         }
         // Always allow a display to be deleted.
@@ -469,8 +459,6 @@ public function getDisplayDetails($view, $display) {
           '#value' => $this->t('Delete @display_title', ['@display_title' => $display_title]),
           '#limit_validation_errors' => [],
           '#submit' => ['::submitDisplayDelete', '::submitDelayDestination'],
-          '#prefix' => '<li class="delete">',
-          "#suffix" => '</li>',
         ];
 
         foreach (Views::fetchPluginNames('display', NULL, [$view->get('storage')->get('base_table')]) as $type => $label) {
@@ -478,13 +466,11 @@ public function getDisplayDetails($view, $display) {
             continue;
           }
 
-          $build['top']['actions']['duplicate_as'][$type] = [
+          $build['top']['actions']['duplicate_as_' . $type] = [
             '#type' => 'submit',
             '#value' => $this->t('Duplicate as @type', ['@type' => $label]),
             '#limit_validation_errors' => [],
             '#submit' => ['::submitDuplicateDisplayAsType', '::submitDelayDestination'],
-            '#prefix' => '<li class="duplicate">',
-            '#suffix' => '</li>',
           ];
         }
       }
@@ -494,8 +480,6 @@ public function getDisplayDetails($view, $display) {
           '#value' => $this->t('Undo delete of @display_title', ['@display_title' => $display_title]),
           '#limit_validation_errors' => [],
           '#submit' => ['::submitDisplayUndoDelete', '::submitDelayDestination'],
-          '#prefix' => '<li class="undo-delete">',
-          "#suffix" => '</li>',
         ];
       }
       if ($is_enabled) {
@@ -504,11 +488,8 @@ public function getDisplayDetails($view, $display) {
           '#value' => $this->t('Disable @display_title', ['@display_title' => $display_title]),
           '#limit_validation_errors' => [],
           '#submit' => ['::submitDisplayDisable', '::submitDelayDestination'],
-          '#prefix' => '<li class="disable">',
-          "#suffix" => '</li>',
         ];
       }
-      $build['top']['actions']['suffix']['#markup'] = '</ul>';
 
       // The area above the three columns.
       $build['top']['display_title'] = [
@@ -706,7 +687,7 @@ public function renderDisplayTop(ViewUI $view) {
 
     // Extra actions for the display
     $element['extra_actions'] = [
-      '#type' => 'dropbutton',
+      '#type' => 'splitbutton',
       '#attributes' => [
         'id' => 'views-display-extra-actions',
       ],
@@ -770,6 +751,12 @@ public function renderDisplayTop(ViewUI $view) {
     }
 
     // Buttons for adding a new display.
+    $element['add_display'] = [
+      '#type' => 'splitbutton',
+      '#main_items' => FALSE,
+      '#trigger_label' => $this->t('Add'),
+      '#trigger_label_visible' => TRUE,
+    ];
     foreach (Views::fetchPluginNames('display', NULL, [$view->get('base_table')]) as $type => $label) {
       $element['add_display'][$type] = [
         '#type' => 'submit',
@@ -894,7 +881,8 @@ public function submitDuplicateDisplayAsType($form, FormStateInterface $form_sta
 
     // Create the new display.
     $parents = $form_state->getTriggeringElement()['#parents'];
-    $display_type = array_pop($parents);
+
+    $display_type = str_replace('duplicate_as_', '', array_pop($parents));
 
     $new_display_id = $view->duplicateDisplayAsType($display_id, $display_type);
 
@@ -968,7 +956,7 @@ public function getFormBucket(ViewUI $view, $type, $display) {
     $build['#title'] = $types[$type]['title'];
 
     $rearrange_url = Url::fromRoute('views_ui.form_rearrange', ['js' => 'nojs', 'view' => $view->id(), 'display_id' => $display['id'], 'type' => $type]);
-    $class = 'icon compact rearrange';
+    $class = 'rearrange';
 
     // Different types now have different rearrange forms, so we use this switch
     // to get the right one.
@@ -978,7 +966,7 @@ public function getFormBucket(ViewUI $view, $type, $display) {
         // the used path.
         $rearrange_url = Url::fromRoute('views_ui.form_rearrange_filter', ['js' => 'nojs', 'view' => $view->id(), 'display_id' => $display['id']]);
         // TODO: Add another class to have another symbol for filter rearrange.
-        $class = 'icon compact rearrange';
+        $class = 'rearrange';
         break;
       case 'field':
         // Fetch the style plugin info so we know whether to list fields or not.
@@ -1017,7 +1005,7 @@ public function getFormBucket(ViewUI $view, $type, $display) {
     $actions['add'] = [
       'title' => $add_text,
       'url' => Url::fromRoute('views_ui.form_add_handler', ['js' => 'nojs', 'view' => $view->id(), 'display_id' => $display['id'], 'type' => $type]),
-      'attributes' => ['class' => ['icon compact add', 'views-ajax-link'], 'id' => 'views-add-' . $type],
+      'attributes' => ['class' => ['add', 'views-ajax-link'], 'id' => 'views-add-' . $type],
     ];
     if ($count_handlers > 0) {
       // Create the rearrange text variable for the rearrange action.
@@ -1032,7 +1020,8 @@ public function getFormBucket(ViewUI $view, $type, $display) {
 
     // Render the array of links
     $build['#actions'] = [
-      '#type' => 'dropbutton',
+      '#type' => 'splitbutton',
+      '#splitbutton_type' => ['extrasmall'],
       '#links' => $actions,
       '#attributes' => [
         'class' => ['views-ui-settings-bucket-operations'],
diff --git a/core/modules/workflows/src/Form/WorkflowEditForm.php b/core/modules/workflows/src/Form/WorkflowEditForm.php
index 0f783f4c04..6d4044dd2b 100644
--- a/core/modules/workflows/src/Form/WorkflowEditForm.php
+++ b/core/modules/workflows/src/Form/WorkflowEditForm.php
@@ -140,7 +140,7 @@ public function form(array $form, FormStateInterface $form_state) {
           '#delta' => $state_weight_delta,
         ],
         'operations' => [
-          '#type' => 'operations',
+          '#type' => 'splitbutton_operations',
           '#links' => $links,
         ],
       ];
@@ -205,7 +205,7 @@ public function form(array $form, FormStateInterface $form_state) {
         ],
         'to' => ['#markup' => $transition->to()->label()],
         'operations' => [
-          '#type' => 'operations',
+          '#type' => 'splitbutton_operations',
           '#links' => $links,
         ],
       ];
diff --git a/core/modules/workflows/src/Form/WorkflowStateEditForm.php b/core/modules/workflows/src/Form/WorkflowStateEditForm.php
index 0ecc3cde6b..6b114ffa23 100644
--- a/core/modules/workflows/src/Form/WorkflowStateEditForm.php
+++ b/core/modules/workflows/src/Form/WorkflowStateEditForm.php
@@ -139,7 +139,7 @@ public function form(array $form, FormStateInterface $form_state) {
           '#markup' => $transition->to()->label(),
         ],
         'operations' => [
-          '#type' => 'operations',
+          '#type' => 'splitbutton_operations',
           '#links' => $links,
         ],
       ];
diff --git a/core/themes/claro/claro.theme b/core/themes/claro/claro.theme
index df810f09b8..70c9425767 100644
--- a/core/themes/claro/claro.theme
+++ b/core/themes/claro/claro.theme
@@ -823,7 +823,7 @@ function claro_preprocess_field_multiple_value_form(&$variables) {
     }
 
     // Make add-more button smaller.
-    if (!empty($variables['button'])) {
+    if (isset($variables['button']['#type']) && $variables['button']['#type'] == 'submit') {
       $variables['button']['#attributes']['class'][] = 'button--small';
     }
   }
diff --git a/core/themes/claro/css/components/form.css b/core/themes/claro/css/components/form.css
index c2ab325e92..c966ca8e46 100644
--- a/core/themes/claro/css/components/form.css
+++ b/core/themes/claro/css/components/form.css
@@ -201,8 +201,8 @@ tr .form-item,
   margin-bottom: 1rem;
 }
 
-.form-actions .button,
-.form-actions .action-link {
+.form-actions > .button,
+.form-actions > .action-link {
   margin-top: 1rem;
   margin-bottom: 1rem;
 }
diff --git a/core/themes/claro/css/components/form.pcss.css b/core/themes/claro/css/components/form.pcss.css
index a39ea94255..798f32ada8 100644
--- a/core/themes/claro/css/components/form.pcss.css
+++ b/core/themes/claro/css/components/form.pcss.css
@@ -124,8 +124,8 @@ tr .form-item,
   margin-top: var(--space-m);
   margin-bottom: var(--space-m);
 }
-.form-actions .button,
-.form-actions .action-link {
+.form-actions > .button,
+.form-actions > .action-link {
   margin-top: var(--space-m);
   margin-bottom: var(--space-m);
 }
diff --git a/core/themes/stable/css/core/splitbutton/splitbutton.css b/core/themes/stable/css/core/splitbutton/splitbutton.css
new file mode 100644
index 0000000000..91cc1ffd50
--- /dev/null
+++ b/core/themes/stable/css/core/splitbutton/splitbutton.css
@@ -0,0 +1,127 @@
+
+/**
+ * @file
+ * Base styles for splitbuttons.
+ */
+
+/**
+ * When a splitbutton has only one item, it is simply a button.
+ */
+.splitbutton {
+  display: inline-block;
+  box-sizing: border-box;
+  max-width: 100%;
+}
+
+.splitbutton--multiple {
+  padding-right: 2em; /* LTR */
+}
+
+[dir="rtl"] .splitbutton--multiple {
+  padding-right: 0;
+  padding-left: 2em;
+}
+
+.js .splitbutton--multiple {
+  position: relative;
+}
+
+.splitbutton__action.splitbutton__action {
+  width: 100%;
+  margin: 0;
+  text-align: left; /* LTR */
+}
+[dir="rtl"] .splitbutton__action.splitbutton__action {
+  text-align: right;
+}
+
+@media screen and (max-width: 600px) {
+  .js .splitbutton {
+    width: 100%;
+  }
+}
+
+.js td .splitbutton-multiple .splitbutton-action a,
+.js td .splitbutton-multiple .splitbutton-action input,
+.js td .splitbutton-multiple .splitbutton-action button {
+  width: auto;
+}
+
+/* UL styles are over-scoped in core, so this selector needs weight parity. */
+.splitbutton__list.splitbutton__list {
+  margin: 0;
+  padding: 0;
+  list-style: none;
+}
+
+/**
+ * To prevent the potential toggle jump, we hide the list without using
+ * display: none.
+ * For the collapsed splitbuttons, the required tabindex="-1" is added and
+ * removed by splitbutton.js.
+ */
+.js .splitbutton__list:not(.open) .splitbutton__list-item {
+  height: 0;
+  overflow: hidden;
+}
+
+/**
+ * The splitbutton styling.
+ *
+ * A splitbutton is a widget that displays a list of action links as a button
+ * with a primary action. Secondary actions are hidden behind a click on a
+ * twisty arrow.
+ *
+ * The arrow is created using border on a zero-width, zero-height span.
+ * The arrow inherits the link color, but can be overridden with border colors.
+ */
+.splitbutton.open {
+  z-index: 100;
+  max-width: none;
+}
+
+.splitbutton__toggle.splitbutton__toggle {
+  position: absolute;
+  top: 0;
+  right: 0; /* LTR */
+  bottom: 0;
+  width: 2em;
+  margin: 0;
+  padding: 0;
+}
+[dir="rtl"] .splitbutton__toggle.splitbutton__toggle {
+  right: auto;
+  left: 0;
+}
+
+.splitbutton__toggle-arrow {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  width: 0;
+  height: 0;
+  transform: translate(-50%, -50%);
+  border-width: 0.3333em 0.3333em 0;
+  border-style: solid;
+  border-right-color: transparent;
+  border-bottom-color: transparent;
+  border-left-color: transparent;
+  line-height: 0;
+}
+
+.splitbutton.open .splitbutton__toggle-arrow {
+  border-top-color: transparent;
+  border-bottom: 0.3333em solid;
+}
+
+.splitbutton .ajax-progress-throbber {
+  position: absolute;
+  top: 100%;
+  left: 0;
+  display: flex;
+  width: auto;
+}
+
+.splitbutton .ajax-progress-throbber .message {
+  flex: 1 0 auto;
+}
diff --git a/core/themes/stable/stable.info.yml b/core/themes/stable/stable.info.yml
index 0f20f7c604..608db4af6e 100644
--- a/core/themes/stable/stable.info.yml
+++ b/core/themes/stable/stable.info.yml
@@ -85,6 +85,13 @@ libraries-override:
     css:
       component:
         misc/dropbutton/dropbutton.css: css/core/dropbutton/dropbutton.css
+
+  core/drupal.splitbutton:
+    css:
+      component:
+        misc/splitbutton/splitbutton.css: css/core/splitbutton/splitbutton.css
+
+
   core/drupal.vertical-tabs:
     css:
       component:
diff --git a/core/themes/stable/templates/form/splitbutton.html.twig b/core/themes/stable/templates/form/splitbutton.html.twig
new file mode 100644
index 0000000000..1ab5853850
--- /dev/null
+++ b/core/themes/stable/templates/form/splitbutton.html.twig
@@ -0,0 +1,41 @@
+{#
+/**
+ * @file
+ * Default theme implementation of a splitbutton used to wrap child elements.
+ *
+ * Used for grouped buttons and link items.
+ *
+ * Available variables:
+ * - attributes: HTML attributes for the containing element.
+ * - content_attributes: HTML attributes for the list element.
+ * - item_attributes: HTML attributes for the list item.
+ * - main_items: The uncollapsed splitbutton elements.
+ * - items: Further child elements of the splitbutton.
+ * - multiple: Whether the splitbutton has more than one item.
+ *
+ * @see template_preprocess_splitbutton()
+ */
+#}
+{% if main_items or items %}
+{%
+  set classes = [
+    'splitbutton',
+    multiple ? 'splitbutton--multiple' : 'splitbutton--single',
+  ]
+%}
+<div{{ attributes.addClass(classes) }}>
+  <div class="splitbutton__main-items">
+  {% for main_item in main_items %}
+    {{ main_item.value }}
+  {% endfor %}
+  </div>
+
+  {% if items %}
+  <ul{{ content_attributes.addClass('splitbutton__list') }}>
+    {% for item in items %}
+      <li{{ item.attributes.addClass('splitbutton__list-item') }}>{{ item.value }}</li>
+    {% endfor %}
+  </ul>
+  {% endif %}
+</div>
+{% endif %}
