Split button
One button in two halves: a default action, and a caret that opens related actions. Use it when one choice is the obvious one but a few alternatives should stay within reach.
Usage
The action half runs onclick. The trailing half opens the menu and never triggers the
action. Give it a menuLabel, because it shows only a caret. While the menu is open
the caret turns over and its half rounds out into a circle.
<script lang="ts">
import { MenuItem, Snackbar, SplitButton } from 'noph-ui'
let snackbar: ReturnType<typeof Snackbar> | undefined = $state()
const say = () => snackbar?.show()
</script>
<SplitButton label="Save" onclick={say}>
{#snippet menu()}
<MenuItem onclick={say}>Save as draft</MenuItem>
<MenuItem onclick={say}>Save a copy</MenuItem>
{/snippet}
</SplitButton>
<Snackbar bind:this={snackbar} label="Action clicked" />
Variants
Both halves take the same variant, four of the ones a Button takes.
<script lang="ts">
import { MenuItem, Snackbar, SplitButton } from 'noph-ui'
let snackbar: ReturnType<typeof Snackbar> | undefined = $state()
const say = () => snackbar?.show()
</script>
{#each ['filled', 'tonal', 'outlined', 'elevated'] as const as variant (variant)}
<SplitButton {variant} label={variant} onclick={say}>
{#snippet menu()}
<MenuItem onclick={say}>First</MenuItem>
<MenuItem onclick={say}>Second</MenuItem>
{/snippet}
</SplitButton>
{/each}
<Snackbar bind:this={snackbar} label="Action clicked" />
With an icon
<script lang="ts">
import { MenuItem, Snackbar, SplitButton } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
let snackbar: ReturnType<typeof Snackbar> | undefined = $state()
const say = () => snackbar?.show()
</script>
<SplitButton label="Send" onclick={say}>
{#snippet icon()}<Icon>send</Icon>{/snippet}
{#snippet menu()}
<MenuItem onclick={say}>Schedule send</MenuItem>
{/snippet}
</SplitButton>
<Snackbar bind:this={snackbar} label="Action clicked" />
Theming
A split button is two buttons in a group, so it is themed through the button tokens of the variant you picked: --np-filled-button-container-color and its siblings reach both halves.
The shape comes from --np-shape-corner-full, and the two halves keep the small inner
corners that Material 3 Expressive asks for. The caret rotates and the halves swap their shape on
press through the expressive motion tokens, and all of it stops for anyone who prefers reduced
motion.
Accessibility
Both halves are real buttons in a role="group". The leading half is named by its label, and the caret is named by menuLabel, since nothing in an arrow
says what it opens. Give menuLabel a name that fits the action, More send options rather than the default.
The caret carries aria-haspopup="menu" and aria-expanded, and opens the menu, which brings its own arrow key navigation and
returns focus to the caret when it closes. Tab reaches both halves, so a keyboard user can take
the default action without ever opening the menu.
API
Renders a connected ButtonGroup holding the
two halves and the menu. bind:element gives you the group.
| Attribute | Type | Default | Description |
|---|---|---|---|
label | string | '' | Text of the action half. |
icon | Snippet | undefined | undefined | Leading icon on the action half. |
menu | Snippet | undefined | undefined | Menu items, rendered inside a Menu. |
onclick | MouseEventHandler | undefined | undefined | The action half's handler. The trailing half opens the menu instead. |
variant | 'filled' | 'tonal' | 'outlined' | 'elevated' | 'filled' | Applied to both halves. |
size | 'xs' | 's' | 'm' | 'l' | 'xl' | 's' | Applied to both halves. |
open | boolean | false | Bindable. Whether the menu is open. |
menuLabel | string | 'More options' | Accessible name for the trailing half. |