Switch
An on/off toggle — three sizes, three shapes, optional glyph, accessible, in React (@bpdm/ui) and Angular (@bpdm/ng).
The Switch is an on/off toggle built on Radix. Use it for instant settings
(no Save button). It's controlled (checked + onCheckedChange) or uncontrolled
(defaultChecked); in Angular it's <bpdm-switch> with [(checked)],
[(ngModel)], or reactive forms.
Usage
import { Switch } from '@bpdm/ui/switch';
export function SwitchUsage() {
return <Switch defaultChecked aria-label="Toggle setting" />;
}import { Component } from '@angular/core';
import { BpdmSwitch } from '@bpdm/ng';
@Component({
selector: 'switch-usage',
imports: [BpdmSwitch],
templateUrl: './switch-usage.html',
})
export class SwitchUsage {}<bpdm-switch [checked]="true" aria-label="Toggle setting" />Sizes
The size prop offers sm, md (default), and lg.
import { Switch } from '@bpdm/ui/switch';
export function Sizes() {
return (
<div className="flex items-center gap-4">
<Switch size="sm" defaultChecked aria-label="Small" />
<Switch size="md" defaultChecked aria-label="Medium" />
<Switch size="lg" defaultChecked aria-label="Large" />
</div>
);
}import { Component } from '@angular/core';
import { BpdmSwitch } from '@bpdm/ng';
@Component({
selector: 'app-sizes',
standalone: true,
imports: [BpdmSwitch],
template: `
<div class="flex items-center gap-4">
<bpdm-switch size="sm" [checked]="true" aria-label="Small" />
<bpdm-switch size="md" [checked]="true" aria-label="Medium" />
<bpdm-switch size="lg" [checked]="true" aria-label="Large" />
</div>
`,
})
export class SizesComponent {}Shapes
The shape prop offers pill (default), square (rounded), and sharp.
import { Switch } from '@bpdm/ui/switch';
export function Shapes() {
return (
<div className="flex items-center gap-4">
<Switch shape="pill" defaultChecked aria-label="Pill" />
<Switch shape="square" defaultChecked aria-label="Square" />
<Switch shape="sharp" defaultChecked aria-label="Sharp" />
</div>
);
}import { Component } from '@angular/core';
import { BpdmSwitch } from '@bpdm/ng';
@Component({
selector: 'app-shapes',
standalone: true,
imports: [BpdmSwitch],
template: `
<div class="flex items-center gap-4">
<bpdm-switch shape="pill" [checked]="true" aria-label="Pill" />
<bpdm-switch shape="square" [checked]="true" aria-label="Square" />
<bpdm-switch shape="sharp" [checked]="true" aria-label="Sharp" />
</div>
`,
})
export class ShapesComponent {}With icon
Set icon to render a ✓ / ✗ glyph inside the thumb — handy when the on/off state
needs to read at a glance.
import { Switch } from '@bpdm/ui/switch';
export function WithIcon() {
return (
<div className="flex items-center gap-4">
<Switch icon defaultChecked aria-label="On" />
<Switch icon aria-label="Off" />
<Switch icon size="lg" defaultChecked aria-label="Large on" />
</div>
);
}import { Component } from '@angular/core';
import { BpdmSwitch } from '@bpdm/ng';
@Component({
selector: 'app-with-icon',
standalone: true,
imports: [BpdmSwitch],
template: `
<div class="flex items-center gap-4">
<bpdm-switch icon [checked]="true" aria-label="On" />
<bpdm-switch icon aria-label="Off" />
<bpdm-switch icon size="lg" [checked]="true" aria-label="Large on" />
</div>
`,
})
export class WithIconComponent {}States
Off, on, and disabled (on or off).
import { Switch } from '@bpdm/ui/switch';
export function States() {
return (
<div className="flex items-center gap-4">
<Switch aria-label="Off" />
<Switch defaultChecked aria-label="On" />
<Switch disabled aria-label="Disabled" />
<Switch disabled defaultChecked aria-label="Disabled and on" />
</div>
);
}import { Component } from '@angular/core';
import { BpdmSwitch } from '@bpdm/ng';
@Component({
selector: 'app-states',
standalone: true,
imports: [BpdmSwitch],
template: `
<div class="flex items-center gap-4">
<bpdm-switch aria-label="Off" />
<bpdm-switch [checked]="true" aria-label="On" />
<bpdm-switch disabled aria-label="Disabled" />
<bpdm-switch disabled [checked]="true" aria-label="Disabled and on" />
</div>
`,
})
export class StatesComponent {}With a label
Link a <label> to the switch by id (htmlFor in React, for in Angular) so
clicking the text toggles it and screen readers announce the control's name.
import { Switch } from '@bpdm/ui/switch';
export function AirplaneToggle() {
return (
<div className="flex items-center gap-3">
<Switch id="airplane" defaultChecked />
<label htmlFor="airplane" className="text-sm font-medium">
Airplane mode
</label>
</div>
);
}import { Component } from '@angular/core';
import { BpdmSwitch } from '@bpdm/ng';
@Component({
selector: 'app-labelled-switch',
standalone: true,
imports: [BpdmSwitch],
template: `
<div class="flex items-center gap-3">
<bpdm-switch id="airplane" [checked]="true" />
<label for="airplane" class="text-sm font-medium">Airplane mode</label>
</div>
`,
})
export class LabelledSwitchComponent {}Settings list
A common real-world layout — one labelled row per toggle.
import { Switch } from '@bpdm/ui/switch';
const settings = [
{ id: 's-2fa', label: 'Two-factor authentication', on: true },
{ id: 's-email', label: 'Email notifications', on: false },
{ id: 's-beta', label: 'Beta features', on: true },
];
export function SettingsList() {
return (
<div className="flex flex-col divide-y rounded-lg border">
{settings.map((s) => (
<div key={s.id} className="flex items-center justify-between gap-4 px-4 py-3">
<label htmlFor={s.id} className="text-sm">
{s.label}
</label>
<Switch id={s.id} defaultChecked={s.on} />
</div>
))}
</div>
);
}import { Component } from '@angular/core';
import { BpdmSwitch } from '@bpdm/ng';
@Component({
selector: 'settings-list',
imports: [BpdmSwitch],
templateUrl: './switch-settings.html',
})
export class SettingsList {
readonly settings = [
{ id: 's-2fa', label: 'Two-factor authentication', on: true },
{ id: 's-email', label: 'Email notifications', on: false },
{ id: 's-beta', label: 'Beta features', on: true },
];
}<div class="flex flex-col divide-y rounded-lg border">
@for (s of settings; track s.id) {
<div class="flex items-center justify-between gap-4 px-4 py-3">
<label [attr.for]="s.id" class="text-sm">{{ s.label }}</label>
<bpdm-switch [id]="s.id" [checked]="s.on" />
</div>
}
</div>Controlled value
Drive the state yourself with checked + onCheckedChange (React) or two-way
[(checked)] / [(ngModel)] (Angular).
import { useState } from 'react';
import { Switch } from '@bpdm/ui/switch';
export function WifiToggle() {
const [on, setOn] = useState(true);
return (
<div className="flex items-center gap-3">
<Switch id="wifi" checked={on} onCheckedChange={setOn} />
<label htmlFor="wifi" className="text-sm font-medium">
Wi-Fi is {on ? 'on' : 'off'}
</label>
</div>
);
}import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BpdmSwitch } from '@bpdm/ng';
@Component({
selector: 'app-controlled-switch',
standalone: true,
imports: [FormsModule, BpdmSwitch],
// [(checked)] shown here; [(ngModel)] / reactive forms work the same way
template: `<bpdm-switch [(checked)]="on" aria-label="Notifications" />`,
})
export class ControlledSwitchComponent {
on = true;
}API
| Prop | Type | Default | Description |
|---|---|---|---|
checked / defaultChecked | boolean | — | Controlled / uncontrolled on state. |
onCheckedChange | (checked: boolean) => void | — | React — fires on toggle. Angular uses [(checked)] / [(ngModel)]. |
size | sm | md | lg | md | Track and thumb size. |
shape | pill | square | sharp | pill | Corner style of the track and thumb. |
icon | boolean | false | Show a ✓ / ✗ glyph inside the thumb. |
disabled | boolean | false | Disable the control. |
id | string | — | Control id (label association / testing), forwarded to the switch. |
aria-label / aria-labelledby / aria-describedby | string | — | Accessible name / description, forwarded to the switch. |
In React, all native Radix Switch props (name, value, required, id, aria-*,
data-*, …) are spread onto the control and the ref is forwarded. In Angular,
size / shape / icon / id / aria-label / aria-labelledby / aria-describedby
are @Inputs on <bpdm-switch>, which is a ControlValueAccessor ([(ngModel)] /
reactive forms) with a standalone [(checked)].
Accessibility
- A true
role="switch"witharia-checkedreflecting state and full keyboard support (Space / Enter toggle; Tab focuses with a visible ring). - Give it an accessible name — link a
<label>byid(htmlForin React,forin Angular), or passaria-label/aria-labelledby(forwarded to the control in both frameworks). - The thumb glyph (
icon) isaria-hiddenand decorative — state is conveyed viaaria-checked, never the glyph alone.
Internationalization
- The switch renders no text of its own — pass a translated accessible name
(
aria-label/ associated<label>) and translated help viaaria-describedby. There's nothing hard-coded to override. - RTL-safe: in a
dir="rtl"context the thumb slides to the left when on (the checked-state translate is mirrored), so the motion reads correctly right-to-left. Works from the ambientdirin both frameworks. See the Internationalization guide.