Split Buttons
Split buttons let people take a primary action and choose from a set of related secondary actions.
Split buttons consist of a leading button for the primary action and a trailing toggle for secondary options. They provide a compact way to group related actions while maintaining a clear primary call to action.
Introduction
Material Design 3 Expressive split buttons are advanced interactive elements designed with high-fidelity motion physics and shape-morphing capabilities. They provide tactile feedback and maintain visual continuity by synchronizing the inner corner radii of both buttons during state transitions.
Anatomy
- Leading Button: The primary action element. Supports text labels and leading icons.
- Trailing Button: A toggle or action button for secondary options. Features a rotating chevron or custom icon.
- Separator: A 2px visual gap between the two buttons, ensuring they feel connected but distinct.
- Shared Morphing: The inner corners of both buttons utilize synchronized spring physics to adjust their radius during hover, focus, and selection states.
Variants
Color Styles
Split buttons support four main visual variants to accommodate different levels of hierarchy:
- Filled: High emphasis, for the primary set of actions.
- Tonal: Medium emphasis, for secondary flows or less prominent actions.
- Elevated: High emphasis, used on surface backgrounds to provide depth.
- Outlined: Medium-low emphasis, for actions that need clear boundaries without overwhelming the layout.
Uncheckable Variant
The trailing button can also be used as a simple secondary action button without a persistent toggle state. This is useful for immediate actions like "Schedule" or "Delete".
Features
Expressive Motion
This component utilizes a dual-motion scheme optimized for both aesthetics and utility:
- Spatial Spring: Used for the container's shape morphing (border-radius). It uses a fast, bouncy spring to create a tactile feel.
- Standard Motion: Used for the trailing icon rotation. This ensures functional clarity and precision when opening menus.
Responsive Scaling
Following the MD3 Expressive specification, split buttons scale across five sizes to support dense mobile layouts and large spatial UIs:
| Size | Height | Use Case |
|---|---|---|
| XS | 32dp | Dense data tables or toolbars |
| SM | 40dp | Default mobile and web interfaces |
| MD | 56dp | Desktop-first or high-density layouts |
| LG | 96dp | Hero moments and large spatial displays |
| XL | 136dp | Large immersive experiences |
Usage
Basic Usage
import {
SplitButtonLayout,
SplitButtonLeading,
SplitButtonTrailing
} from "@bug-on/m3-expressive";
const [checked, setChecked] = React.useState(false);
<SplitButtonLayout
variant="filled"
leadingButton={<SplitButtonLeading>Save</SplitButtonLeading>}
trailingButton={
<SplitButtonTrailing
checked={checked}
onCheckedChange={setChecked}
aria-label="More save options"
/>
}
/>
Menu Integration
The Split Button is most commonly used as a trigger for a Menu. Synchronizing the checked state of the trailing button with the open state of the menu ensures a smooth expressive morphing transition.
Best Practices
Do
- Align the menu with the trailing button to maintain visual context.
- Use Large or Extra Large sizes for primary "Hero" actions on landing pages.
- Provide a descriptive
aria-labelfor the trailing button to explain what options it reveals.
Don't
- Don't use very long labels for the leading button; keep it to one or two words.
- Don't change the trailing icon if it's used for a menu; the rotating chevron is a standard affordance.
Design Tokens
Corner Radius
Split buttons utilize a complex inner corner radius that adjusts based on state to create the expressive morphing effect:
| Size | Default | Hovered/Pressed |
|---|---|---|
| XS | 4dp | 8dp (Small) |
| SM / MD | 4dp | 12dp (Medium) |
| LG | 8dp | 28dp (Large+) |
| XL | 12dp | 28dp (Large+) |
The outer corners always maintain a Full Pill (50%) shape to ensure a consistent silhouette.
Motion
| Animation | Spring Token | Tier |
|---|---|---|
| Shape/Size | FAST_SPATIAL_SPRING | Fast |
| Icon Rotation | STANDARD_EASING | Functional |
Accessibility
- Keyboard Support: Navigate between buttons using
Tab. Activate the primary action withEnterand the menu toggle withSpaceorEnter. - ARIA Roles: The container uses
role="group"to associate related actions. The trailing button includesaria-haspopup="menu"(customizable) andaria-expanded(viachecked) for screen reader compatibility. - Reduced Motion: All spring animations automatically scale down or disable when the user prefers reduced motion.
API Reference
SplitButtonLayout
| Prop | Type | Default | Description |
|---|---|---|---|
leadingButton | ReactNode | — | The primary action button. |
trailingButton | ReactNode | — | The secondary action or toggle component. |
variant | "filled" | "tonal" | "elevated" | "outlined" | — | Visual variant for both buttons. Automatically passed down to sub-components via Context. |
size | "xs" | "sm" | "md" | "lg" | "xl" | — | Size for both buttons. Automatically passed down to sub-components via Context. |
spacing | number | 2 | Gap between buttons in pixels. |
className | string | — | Custom styling classes. |
SplitButtonLeading (and variants)
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "filled" | "tonal" | "elevated" | "outlined" | "filled" | Visual variant. |
size | "xs" | "sm" | "md" | "lg" | "xl" | "sm" | Component size. |
icon | ReactNode | — | Leading icon. |
disabled | boolean | false | Disables the button. |
asChild | boolean | false | Renders as the child element while keeping styles. |
SplitButtonTrailing (and variants)
| Prop | Type | Default | Description |
|---|---|---|---|
checked | boolean | false | Toggle state (sync with menu open state). |
onCheckedChange | (checked: boolean) => void | — | Callback for state change. |
icon | ReactNode | arrow_drop_down | Icon to display (rotates when checked). |
size | "xs" | "sm" | "md" | "lg" | "xl" | "sm" | Component size. |
aria-label | string | — | Required. Label for screen readers. |
aria-haspopup | boolean | "menu" | "listbox" | "tree" | "grid" | "dialog" | "menu" | Type of interactive popup element. |
asChild | boolean | false | Renders as the child element while keeping styles. |
SplitButtonTrailingUncheckable
| Prop | Type | Default | Description |
|---|---|---|---|
icon | ReactNode | — | Icon to display. |
onClick | () => void | — | Click handler. |
size | "xs" | "sm" | "md" | "lg" | "xl" | "sm" | Component size. |
aria-label | string | — | Required. Label for screen readers. |
asChild | boolean | false | Renders as the child element while keeping styles. |