Primitives
Switch
A binary on / off control toggled by click, Enter or Space.
Toggle it with the pointer, Space or Enter, and watch data-state follow.
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.
Signal Forms
forSwitch implements FormCheckboxControl, so a single [formField] binding wires checked state, validity and touched both ways without a ControlValueAccessor.
API
ForSwitch
| Property | Type | Description |
|---|---|---|
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 attribute | Values | |
|---|---|---|
data-state | checked | unchecked | |
data-disabled | present | absent | |
data-readonly | present | absent | |
data-touched | present | absent | |
data-dirty | present | absent | |
data-pending | present | absent | |
data-invalid | present | absent |
Keyboard
| Key | Action | |
|---|---|---|
Space | Toggle the switch. | |
Enter | Also 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 forcestype="button"through a host binding to prevent submit-by-Enter inside a<form>(a consumertype="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 getstabindex="0"and the same Enter / Space activation synthesized, sorole="switch"is never announced on an element a keyboard user cannot reach; it gets notypeattribute at all, sincetypeis not valid there. - A disabled switch stays focusable (per APG): it reflects
aria-disabled="true"+data-disabled=""rather than the nativedisabledattribute, 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/formsis 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/signalsis 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.