Button groups

A row of buttons or icon buttons that react to a press together.

Usage

<script lang="ts">
	import { ButtonGroup, IconButton } from 'noph-ui'
	import { Icon } from 'noph-ui/icons'
</script>

<ButtonGroup aria-label="Playback">
	<IconButton variant="tonal" width="narrow" size="m" title="Previous">
		<Icon>skip_previous</Icon>
	</IconButton>
	<IconButton variant="filled" width="wide" size="m" title="Play">
		<Icon>play_arrow</Icon>
	</IconButton>
	<IconButton variant="tonal" width="narrow" size="m" title="Next">
		<Icon>skip_next</Icon>
	</IconButton>
</ButtonGroup>
A group needs a name of its own, so pass an aria-label that says what the buttons belong to.

Connected

For selecting an option, switching a view or sorting a page. The buttons are toggle buttons, and the group takes over the corners between them. The outer ends keep the round shape of the button, so leave its buttons on shape="round".

<script lang="ts">
	import { Button, ButtonGroup } from 'noph-ui'
	import { Icon } from 'noph-ui/icons'

	let places = $state([
		{ label: 'Work', icon: 'work', selected: true },
		{ label: 'Restaurant', icon: 'restaurant', selected: false },
		{ label: 'Coffee', icon: 'coffee', selected: false },
		{ label: 'Home', icon: 'home', selected: false },
	])

	const choose = (index: number) => {
		places.forEach((place, i) => (place.selected = i === index))
	}
</script>

<ButtonGroup variant="connected" aria-label="Place">
	{#each places as place, index (place.label)}
		<Button variant="tonal" toggle bind:selected={place.selected} onclick={() => choose(index)}>
			{#snippet start()}
				<Icon>{place.icon}</Icon>
			{/snippet}
			{place.label}
		</Button>
	{/each}
</ButtonGroup>

Multiple choice

<script lang="ts">
	import { Button, ButtonGroup } from 'noph-ui'
	import { Icon } from 'noph-ui/icons'

	let styles = $state({ bold: true, italic: false, underline: false })
</script>

<ButtonGroup variant="connected" aria-label="Text style">
	<Button variant="tonal" toggle bind:selected={styles.bold}>
		{#snippet start()}
			<Icon>format_bold</Icon>
		{/snippet}
		Bold
	</Button>
	<Button variant="tonal" toggle bind:selected={styles.italic}>
		{#snippet start()}
			<Icon>format_italic</Icon>
		{/snippet}
		Italic
	</Button>
	<Button variant="tonal" toggle bind:selected={styles.underline}>
		{#snippet start()}
			<Icon>format_underlined</Icon>
		{/snippet}
		Underline
	</Button>
</ButtonGroup>

Sizes

The buttons carry their own size, the group only holds them together.

<script lang="ts">
	import { ButtonGroup, IconButton } from 'noph-ui'
	import { Icon } from 'noph-ui/icons'

	const alignments = [
		{ title: 'Align left', icon: 'format_align_left' },
		{ title: 'Align center', icon: 'format_align_center' },
		{ title: 'Align right', icon: 'format_align_right' },
	]
</script>

{#each ['xs', 'm'] as const as size (size)}
	<ButtonGroup variant="connected" aria-label="Alignment, size {size}">
		{#each alignments as alignment (alignment.icon)}
			<IconButton {size} variant="tonal" title={alignment.title}>
				<Icon>{alignment.icon}</Icon>
			</IconButton>
		{/each}
	</ButtonGroup>
{/each}

Motion

expandedRatio is the share of its width a pressed button grows by, compressionLimit caps what a single neighbour gives up. Only standard groups change widths, and reduced motion turns the motion off in both variants.

<script lang="ts">
	import { Button, ButtonGroup } from 'noph-ui'
</script>

<ButtonGroup expandedRatio={0.5} aria-label="Rating">
	<Button variant="tonal">Bad</Button>
	<Button variant="tonal">Fine</Button>
	<Button variant="tonal">Great</Button>
</ButtonGroup>

Theming

Tokens

TokenDefault value
--np-button-group-space0.75rem, 0.125rem when connected
--np-button-group-inner-corner0.5rem
--np-button-group-pressed-inner-corner0.25rem

The corner tokens only apply to a connected group.

Example

<script lang="ts">
	import { Button, ButtonGroup } from 'noph-ui'
</script>

<ButtonGroup
	variant="connected"
	aria-label="Custom button group"
	--np-button-group-space="0.25rem"
	--np-button-group-inner-corner="1rem"
>
	<Button variant="tonal">Start</Button>
	<Button variant="tonal">Middle</Button>
	<Button variant="tonal">End</Button>
</ButtonGroup>

Accessibility

The group renders role="group", which ties the buttons together without taking their individual names away. Pass a aria-label to say what the group is for, or your own role where the buttons mean something more specific, radiogroup for instance.

Tab reaches every button in the group, and the press motion runs for the space and enter keys the same way it does for a pointer, so a keyboard user sees the same feedback. Buttons that only carry an icon still need a title.

API

Props

AttributeTypeDefaultDescription
variant'standard' | 'connected''standard'A connected group moves its buttons together and shortens the corners between them.
expandedRationumber0.15Share of its width a pressed button grows by. Standard groups only.
compressionLimitnumber24Pixels a single neighbour gives up at most. Standard groups only.
elementbind:HTMLElementundefinedA reference to the root DOM element of the component. This variable is bound using bind:this, allowing direct access to the underlying HTML element for manipulation or querying within the component's logic.
...attributesHTMLAttributes<HTMLDivElement>Every other attribute is passed to the group element, a div.