Toggle Group
A set of toggle buttons of which one or several are pressed, such as text alignment or formatting.
- v1.0.0
- WAI-ARIA: toggle buttons
- SSR
- Zoneless
- 3.1 kB gzip
- +1.6 kB Angular CDK, shared
import { Component, signal } from '@angular/core';
import { NdIcon } from '@ndop/core';
import { ToggleGroup, ToggleGroupItem } from '@/ui/toggle-group/toggle-group';
/** Text formatting where any number of options combine, in the outline style. */
@Component({
selector: 'example-toggle-group-demo',
imports: [NdIcon, ToggleGroup, ToggleGroupItem],
template: `
<div
ndToggleGroup
type="multiple"
variant="outline"
aria-label="Text formatting"
[(value)]="formatting"
>
<button ndToggleGroupItem value="bold" aria-label="Toggle bold">
<svg ndIcon="bold"></svg>
</button>
<button ndToggleGroupItem value="italic" aria-label="Toggle italic">
<svg ndIcon="italic"></svg>
</button>
<button ndToggleGroupItem value="underline" aria-label="Toggle underline">
<svg ndIcon="underline"></svg>
</button>
</div>
`,
})
export class ToggleGroupDemoExample {
protected readonly formatting = signal<readonly string[]>(['bold']);
}
A toggle group is a set of toggle buttons: one pressed at most, such as
text alignment, or any number, such as bold, italic and underline. Only
one of its buttons is in the tab order, and the arrow keys move between
them. Its value is always an array of the pressed buttons' values. With
spacing 0, the default, its buttons join into one bar; the outline
variant draws a border around each.
Installation
npx @ndop/cli add toggle-grouppnpm dlx @ndop/cli add toggle-groupyarn dlx @ndop/cli add toggle-groupbunx @ndop/cli add toggle-groupInstall the dependencies
npm install @ndop/primitives @ndop/corepnpm add @ndop/primitives @ndop/coreyarn add @ndop/primitives @ndop/corebun add @ndop/primitives @ndop/coreCopy the code into your project
At the paths your
ndop.jsonaliases point to; these use the defaults.src/app/ui/toggle-group/toggle-group.tsimport { computed, Directive, inject, input, numberAttribute, } from '@angular/core'; import { NdToggleGroup, NdToggleGroupItem, } from '@ndop/primitives/toggle-group'; import { toggleVariants, type ToggleSize, type ToggleVariant, } from '@/ui/toggle/toggle'; import { cn } from '@/lib/utils'; /** * A group of toggle buttons (`@ndop/primitives/toggle-group`): one pressed * at most (`type="single"`, the default) or any number * (`type="multiple"`). Arrow keys move between items. Its value is always * an array of the pressed items' values. Name it with `aria-label`. Its * `variant` and `size` apply to every item; with `spacing` 0 (the default) * the items join into one bar. * * @example * ```html * <div ndToggleGroup type="multiple" aria-label="Text formatting" [(value)]="formatting"> * <button ndToggleGroupItem value="bold" aria-label="Toggle bold"><svg ndIcon="bold"></svg></button> * <button ndToggleGroupItem value="italic" aria-label="Toggle italic"><svg ndIcon="italic"></svg></button> * </div> * ``` */ @Directive({ selector: '[ndToggleGroup]', hostDirectives: [ { directive: NdToggleGroup, inputs: ['value', 'defaultValue', 'type', 'orientation', 'disabled'], outputs: ['valueChange'], }, ], host: { 'data-slot': 'toggle-group', '[attr.data-variant]': 'variant()', '[attr.data-size]': 'size()', '[attr.data-spacing]': 'spacing()', '[style.--gap]': 'spacing()', '[class]': 'classes()', }, }) export class ToggleGroup { /** Visual style of every item. */ readonly variant = input<ToggleVariant>('default'); /** Size of every item. */ readonly size = input<ToggleSize>('default'); /** * Space between items, in Tailwind spacing units; 0 joins them into one * bar. */ readonly spacing = input(0, { transform: numberAttribute }); /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( 'group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs', 'data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch', this.class(), ), ); } /** * One toggle button of a {@link ToggleGroup}, styled like a Toggle with the * group's variant and size. * * @example * ```html * <button ndToggleGroupItem value="bold" aria-label="Toggle bold"> * <svg ndIcon="bold"></svg> * </button> * ``` */ @Directive({ selector: 'button[ndToggleGroupItem]', hostDirectives: [ { directive: NdToggleGroupItem, inputs: ['value', 'disabled'] }, ], host: { 'data-slot': 'toggle-group-item', '[attr.data-variant]': 'resolvedVariant()', '[attr.data-size]': 'resolvedSize()', '[attr.data-spacing]': 'group.spacing()', '[class]': 'classes()', }, }) export class ToggleGroupItem { /** Visual style, when the group doesn't set one. */ readonly variant = input<ToggleVariant>(); /** Size, when the group doesn't set one. */ readonly size = input<ToggleSize>(); /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly group = inject(ToggleGroup); protected readonly resolvedVariant = computed( () => this.variant() ?? this.group.variant(), ); protected readonly resolvedSize = computed( () => this.size() ?? this.group.size(), ); protected readonly classes = computed(() => cn( toggleVariants({ variant: this.resolvedVariant(), size: this.resolvedSize(), }), 'w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10', 'data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-s-md data-[spacing=0]:last:rounded-e-md data-[spacing=0]:data-[variant=outline]:border-s-0 data-[spacing=0]:data-[variant=outline]:first:border-s', this.class(), ), ); }src/app/lib/utils.ts/** * Class helpers used by every styled item. They come from `@ndop/core`; * this file is yours, so it is the place to customize them, for example by * wrapping `cn` with an extended tailwind-merge configuration for your own * utilities. * * @packageDocumentation */ export { cn, variants, type ClassValue, type VariantOf } from '@ndop/core';src/app/ui/toggle/toggle.tsimport { computed, Directive, input } from '@angular/core'; import { NdToggle } from '@ndop/primitives/toggle'; import { cn, variants, type VariantOf } from '@/lib/utils'; /** Class table for {@link Toggle}, shared with Toggle Group's items. */ export const toggleVariants = variants({ base: "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", variants: { variant: { default: 'bg-transparent', outline: 'border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground', }, size: { default: 'h-9 min-w-9 px-2', sm: 'h-8 min-w-8 px-1.5', lg: 'h-10 min-w-10 px-2.5', }, }, defaults: { variant: 'default', size: 'default' }, }); /** Visual style of a {@link Toggle}. */ export type ToggleVariant = VariantOf<typeof toggleVariants, 'variant'>; /** Size of a {@link Toggle}. */ export type ToggleSize = VariantOf<typeof toggleVariants, 'size'>; /** * A toggle button (`@ndop/primitives/toggle`): pressed or released, * announced with `aria-pressed`. Bind it with `[(pressed)]`. Keep its name * the same in both states, such as `aria-label="Toggle bold"`. * * @example * ```html * <button ndToggle aria-label="Toggle bold" [(pressed)]="bold"> * <svg ndIcon="bold"></svg> * </button> * ``` */ @Directive({ selector: 'button[ndToggle]', hostDirectives: [ { directive: NdToggle, inputs: ['pressed', 'defaultPressed', 'disabled'], outputs: ['pressedChange'], }, ], host: { 'data-slot': 'toggle', '[attr.data-variant]': 'variant()', '[attr.data-size]': 'size()', '[class]': 'classes()', }, }) export class Toggle { /** Visual style. */ readonly variant = input<ToggleVariant>('default'); /** Size. */ readonly size = input<ToggleSize>('default'); /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( toggleVariants({ variant: this.variant(), size: this.size() }), this.class(), ), ); }Update the import paths to match your project
The files import each other through the
@/uiand@/libaliases.
Usage
import { ToggleGroup, ToggleGroupItem } from '@/ui/toggle-group/toggle-group';<div
ndToggleGroup
type="multiple"
variant="outline"
aria-label="Text formatting"
[(value)]="formatting"
>
<button ndToggleGroupItem value="bold" aria-label="Toggle bold">
<svg ndIcon="bold"></svg>
</button>
<button ndToggleGroupItem value="italic" aria-label="Toggle italic">
<svg ndIcon="italic"></svg>
</button>
<button ndToggleGroupItem value="underline" aria-label="Toggle underline">
<svg ndIcon="underline"></svg>
</button>
</div>Examples
Toggle Group single
Text alignment, where one option at most is pressed.
import { Component, signal } from '@angular/core';
import { NdIcon } from '@ndop/core';
import { ToggleGroup, ToggleGroupItem } from '@/ui/toggle-group/toggle-group';
/** Text alignment, where one option at most is pressed. */
@Component({
selector: 'example-toggle-group-single',
imports: [NdIcon, ToggleGroup, ToggleGroupItem],
template: `
<div ndToggleGroup aria-label="Text alignment" [(value)]="alignment">
<button ndToggleGroupItem value="start" aria-label="Align start">
<svg ndIcon="text-align-start"></svg>
</button>
<button ndToggleGroupItem value="center" aria-label="Align center">
<svg ndIcon="text-align-center"></svg>
</button>
<button ndToggleGroupItem value="end" aria-label="Align end">
<svg ndIcon="text-align-end"></svg>
</button>
</div>
`,
})
export class ToggleGroupSingleExample {
protected readonly alignment = signal<readonly string[]>(['start']);
}
Toggle Group sizes
The small and large sizes.
import { Component } from '@angular/core';
import { NdIcon } from '@ndop/core';
import { ToggleGroup, ToggleGroupItem } from '@/ui/toggle-group/toggle-group';
/** The small and large sizes. */
@Component({
selector: 'example-toggle-group-sizes',
imports: [NdIcon, ToggleGroup, ToggleGroupItem],
template: `
<div class="flex flex-col items-start gap-4">
@for (size of sizes; track size) {
<div
ndToggleGroup
type="multiple"
variant="outline"
[size]="size"
[attr.aria-label]="'Text formatting, ' + size"
>
<button ndToggleGroupItem value="bold" aria-label="Toggle bold">
<svg ndIcon="bold"></svg>
</button>
<button ndToggleGroupItem value="italic" aria-label="Toggle italic">
<svg ndIcon="italic"></svg>
</button>
<button
ndToggleGroupItem
value="underline"
aria-label="Toggle underline"
>
<svg ndIcon="underline"></svg>
</button>
</div>
}
</div>
`,
})
export class ToggleGroupSizesExample {
protected readonly sizes = ['sm', 'lg'] as const;
}
Toggle Group spacing
Separate buttons instead of one joined bar.
import { Component } from '@angular/core';
import { ToggleGroup, ToggleGroupItem } from '@/ui/toggle-group/toggle-group';
/** Separate buttons instead of one joined bar. */
@Component({
selector: 'example-toggle-group-spacing',
imports: [ToggleGroup, ToggleGroupItem],
template: `
<div
ndToggleGroup
variant="outline"
size="sm"
[spacing]="2"
aria-label="Release channel"
[defaultValue]="['stable']"
>
<button ndToggleGroupItem value="stable">Stable</button>
<button ndToggleGroupItem value="beta">Beta</button>
<button ndToggleGroupItem value="nightly">Nightly</button>
</div>
`,
})
export class ToggleGroupSpacingExample {}
Toggle Group vertical
A vertical group, moved through with the up and down arrows.
import { Component } from '@angular/core';
import { ToggleGroup, ToggleGroupItem } from '@/ui/toggle-group/toggle-group';
/** A vertical group, moved through with the up and down arrows. */
@Component({
selector: 'example-toggle-group-vertical',
imports: [ToggleGroup, ToggleGroupItem],
template: `
<div
ndToggleGroup
orientation="vertical"
variant="outline"
[spacing]="1"
aria-label="Layout"
[defaultValue]="['list']"
>
<button ndToggleGroupItem value="list">List</button>
<button ndToggleGroupItem value="board">Board</button>
<button ndToggleGroupItem value="calendar">Calendar</button>
</div>
`,
})
export class ToggleGroupVerticalExample {}
Toggle Group disabled
A disabled group.
import { Component } from '@angular/core';
import { NdIcon } from '@ndop/core';
import { ToggleGroup, ToggleGroupItem } from '@/ui/toggle-group/toggle-group';
/** A disabled group. */
@Component({
selector: 'example-toggle-group-disabled',
imports: [NdIcon, ToggleGroup, ToggleGroupItem],
template: `
<div
ndToggleGroup
type="multiple"
disabled
aria-label="Text formatting"
[defaultValue]="['bold']"
>
<button ndToggleGroupItem value="bold" aria-label="Toggle bold">
<svg ndIcon="bold"></svg>
</button>
<button ndToggleGroupItem value="italic" aria-label="Toggle italic">
<svg ndIcon="italic"></svg>
</button>
<button ndToggleGroupItem value="underline" aria-label="Toggle underline">
<svg ndIcon="underline"></svg>
</button>
</div>
`,
})
export class ToggleGroupDisabledExample {}
Accessibility
Implements the toggle buttons pattern of the WAI-ARIA Authoring Practices.
Keyboard
Keys the component handles.
| Key | Action |
|---|---|
| Tab | Moves focus into the group, to its tab stop; then out of it. |
| SpaceorEnter | Presses or releases the focused item. |
| ArrowRightorArrowLeft | Horizontal groups: moves focus to the next or previous enabled item, wrapping, without pressing it. |
| ArrowDownorArrowUp | Vertical groups: moves focus to the next or previous enabled item, wrapping, without pressing it. |
| HomeorEnd | Moves focus to the first or last enabled item. |
Focus
- Arrow keys, Home and End move focus only; pressing stays with Space and Enter. Disabled items are skipped.
Right-to-left
In right-to-left layouts, ArrowLeft moves to the next item and ArrowRight to the previous one. Direction comes from the nearest dir attribute, on the item or an ancestor.
API reference
ToggleGroup
[ndToggleGroup]role groupA group of toggle buttons (@ndop/primitives/toggle-group): one pressed
at most (type="single", the default) or any number
(type="multiple"). Arrow keys move between items. Its value is always
an array of the pressed items' values. Name it with aria-label. Its
variant and size apply to every item; with spacing 0 (the default)
the items join into one bar.
| ToggleGroup binding | Type | Default |
|---|---|---|
[variant]Visual style of every item. | 'default' | 'outline' | 'default' |
[size]Size of every item. | 'default' | 'sm' | 'lg' | 'default' |
[spacing]Space between items, in Tailwind spacing units; 0 joins them into one
bar. | number | 0 |
[class]Extra classes, merged last so they win. | string | '' |
[defaultValue]Initial value when it isn't bound. | readonly string[] | [] |
[type]single: pressing an item releases the others. multiple: items are
pressed independently. | 'single' | 'multiple' | 'single' |
[orientation]Which arrow keys move focus: left and right, or up and down. | 'horizontal' | 'vertical' | 'horizontal' |
[disabled]Disables every item. A disabled bound field does too. | boolean | false |
[(value)]The pressed items' values, in both modes. Two-way bindable with
[(value)]. | readonly string[] | [] |
| Data attribute | Values |
|---|---|
data-slot | toggle-group |
data-variant | — |
data-size | — |
data-spacing | — |
data-orientation | — |
data-disabled | — |
ToggleGroupItem
button[ndToggleGroupItem]One toggle button of a ToggleGroup, styled like a Toggle with the
group's variant and size.
Manages aria-pressed.
| ToggleGroupItem binding | Type | Default |
|---|---|---|
[variant]Visual style, when the group doesn't set one. | 'default' | 'outline' | — |
[size]Size, when the group doesn't set one. | 'default' | 'sm' | 'lg' | — |
[class]Extra classes, merged last so they win. | string | '' |
[value]requiredThe value this item adds to the group's while pressed. | string | — |
[disabled]Disables this item; arrow keys skip it. | boolean | false |
| Data attribute | Values |
|---|---|
data-slot | toggle-group-item |
data-variant | — |
data-size | — |
data-spacing | — |
data-state | — |
data-disabled | — |
data-orientation | — |
Agent notes
Do
- Name the group with
aria-label, such as "Text alignment", and every icon-only button too. - Use
type="multiple"when the options combine.
Don't
- Don't use it for navigation between views of a page; use Tabs.
- Don't use a single toggle group for unrelated options.