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
| Token | Default value |
|---|---|
--np-button-group-space | 1.125rem extra small, 0.75rem small, 0.5rem above, 0.125rem when connected |
--np-button-group-inner-corner | 0.25rem extra small, 0.5rem small and medium, 1rem large, 1.25rem extra large |
--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". 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
| Attribute | Type | Default | Description |
|---|---|---|---|
variant | 'standard' | 'connected' | 'standard' | A connected group moves its buttons together and shortens the corners between them. |
size | 'xs' | 's' | 'm' | 'l' | 'xl' | undefined | Size of the buttons that do not set their own. |
shape | 'round' | 'square' | undefined | Shape of the buttons that do not set their own. |
selection | 'single' | 'multiple' | undefined | Lets the group manage selection for its buttons with a value. |
value | bind:string | string[] | null | undefined | The selected value, or with multiple the selected values. |
required | boolean | false | Keeps one button selected, and makes the form require a selection. |
name | string | undefined | Submits the selection with its form under this name. |
form | string | undefined | The id of the form the selection belongs to, if it is not inside it. |
arrowKeys | boolean | true | The buttons share one tab stop and follow the arrow keys. When false, Tab visits each button. |
expandedRatio | number | 0.15 | Share of its width a pressed button grows by. Standard groups only. |
compressionLimit | number | 24 | Most pixels a single neighbour shrinks by. Standard groups only. |
element | bind:HTMLElement | undefined | The root DOM element of the component, bound with bind:this. |
...attributes | HTMLAttributes<HTMLDivElement> | Other attributes go to the group element, a div. |