/Documentation

Split button

PublishedUpdated

A split button pairs one action with a button that opens more choices: Save, with "Save a copy" behind the chevron; Send, with "Schedule send". Use it when one action is the obvious default and the rest are variations of it; for unrelated choices use a menu, for alternatives of equal weight a button group. See the M3 split button guidelines.

Usage

The leading button does the common thing; the trailing one opens a menu of items, and its chevron turns over while the menu is open.

Vanilla
import { createSplitButton } from 'mtrl';

const splitButton = createSplitButton({
  text: 'Watch later',
  icon: watchIcon,
  trailingLabel: 'More watch options',
  items: [
    { id: 'queue', text: 'Add to queue' },
    { id: 'playlist', text: 'Save to playlist' },
  ],
});
splitButton.on('click', () => watchLater());
document.body.append(splitButton.element);
Web Components
<m-split-button trailing-label="More watch options">
  Watch later
  <m-menu-item value="queue">Add to queue</m-menu-item>
  <m-menu-item value="playlist">Save to playlist</m-menu-item>
</m-split-button>

<script type="module">
  const splitButton = document.querySelector('m-split-button');
  splitButton.setAttribute('icon', watchIcon);
  splitButton.addEventListener('click', () => watchLater());
</script>
React
import { SplitButton, MenuItem } from 'mtrl/react';
import { watchLater, watchIcon } from './app';

export function Example() {
  return (
    <SplitButton onClick={() => watchLater()} icon={watchIcon} trailingLabel="More watch options">
      Watch later
      <MenuItem value="queue">Add to queue</MenuItem>
      <MenuItem value="playlist">Save to playlist</MenuItem>
    </SplitButton>
  );
}
Vue
<script setup lang="ts">
import { MSplitButton, MMenuItem } from 'mtrl/vue';
import { watchLater, watchIcon } from './app';
</script>

<template>
  <MSplitButton @click="watchLater()" :icon="watchIcon" trailing-label="More watch options">
    Watch later
    <MMenuItem value="queue">Add to queue</MMenuItem>
    <MMenuItem value="playlist">Save to playlist</MMenuItem>
  </MSplitButton>
</template>
Svelte
<script lang="ts">
  import { SplitButton, MenuItem } from 'mtrl/svelte';
  import { watchLater, watchIcon } from './app';
</script>

<SplitButton onclick={() => watchLater()} icon={watchIcon} trailingLabel="More watch options">
  Watch later
  <MenuItem value="queue">Add to queue</MenuItem>
  <MenuItem value="playlist">Save to playlist</MenuItem>
</SplitButton>
SolidJS
import { SplitButton, MenuItem } from 'mtrl/solid';
import { watchLater, watchIcon } from './app';

export function Example() {
  return (
    <SplitButton onClick={() => watchLater()} icon={watchIcon} trailingLabel="More watch options">
      Watch later
      <MenuItem value="queue">Add to queue</MenuItem>
      <MenuItem value="playlist">Save to playlist</MenuItem>
    </SplitButton>
  );
}

A chosen item emits select: the factory's payload has the item, the web component's detail its value, the item's id.

Examples

Variant and size

variant is filled, tonal, outlined or elevated, shared by both halves, and size runs from xs to xl like the button's. Opening the menu changes a state layer and the shape, never the color.

Vanilla
import { createSplitButton } from 'mtrl';

const splitButton = createSplitButton({
  text: 'Save',
  variant: 'tonal',
  size: 'm',
  items: [
    { id: 'save-as', text: 'Save as…' },
    { id: 'save-copy', text: 'Save a copy' },
  ],
});
document.body.append(splitButton.element);
Web Components
<m-split-button variant="tonal" size="m">
  Save
  <m-menu-item value="save-as">Save as…</m-menu-item>
  <m-menu-item value="save-copy">Save a copy</m-menu-item>
</m-split-button>
React
import { SplitButton, MenuItem } from 'mtrl/react';

export function Example() {
  return (
    <SplitButton variant="tonal" size="m">
      Save
      <MenuItem value="save-as">Save as…</MenuItem>
      <MenuItem value="save-copy">Save a copy</MenuItem>
    </SplitButton>
  );
}
Vue
<script setup lang="ts">
import { MSplitButton, MMenuItem } from 'mtrl/vue';
</script>

<template>
  <MSplitButton variant="tonal" size="m">
    Save
    <MMenuItem value="save-as">Save as…</MMenuItem>
    <MMenuItem value="save-copy">Save a copy</MMenuItem>
  </MSplitButton>
</template>
Svelte
<script lang="ts">
  import { SplitButton, MenuItem } from 'mtrl/svelte';
</script>

