Skip to content

ExpressiveCSS

Browse documentation

Split button

A lead action and a trailing half that opens a menu of related ones, in five sizes.

View as Markdown

A split button puts the action you take most of the time next to a menu of the ones you might take instead, such as Save and "save as", Reply and "reply all", or Export and every other format.

Put two buttons in a <div class="split-button">. The leading half does the work. The trailing half is a menu-trigger that opens a <menu>. The halves sit 2dp apart. The outer corners are round, and the inner corners at the seam stay nearly square until the user hovers, presses or opens the menu.

  • <div class="split-button">
      <button class="button">Save</button>
      <button class="button menu-trigger" data-target="save-menu" aria-label="More save options">
        <span class="material-symbols" aria-hidden="true">arrow_drop_down</span>
      </button>
      <menu id="save-menu">
        <li><button type="button">Save a copy</button></li>
        <li><button type="button">Save as template</button></li>
        <li><button type="button">Export as PDF</button></li>
      </menu>
    </div>

    The trailing half is an ordinary Menu trigger and behaves like any other: Expressive.AutoInit() starts it, data-target names the surface, and the menu keeps its own keyboard model, alignment and options. This component adds no script of its own.

    Here the menu sizes itself to its content. Menu normally matches the surface width to its trigger. This trigger is a chevron in a 48dp box, and the surface cannot go below its 112dp minimum. Items still wrap at the menu's 280dp maximum.

    Both halves are buttons, so every style class works on them. Use the same class on both, or the pair no longer looks like one shape. A tonal split button is the usual choice when the leading action is not the page's primary one.

  • Activation

    Opening the menu changes the trailing half in two ways. The chevron flips, and its inner corner becomes fully round, so the pair looks like two separate buttons while the menu is open. Both changes come from aria-expanded, which Menu sets, so you have nothing to toggle or author. Open the demo above and watch the corner.

    Hovering or pressing either half rounds that half's inner corner part of the way, using Material's values: 8dp at xsmall, 12dp through medium, and 20dp at the two largest sizes. These changes do not animate under prefers-reduced-motion.

    Sizes

    Put a size class on the split-button, never on a half. The container takes one of five sizes: xsmall, small (the default), medium, large and xlarge. They are 32, 40, 56, 96 and 136dp tall, the same heights as a common button. The container sizes both halves, and the seam, the insets and the chevron scale with it.

  • The seam widens with the size: 4dp through medium, 8dp at large and 12dp at xlarge. Use the two largest sizes for a split button that is the only thing on a screen.

  • <div class="split-button medium">
      <button class="button tonal">Medium</button>
      <button class="button tonal menu-trigger" data-target="size-menu" aria-label="More options">
        <span class="material-symbols" aria-hidden="true">arrow_drop_down</span>
      </button>
      <menu id="size-menu">
        <li><button type="button">One</button></li>
        <li><button type="button">Two</button></li>
      </menu>
    </div>

    The trailing half is narrower than the leading one, and its chevron is larger than a button icon of the same size: 22dp against 20dp at the smallest size and 50dp against 40dp at the largest. This keeps the trailing half 48dp wide at the two smallest sizes, which is a full touch target. At the three largest sizes it keeps the half square.

    Semantics

    The container declares no role. A composite role such as toolbar promises arrow-key navigation, and this component leaves it out on purpose, because the two halves are independent commands reached with Tab. The only composite widget here is the <menu>, which has its own role and keyboard model.

    Mark the trailing half's chevron aria-hidden="true", because it is decoration. Give the button its own aria-label, such as "More save options" rather than "More". Name it for what the menu contains, since a screen reader announces it right after the leading half's label.

    Menu sets aria-expanded, so do not author it. The constructor adds it to the trigger, every open and close rewrites it, and the expanded shape above depends on it. An authored "true" draws an open split button over a closed menu.

    Write both halves as direct child controls: a <button>, or an <a class="button" href> when it navigates. Do not wrap them, because the styles for the seam, the insets and the larger chevron target direct children. Do not use an icon-button either. It is a separate component that sets its own height, insets, colours and icon sizing on the element. None of this geometry applies to it, and it conflicts with the seam. The trailing half is a <button class="button menu-trigger">.

    Source order matters, and the stylesheet does not check it. Write the children in this order:

    1. The lead action.
    2. The trigger.
    3. The menu, if it is inside the container.

    The stylesheet tells the halves apart by role rather than by position, which is what lets you put the <menu> inside the container. As a result, it does not notice a trigger written first. That trigger would get the trailing side's round outer corners while flex places it on the leading side, so the seam and the round ends swap.

    Use exactly two controls. A third control also gets the leading half's rules. A container holding only the trigger leaves it squared off along a seam with nothing to join. For one control that opens a menu, use a menu trigger without the container. Put several peer actions in a button group, and several related ones in the menu.

    Tokens

    Set these on the container, or in a rule of your own that picks out the split buttons you want to change. The values below are the defaults for a small split button. The size classes override all but the first two.

    Token Default
    --md-comp-split-button-between-space2px
    --md-comp-split-button-container-shape9999px
    --md-comp-split-button-leading-button-trailing-space12px
    --md-comp-split-button-trailing-button-space13px
    --md-comp-split-button-trailing-icon-size22px
    --md-comp-split-button-inner-corner-corner-size4px
    --md-comp-split-button-inner-pressed-corner-corner-size12px

    Height, the leading half's outer inset, the outline width and every colour come from the button tokens, which the size class sets on the container for both halves. Material specifies separate leading and trailing insets for the trailing half. They are equal in all five sizes, so one token covers both. The hovered and pressed seam also share one token, because Material gives them the same value.

    No matching pages

  • Source color

    The seed every generated ramp derives from. Pick one and browse the docs — the whole theme follows. Error does not: it is a fixed hue.

    Default source: Mist Iris.

    ExpressiveCSS presets: Lagoon, Harbor, Breeze, Cobalt, Iris.

    Bold

    Soft

    Mist

    Glow