forty-cdk
llms.txt

Primitives

Switch

A binary on / off control toggled by click, Enter or Space.

forty-cdk/switch WAI-ARIA APG

Toggle it with the pointer, Space or Enter, and watch data-state follow.

Notifications off

Headless and styleless, it doubles as a FormCheckboxControl for Angular Signal Forms. A switch changes state immediately on activation, so it is semantically distinct from a checkbox (which represents a deferred selection).

When to choose

  • Switch: immediate setting (flipping it changes the world right now). Always binary.
  • Checkbox: deferred selection (user is choosing options for a form to apply later). Supports tri-state.

Use the one that matches your semantics. ForSwitch and ForCheckbox are intentionally separate even though they share most of their state surface.

Anatomy

<button forSwitch [(checked)]="enabled">
  <span class="thumb"></span>
</button>

<!-- With Signal Forms — [formField] wires value, validity and touched: -->
<button forSwitch [formField]="settings.notifications"></button>

Examples

States

One class and one directive, three states. disabled and readonly both keep the switch focusable and announced (per APG) while interaction is a no-op; they reflect aria-disabled / data-disabled and aria-readonly / data-readonly, which is all the example's stylesheet keys on.

Default
Disabled
Read-only

Signal Forms

forSwitch implements FormCheckboxControl, so a single [formField] binding wires checked state, validity and touched both ways without a ControlValueAccessor.

I accept the privacy policy

API

ForSwitch

PropertyTypeDescription
checked
model
Two-way bindable on/off state. Required by FormCheckboxControl.
Default: —
disabled
input
Ignores click; reflects aria-disabled="true" and data-disabled. Stays focusable (per APG).
Default: —
readonly
input
Ignores click; reflects aria-readonly="true". Stays focusable.
Default: —
required
input
Reflects aria-required="true".
Default: —
invalid
input
Reflects aria-invalid="true".
Default: —
pending
input
Reflects aria-busy="true" while async validation is in flight.
Default: —
name
input
Reflects on name.
Default: —
errors
input
Validation errors fed by [formField]. The directive does not render them, since that is consumer territory.
Default: —
touched
model
Set to true on blur. Two-way so the field can read it back.
Default: —
Data attributeValues
data-statechecked | unchecked
data-disabledpresent | absent
data-readonlypresent | absent
data-touchedpresent | absent
data-dirtypresent | absent
data-pendingpresent | absent
data-invalidpresent | absent

Keyboard

KeyAction
SpaceToggle the switch.
EnterAlso toggles (a documented superset of the APG).

Both keys work on any host element. On a <button> they come from native button behavior; on any other host (<div>, <span>, or a hostDirectives wrapper's own host) the directive adds tabindex="0" and synthesizes the same activation. Space keydown always blocks page scrolling; the toggle fires on its keyup.

Accessibility

Implements the WAI-ARIA Switch pattern.

  • Prefer a <button>. The directive forces type="button" through a host binding to prevent submit-by-Enter inside a <form> (a consumer type="submit" on the host is overridden, not honoured), and Enter / Space toggle the switch via native button behavior. Any other host element (e.g. <div>) works too: it gets tabindex="0" and the same Enter / Space activation synthesized, so role="switch" is never announced on an element a keyboard user cannot reach; it gets no type attribute at all, since type is not valid there.
  • A disabled switch stays focusable (per APG): it reflects aria-disabled="true" + data-disabled="" rather than the native disabled attribute, so assistive tech still announces it while click / keyboard activation is a no-op. Form-submit exclusion is handled by the hidden <input>, not the visible button.
  • role="switch" is announced as "switch, on/off" by screen readers, distinct from "checkbox, checked/not checked".
  • @angular/forms is an optional peer. If you're not using Signal Forms, don't install it: the directive runs fine without it (only the type import from @angular/forms/signals is type-only and erased at build).

Styling

forty-cdk ships no styles: put your own class on each piece and key your CSS off the data-* attributes listed under API, not off the for* selectors (Styling forty-cdk explains why).

.switch .thumb {
  transition: transform 150ms;
}

.switch[data-state='checked'] .thumb {
  transform: translateX(100%);
}

Wrapping in a design system

Wrapping form primitives documents both supported wrapper patterns: hostDirectives with the exported FOR_SWITCH_HOST_DIRECTIVE_INPUTS / FOR_SWITCH_HOST_DIRECTIVE_OUTPUTS name tuples, and subclassing.