Switch
An on/off control that takes effect right away, such as a setting.
- v1.0.0
- WAI-ARIA: Switch
- SSR
- Zoneless
- 2.4 kB gzip
import { Component } from '@angular/core';
import { Label } from '@/ui/label/label';
import { Switch, SwitchThumb } from '@/ui/switch/switch';
/** A switch that turns a setting on or off right away. */
@Component({
selector: 'example-switch-demo',
imports: [Label, Switch, SwitchThumb],
template: `
<div class="flex items-center gap-2">
<button ndSwitch id="switch-demo-airplane">
<span ndSwitchThumb></span>
</button>
<label ndLabel for="switch-demo-airplane">Airplane Mode</label>
</div>
`,
})
export class SwitchDemoExample {}
A switch turns a setting on or off, taking effect right away, like the
switches in a phone's settings. It's a native button announced as a
switch, with its state as on or off, and binds with [(checked)] or any
forms library. The thumb moves to the end side when it's on, so it
mirrors in right-to-left layouts. A small size suits dense lists.
Installation
npx @ndop/cli add switchpnpm dlx @ndop/cli add switchyarn dlx @ndop/cli add switchbunx @ndop/cli add switchInstall 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/switch/switch.tsimport { computed, Directive, input } from '@angular/core'; import { NdSwitch, NdSwitchThumb } from '@ndop/primitives/switch'; import { cn } from '@/lib/utils'; /** * A switch on a native `button` (`@ndop/primitives/switch`): an on/off * setting that applies at once. Bind it with `[(checked)]` or any forms * library, name it with a `label[ndLabel]`, and put a `ndSwitchThumb` * inside. * * @example * ```html * <button ndSwitch id="airplane" [(checked)]="airplaneMode"> * <span ndSwitchThumb></span> * </button> * <label ndLabel for="airplane">Airplane Mode</label> * ``` */ @Directive({ selector: 'button[ndSwitch]', hostDirectives: [ { directive: NdSwitch, inputs: [ 'checked', 'defaultChecked', 'disabled', 'required', 'invalid', 'id', ], outputs: ['checkedChange'], }, ], host: { 'data-slot': 'switch', '[attr.data-size]': 'size()', '[class]': 'classes()', }, }) export class Switch { /** Size: `default`, or `sm` for dense layouts. */ readonly size = input<'default' | 'sm'>('default'); /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( 'peer group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80', 'aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40', this.class(), ), ); } /** * The thumb of a {@link Switch}. It moves to the end side when on, so to * the left in right-to-left layouts. * * @example * ```html * <span ndSwitchThumb></span> * ``` */ @Directive({ selector: '[ndSwitchThumb]', hostDirectives: [NdSwitchThumb], host: { 'data-slot': 'switch-thumb', '[class]': 'classes()' }, }) export class SwitchThumb { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( 'pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground', 'rtl:data-[state=checked]:-translate-x-[calc(100%-2px)]', 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';Update the import paths to match your project
The files import each other through the
@/uiand@/libaliases.
Usage
import { Label } from '@/ui/label/label';
import { Switch, SwitchThumb } from '@/ui/switch/switch';<div class="flex items-center gap-2">
<button ndSwitch id="switch-demo-airplane">
<span ndSwitchThumb></span>
</button>
<label ndLabel for="switch-demo-airplane">Airplane Mode</label>
</div>Examples
Switch small
The small size, for dense lists of settings.
import { Component } from '@angular/core';
import { Label } from '@/ui/label/label';
import { Switch, SwitchThumb } from '@/ui/switch/switch';
/** The small size, for dense lists of settings. */
@Component({
selector: 'example-switch-small',
imports: [Label, Switch, SwitchThumb],
template: `
<div class="flex items-center gap-2">
<button
ndSwitch
size="sm"
id="switch-small-captions"
[defaultChecked]="true"
>
<span ndSwitchThumb></span>
</button>
<label ndLabel for="switch-small-captions">Captions</label>
</div>
`,
})
export class SwitchSmallExample {}
Switch disabled
Disabled switches, on and off.
import { Component } from '@angular/core';
import { Label } from '@/ui/label/label';
import { Switch, SwitchThumb } from '@/ui/switch/switch';
/** Disabled switches, on and off. */
@Component({
selector: 'example-switch-disabled',
imports: [Label, Switch, SwitchThumb],
template: `
<div class="flex flex-col gap-4">
<div class="flex items-center gap-2">
<button ndSwitch id="switch-disabled-off" disabled>
<span ndSwitchThumb></span>
</button>
<label ndLabel for="switch-disabled-off">Beta features</label>
</div>
<div class="flex items-center gap-2">
<button
ndSwitch
id="switch-disabled-on"
disabled
[defaultChecked]="true"
>
<span ndSwitchThumb></span>
</button>
<label ndLabel for="switch-disabled-on">Managed by your admin</label>
</div>
</div>
`,
})
export class SwitchDisabledExample {}
Switch form
A settings card of switches bound to a Signal Forms model.
import { Component, signal } from '@angular/core';
import { disabled, form, FormField } from '@angular/forms/signals';
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
} from '@/ui/form-field/form-field';
import { Switch, SwitchThumb } from '@/ui/switch/switch';
/**
* Email settings as switches bound to a Signal Forms model, each saved as
* soon as it changes; security emails can't be turned off.
*/
@Component({
selector: 'example-switch-form',
imports: [
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
FormField,
Switch,
SwitchThumb,
],
template: `
<fieldset ndFieldSet class="w-full max-w-md">
<legend ndFieldLegend>Email notifications</legend>
<div ndFieldGroup>
<div ndField orientation="horizontal" class="rounded-lg border p-4">
<div ndFieldContent>
<label ndFieldLabel for="switch-form-marketing">
Marketing emails
</label>
<p ndFieldDescription>
Receive emails about new products, features and more.
</p>
</div>
<button
ndSwitch
id="switch-form-marketing"
[formField]="settings.marketing"
>
<span ndSwitchThumb></span>
</button>
</div>
<div ndField orientation="horizontal" class="rounded-lg border p-4">
<div ndFieldContent>
<label ndFieldLabel for="switch-form-security">
Security emails
</label>
<p ndFieldDescription>
Receive emails about your account security.
</p>
</div>
<button
ndSwitch
id="switch-form-security"
[formField]="settings.security"
>
<span ndSwitchThumb></span>
</button>
</div>
</div>
</fieldset>
`,
})
export class SwitchFormExample {
protected readonly model = signal({ marketing: false, security: true });
protected readonly settings = form(this.model, (path) => {
disabled(path.security);
});
}
Accessibility
Implements the Switch pattern of the WAI-ARIA Authoring Practices.
Keyboard
Keys the component handles.
| Key | Action |
|---|---|
| Space | Turns it on or off. |
| Enter | Turns it on or off. |
Focus
- Unchanged from a native button: activating it keeps focus on it.
- Leaving it marks a bound field touched.
Right-to-left
The thumb's position is styling: the styled item moves it toward the end side, so it mirrors in right-to-left layouts.
API reference
Switch
button[ndSwitch]role switchA switch on a native button (@ndop/primitives/switch): an on/off
setting that applies at once. Bind it with [(checked)] or any forms
library, name it with a label[ndLabel], and put a ndSwitchThumb
inside.
Manages aria-checked, aria-required, aria-invalid.
| Switch binding | Type | Default |
|---|---|---|
[size]Size: default, or sm for dense layouts. | 'default' | 'sm' | 'default' |
[class]Extra classes, merged last so they win. | string | '' |
[defaultChecked]Initial checked when it isn't bound. | boolean | false |
[disabled]Disables it. A disabled bound field disables it too. | boolean | false |
[required]Marks it required: aria-required="true". | boolean | false |
[invalid]Marks it invalid when no forms library is bound; with one, the field's
state decides. | boolean | false |
[id]Element id; also the key a label[ndLabel] uses to find it. | string | undefined | — |
[(checked)]Whether it's on. Two-way bindable with [(checked)]. | boolean | false |
| Data attribute | Values |
|---|---|
data-slot | switch |
data-size | — |
data-state | — |
data-disabled | — |
data-invalid | — |
SwitchThumb
[ndSwitchThumb]The thumb of a Switch. It moves to the end side when on, so to
the left in right-to-left layouts.
| SwitchThumb binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | switch-thumb |
data-state | — |
Agent notes
Do
- Name it with a label that says what it controls, such as "Airplane mode".
- Apply the change as soon as it's switched.
Don't
- Don't use a switch in a form that's only applied when submitted; use a Checkbox.
- Don't change the label with the state, such as "On" and "Off".