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:
- The lead action.
- The trigger.
- 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-space | 2px |
--md-comp-split-button-container-shape | 9999px |
--md-comp-split-button-leading-button-trailing-space | 12px |
--md-comp-split-button-trailing-button-space | 13px |
--md-comp-split-button-trailing-icon-size | 22px |
--md-comp-split-button-inner-corner-corner-size | 4px |
--md-comp-split-button-inner-pressed-corner-corner-size | 12px |
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.