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
| Token | Default value |
|---|---|
--np-button-group-space | 0.75rem, 0.125rem when connected |
--np-button-group-inner-corner | 0.5rem |
--np-button-group-pressed-inner-corner | 0.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
| Attribute | Type | Default | Description |
|---|---|---|---|
variant | 'standard' | 'connected' | 'standard' | A connected group moves its buttons together and shortens the corners between them. |
expandedRatio | number | 0.15 | Share of its width a pressed button grows by. Standard groups only. |
compressionLimit | number | 24 | Pixels a single neighbour gives up at most. Standard groups only. |
element | bind:HTMLElement | undefined | A 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. |
...attributes | HTMLAttributes<HTMLDivElement> | Every other attribute is passed to the group element, a div. |