bpdm/ui
Form

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

switch-usage.tsx
import { Switch } from '@bpdm/ui/switch';

export function SwitchUsage() {
  return <Switch defaultChecked aria-label="Toggle setting" />;
}
switch-usage.ts
import { Component } from '@angular/core';
import { BpdmSwitch } from '@bpdm/ng';

@Component({
  selector: 'switch-usage',
  imports: [BpdmSwitch],
  templateUrl: './switch-usage.html',
})
export class SwitchUsage {}
switch-usage.html
<bpdm-switch [checked]="true" aria-label="Toggle setting" />

Sizes

The size prop offers sm, md (default), and lg.

sm
md
lg
switch-sizes.tsx
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>
  );
}
switch-sizes.component.ts
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.

pill
square
sharp
switch-shapes.tsx
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>
  );
}
switch-shapes.component.ts
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.

on
off
large
switch-icon.tsx
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>
  );
}
switch-icon.component.ts
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).

off
on
disabled
disabled on
switch-states.tsx
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>
  );
}
switch-states.component.ts
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.

switch-label.tsx
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>
  );
}
switch-label.component.ts
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.

switch-settings.tsx
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>
  );
}
switch-settings.ts
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 },
  ];
}
switch-settings.html
<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).

switch-controlled.tsx
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>
  );
}
switch-controlled.component.ts
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

PropTypeDefaultDescription
checked / defaultCheckedbooleanControlled / uncontrolled on state.
onCheckedChange(checked: boolean) => voidReact — fires on toggle. Angular uses [(checked)] / [(ngModel)].
sizesm | md | lgmdTrack and thumb size.
shapepill | square | sharppillCorner style of the track and thumb.
iconbooleanfalseShow a ✓ / ✗ glyph inside the thumb.
disabledbooleanfalseDisable the control.
idstringControl id (label association / testing), forwarded to the switch.
aria-label / aria-labelledby / aria-describedbystringAccessible 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" with aria-checked reflecting state and full keyboard support (Space / Enter toggle; Tab focuses with a visible ring).
  • Give it an accessible name — link a <label> by id (htmlFor in React, for in Angular), or pass aria-label / aria-labelledby (forwarded to the control in both frameworks).
  • The thumb glyph (icon) is aria-hidden and decorative — state is conveyed via aria-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 via aria-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 ambient dir in both frameworks. See the Internationalization guide.

On this page