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>
Pass an aria-label that says what the buttons belong to.

Connected

Use a connected group to select an option, switch a view or sort a page. It spans its container, widens its buttons to fill it and sets the corners between them.

Set selection="single" and give each button a value. The group turns the buttons into toggles and keeps the selected one in value. required keeps one selected at all times. With a name, the group works in its form like a radio group: it submits the selected value, returns to its first value on reset, and with required blocks a submit while nothing is selected.

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

	const places = [
		{ value: 'work', label: 'Work', icon: 'work' },
		{ value: 'restaurant', label: 'Restaurant', icon: 'restaurant' },
		{ value: 'coffee', label: 'Coffee', icon: 'coffee' },
		{ value: 'home', label: 'Home', icon: 'home' },
	]
	let place = $state('work')
</script>

<ButtonGroup
	variant="connected"
	selection="single"
	required
	name="place"
	bind:value={place}
	aria-label="Place"
>
	{#each places as { value, label, icon } (value)}
		<Button variant="tonal" {value}>
			{#snippet start()}
				<Icon>{icon}</Icon>
			{/snippet}
			{label}
		</Button>
	{/each}
</ButtonGroup>

Multiple choice

selection="multiple" lets each button toggle on its own. value holds every selected value. With a name, the form gets one entry per selected value, like a group of checkboxes.

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

	let styles = $state(['bold'])
</script>

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

Sizes

Set size and shape on the group. Its buttons use them unless they set their own. The space between buttons follows their size, so each keeps a 48px target: 18px beside extra small buttons, 12px beside small ones and 8px for larger sizes. In a connected group, the corners where buttons meet follow the size too.

<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 how much a single neighbour shrinks. Only standard groups change widths. 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-space1.125rem extra small, 0.75rem small, 0.5rem above, 0.125rem when connected
--np-button-group-inner-corner0.25rem extra small, 0.5rem small and medium, 1rem large, 1.25rem extra large
--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". The buttons keep their own names. Pass an aria-label to say what the group is for. Pass your own role where the buttons mean something more specific, such as radiogroup.

The buttons share one tab stop: Tab enters the group on its first button, and the arrow keys move between the buttons. Space and Enter press or select the focused button and run the same press motion as a pointer. Set arrowKeys to false to let Tab visit each button instead. Selected buttons announce themselves as pressed. Buttons with only an icon need a title.

API

Props

AttributeTypeDefaultDescription
variant'standard' | 'connected''standard'A connected group moves its buttons together and shortens the corners between them.
size'xs' | 's' | 'm' | 'l' | 'xl'undefinedSize of the buttons that do not set their own.
shape'round' | 'square'undefinedShape of the buttons that do not set their own.
selection'single' | 'multiple'undefinedLets the group manage selection for its buttons with a value.
valuebind:string | string[] | nullundefinedThe selected value, or with multiple the selected values.
requiredbooleanfalseKeeps one button selected, and makes the form require a selection.
namestringundefinedSubmits the selection with its form under this name.
formstringundefinedThe id of the form the selection belongs to, if it is not inside it.
arrowKeysbooleantrueThe buttons share one tab stop and follow the arrow keys. When false, Tab visits each button.
expandedRationumber0.15Share of its width a pressed button grows by. Standard groups only.
compressionLimitnumber24Most pixels a single neighbour shrinks by. Standard groups only.
elementbind:HTMLElementundefinedThe root DOM element of the component, bound with bind:this.
...attributesHTMLAttributes<HTMLDivElement>Other attributes go to the group element, a div.