A split button is one action you will take most of the time, next to a menu of the ones you might take instead — Save and “save as”, Reply and “reply all”, Export and every other format.
The root is a <div class="split-button"> holding
two buttons: the leading half
does the work, and the trailing half is a
menu-trigger that
opens a <menu>. The pair sits 2dp apart, the
outside is round, and the seam between them is square-ish until
something happens to it.
<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><a href="#!">Save a copy</a></li>
<li><a href="#!">Save as template</a></li>
<li><a href="#!">Export as PDF</a></li>
</menu>
</div>
The trailing half is an ordinary
Menu trigger, so everything Menu
does it does here: Expressive.AutoInit() starts it,
data-target names the surface, and the menu's own
keyboard model, alignment and options are unchanged. This component
adds no script of its own.
The menu sizes itself to its own content here. Menu normally matches the surface to its trigger, but a trigger narrower than the surface's own 112dp minimum is not a constraint it can honour — and this trigger is a chevron in a 48dp box. Items still wrap at the menu's 280dp maximum.
Both halves are buttons, so every style class works on them — but use
the same one on both, or the pair stops reading as 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 does two things to the trailing half: the chevron
turns over, and the seam swells to fully round, so the pair reads as
two separated buttons for as long as the menu is up. Both are drawn
from aria-expanded, which Menu writes — there is nothing
to toggle yourself, and nothing to author. Open the demo above and
watch the corner.
Hovering or pressing either half swells that half's inner corner part
of the way, which is Material's own ladder: 8dp at
xsmall, 12dp through medium, 20dp at the
two largest sizes. All of it stops under
prefers-reduced-motion.
Sizes
Five sizes, on the container: xsmall,
small (the default), medium,
large and xlarge — 32, 40, 56, 96 and 136dp
tall, the same ladder as a common
button. The class goes on the split-button, never on
a half: the container sizes both, and the seam, the insets and the
chevron all move with it.
The seam widens with the size — 4dp through medium, 8dp
at large, 12dp at xlarge. The two largest
rungs are 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><a href="#!">One</a></li>
<li><a href="#!">Two</a></li>
</menu>
</div>
The trailing half is narrower than the leading one and its chevron is drawn larger than a button icon of the same size — 22dp against 20 at the bottom of the ladder, 50dp against 40 at the top. That keeps it 48dp wide at the two smallest sizes, which is a touch target, and square at the three largest.
Semantics
The container declares no role. A
composite role
such as toolbar promises arrow-key navigation, and this
component rejects it rather than withholding it: the two
halves are independent commands reached with Tab. The one composite
widget here is the <menu>, which carries its own
role and its own keyboard model.
The trailing half's chevron is decoration, so it is
aria-hidden="true" and the button carries an
aria-label of its own — “More save options”, not
“More”. Name it for what the menu contains, since a screen reader
hears it right after the leading half's label.
aria-expanded is Menu's to write. Do not author it: the
constructor stamps it on the trigger and every open and close
rewrites it, and the expanded shape above is drawn from it — an
authored "true" draws an open split button over a closed
menu.
Both halves are controls, written as direct children: a
<button>, or an
<a class="button" href> when it navigates. A
wrapper element is refused — the seam, the insets and the larger
chevron are all written against direct children. So is an
icon-button:
that is its own component, setting its height, insets, colours and
icon sizing on the element itself, so it reaches none of this
geometry and fights the seam it is handed. The trailing half is a
<button class="button menu-trigger">.
Order matters, and only the markup states it. The
halves are told apart by role rather than by position — which is what
lets you write the <menu> inside the container if
you like — so nothing in the sheet notices a trigger written first.
It would take the trailing side's round outer corners while flex put
it on the leading side: a split button with its seam and its round
ends swapped. Lead action first, trigger second, menu last if it is
in there at all. Exactly two controls, and the count is held from
both ends: a third gets the leading half's rules too, and a container
holding only the trigger leaves it squared off along a seam with
nothing to join to. One control that opens a menu is just a
menu trigger — drop the
container. Several peer actions are a
button group, and
several related ones belong 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 — a small split button; the size classes rewrite 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 states a separate leading and trailing inset for the trailing half; they are equal in all five sizes, so one token carries both — as it does for the hovered and pressed seam, which Material also gives the same value.