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.

AttributeTypeDefaultDescription
labelstring''Text of the action half.
iconSnippet | undefinedundefinedLeading icon on the action half.
menuSnippet | undefinedundefinedMenu items, rendered inside a Menu.
onclickMouseEventHandler | undefinedundefinedThe 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.
openbooleanfalseBindable. Whether the menu is open.
menuLabelstring'More options'Accessible name for the trailing half.