<SplitButton variant="tonal" size="m">
  Save
  <MenuItem value="save-as">Save as…</MenuItem>
  <MenuItem value="save-copy">Save a copy</MenuItem>
</SplitButton>
SolidJS
import { SplitButton, MenuItem } from 'mtrl/solid';

export function Example() {
  return (
    <SplitButton variant="tonal" size="m">
      Save
      <MenuItem value="save-as">Save as…</MenuItem>
      <MenuItem value="save-copy">Save a copy</MenuItem>
    </SplitButton>
  );
}

setText() and setIcon() change the leading button. Without items, the factory only reports that the trailing button was activated, with expand, collapse and change, and the app opens whatever it likes. layer: 'top' shows the menu in the browser's top layer.

API

Options

Option Type Default Description
text string undefined Label of the leading button
icon string undefined Icon of the leading button, as an HTML string
variant 'filled' | 'tonal' | 'outlined' | 'elevated' 'filled' Visual style, shared by both halves
size 'xs' | 's' | 'm' | 'l' | 'xl' 's' Size of both halves
disabled boolean false Whether both halves are disabled
trailingLabel string 'More options' Accessible name of the trailing button
ariaLabel string undefined Accessible name of the leading button, when its label is not enough
groupLabel string undefined Accessible name of the pair
items MenuContent[] undefined Menu items for the trailing button to open
layer 'top' undefined Renders the menu beside the trailing button and shows it in the top layer
onClick (event: SplitButtonEvent) => void undefined What the leading button does
onSelect (event: SplitButtonEvent) => void undefined Called with the chosen item
on { [event]: handler } undefined Handlers for any of the events
class string undefined Additional CSS classes
prefix string 'mtrl' Prefix for CSS class names

Methods

Method Parameters Returns Description
setText(text) / getText() text: string SplitButtonComponent / string The leading button's label
setIcon(icon) icon: string SplitButtonComponent The leading button's icon
expand() / collapse() none SplitButtonComponent Opens or closes what the trailing button opens
isExpanded() none boolean Whether it is open
enable() / disable() none SplitButtonComponent Both halves together
isDisabled() none boolean Whether the pair is disabled
on(event, handler) / off(event, handler) event: string, handler: Function SplitButtonComponent Adds or removes a listener
destroy() none void Takes it off the page and releases the menu
Property Type Description
element HTMLElement The group holding both halves
leadingElement HTMLButtonElement The leading button
trailingElement HTMLButtonElement The trailing button
menu MenuComponent The menu, when the component was given items

Events

Event Description Data
click The leading button was activated { splitButton, expanded, originalEvent }
expand / collapse The trailing button opened or closed its choices { splitButton, expanded, originalEvent }
change Either of the two, with the new state { splitButton, expanded, originalEvent }
select A menu item was chosen { splitButton, expanded, originalEvent, item }

The web component's select carries { value }; its click is the leading button's.

Accessibility

  • The two halves are a group, named by groupLabel; the leading button is named by its label or ariaLabel.
  • The trailing button has aria-haspopup and aria-expanded. Its label should say how its choices relate to the action: "More watch options" beside "Watch later".
  • Tab moves from the leading button to the trailing one; Space and Enter activate the focused half.
  • At xs and s, each half still offers a 48dp target. The component mirrors in right-to-left layouts.

Styling

Colors come from the button. Filled and tonal halves stay flat on hover, as in a connected button group, so the pair reads as one control; an elevated split button keeps its elevation.

.mtrl-split-button { }
.mtrl-split-button--filled, .mtrl-split-button--xl, .mtrl-split-button--expanded { }
.mtrl-split-button__leading, .mtrl-split-button__trailing, .mtrl-split-button__chevron { }

.toolbar .mtrl-split-button {
  --mtrl-split-button-inner-shape: 8px;          /* where the halves meet */
  --mtrl-split-button-inner-shape-active: 12px;  /* the same, hovered or pressed */
}

Measurements

From the M3 split button tokens, in dp.

Size Height Leading padding Trailing padding Chevron Inner corner Inner corner, active
xs 32 12 / 10 13 22 4 8
s 40 16 / 12 13 22 4 12
m 56 24 / 24 15 26 8 12
l 96 48 / 48 29 38 12 20
xl 136 64 / 64 43 50 16 20

The halves are 2dp apart, the outer corners are a full pill, and the leading button is at least 48dp wide. The resting inner corner departs from the tokens at m, l and xl (4, 8 and 12dp), which read as square beside a full pill; set --mtrl-split-button-inner-shape to go back to them. While the menu is closed, the chevron sits 1 to 6dp off center, so it looks centered between two differently shaped ends; open, it centers and turns 180 degrees.