On this page
Choices

Switch released

A setting that takes effect the moment it is flipped, with no Save button beside it.

  • Import@pacit/components/switch
  • Selectorpct-switch

Implements the W3C ARIA APG Switch pattern ↗

Turns off when you leave the house

switch.demo.ts — the same file runs here and is highlighted under Code

Start here

Usage

One import per component, and the shortest possible use — the package itself is installed once, on the start page.

Import this component
import { PctSwitch } from '@pacit/components/switch';
ExportedWhat it is
PctSwitchcomponentpct-switchSwitch. A setting that takes effect the moment it is moved, drawn as a track the thumb travels along — the ARIA APG's Switch pattern, built on the same native <input type="checkbox"> as PctCheckbox and carrying role="switch" over it.
<pct-switch label="Wi-Fi" [(checked)]="wifi" />
Examples

Every state, running

Each example is the component itself, not a screenshot. Open the code under any of them.

On, off, and out of reach

A switch takes effect at once — there is no Save behind it — so a disabled one says "this setting is not yours here" rather than "fill this in later".

A sound and a badge
Managed by your organisation
API

What it reads, and what it writes back

Read from the source at build time — the types, the defaults and the lines of documentation are the library's own.

unreleased marks 1 of them: on main, and not in @pacit/components@0.2.0 — the next release names the version.

Switch. A setting that takes effect the moment it is moved, drawn as a track the thumb travels along — the ARIA APG's Switch pattern, built on the same native <input type="checkbox"> as PctCheckbox and carrying role="switch" over it.

Inputs

NameTypeWhat it does
checkedtwo-waydefault falsebooleanOn/off — the only required field of the FormCheckboxControl contract.
disableddefault falsebooleanBlocks the control and greys it — the native disabled, so it leaves the tab order as well. With [formField] the directive writes it, as it writes every input of the FormUiControl contract.
readonlydefault falsebooleanThe switch is a native checkbox, which has no readonly, so the click is swallowed here instead; the control stays focusable, unlike under disabled, and aria-readonly says so.
invaliddefault falsebooleanThe form's verdict; shown only once touched, so an empty form does not open red.
toucheddefault falsebooleanWhether the user has left the field once; with invalid it gates the error face.
requireddefault falsebooleanMarks the label with the required sign; with [formField] it follows the schema's required().
errorsdefault []readonly ValidationError.WithOptionalFieldTree[]The form's validation errors; the first one's message takes the hint's place once the field is touched.
warningsunreleaseddefault undefinedreadonly ValidationError.WithOptionalFieldTree[] | undefinedA verdict without a veto (0087): shown after the error and before the hint once the field is touched, in the warning tone, with aria-invalid untouched. Left unbound, the control reads what pctWarn() wrote for its own [formField]; bound — [] included — the list given stands.
namedefault ''stringThe native name — what a form submission calls the value.
labeldefault ''stringThe visible label, rendered by the control itself when it stands outside a pct-field; inside one, the field's label is the name.
hintdefault ''stringA line of help under the control, outside a pct-field; the first error message takes its place while the field is invalid and touched.
ariaLabeldefault ''stringThe accessible name of a switch with no visible label — an INPUT rather than an aria-label written on the tag, for the same reason as on the checkbox: the role sits on the <input> inside, the host has no role at all, and an ARIA name on a roleless element is ignored (req-a11y-built-in).
ariaLabelledbydefault ''stringAs ariaLabel, for a name that already stands somewhere on the page.

Outputs

NamePayloadWhen it fires
touchvoidEmitted on blur — lets the form mark the field as touched.

What it writes on the element

The hooks a stylesheet or a test can hold. Stable by contract — the parts inventory gate fails the build when one disappears.

AttributeBound toNote
classpct-switch
data-pct-checkedchecked() ? "" : null
data-pct-invalidshowInvalid() ? "" : null
data-pct-warningwarningLit() ? "" : null
data-pct-disableddisabled() ? "" : null
data-pct-in-fieldinField ? "" : null
Styling

Parts to select, tokens to override

Two ways in, both stable. No class names to depend on.

Parts

In a test, PctSwitchHarness from @pacit/components/testing reads a part by the name below and a state by its attribute — the harness gate holds the names to the package.

PartSelectorWhat it is
control[data-pct-part="control"]the native input
error[data-pct-part="error"]the message when invalid
hint[data-pct-part="hint"]the hint under the label
label[data-pct-part="label"]the label beside the switch
thumb[data-pct-part="thumb"]the knob — painted over the control, and it lets the pointer through to it
track[data-pct-part="track"]the pill the thumb travels
warning[data-pct-part="warning"]the message when the value is allowed and suspect — after the error, before the hint (0087)

Tokens

Every value the component reads, with the light and the dark default. Override a token at any scope and it cascades like any custom property.

TokenStyles
--pct-switch-fg-disabled{pct.text-disabled}#64748bText colour of the switch while disabled.
--pct-switch-fg-invalid{pct.danger}#b91c1c · #f87171Text colour of the switch when invalid.
--pct-switch-fg-warning{pct.warning}#b45309 · #fbbf24Text colour of the warning line (0087); the track keeps its colour, a warning being no fault of the control.
--pct-switch-gap{pct.space.3}8pxGap between the switch and its label.
--pct-switch-height24pxHeight of the switch.
--pct-switch-hint-fg{pct.text-muted}#334155 · #e2e8f0Text colour of the hint.
--pct-switch-label-fg{pct.text}#0f172a · #ffffffText colour of the label.
--pct-switch-padding-x3pxHorizontal padding of the switch.
--pct-switch-radius999pxCorner radius of the switch.
--pct-switch-target-min{pct.target.min}24pxTouch-target floor of the switch.
--pct-switch-thumb-bg{pct.border-strong}#64748bBackground of the thumb.
--pct-switch-thumb-bg-checked{pct.on-primary}#ffffff · #0f172aBackground of the thumb when checked.
--pct-switch-thumb-bg-disabled{pct.text-disabled}#64748bBackground of the thumb while disabled.
--pct-switch-thumb-size16pxSize of the thumb.
--pct-switch-track-bg{pct.surface}#ffffff · #0f172aBackground of the track.
--pct-switch-track-bg-checked{pct.primary}#2563eb · #60a5faBackground of the track when checked.
--pct-switch-track-bg-disabled{pct.surface-disabled}#f1f5f9 · #1e293bBackground of the track while disabled.
--pct-switch-track-border{pct.border-strong}#64748bEdge colour of the track.
--pct-switch-track-border-checked{pct.primary}#2563eb · #60a5faEdge colour of the track when checked.
--pct-switch-track-border-disabled{pct.border}#e2e8f0 · #334155Edge colour of the track while disabled.
--pct-switch-track-border-hover{pct.text-muted}#334155 · #e2e8f0Edge colour of the track on hover.
--pct-switch-track-border-invalid{pct.danger}#b91c1c · #f87171Edge colour of the track when invalid.
--pct-switch-width40pxWidth of the switch.

Theming

A brand override in a few declarations. Scope it to a theme attribute, a page or one container — the component never notices.

[data-theme='brand'] {
  --pct-switch-track-bg-checked: #0f766e;
  --pct-switch-thumb-bg: #ffffff;
}
Turns off when you leave the house
Accessibility

Measured, not assumed

Pattern: Switch — the role="switch" variant built on a native checkbox, which the APG's own HTML example is

Keyboard

keyeffecttest
Spacetogglenative <input type="checkbox">

No handling of its own (req-api-platform) — and that is the point of the native road: the APG lists Space for the pattern and the platform already implements it. readonly blocks the change without losing focusability.

What is measured, and what is not yet

Every line is a test in three engines or a gate on every commit. The gaps are listed the same way, each with the requirement that will close it.

MeasuredARIA APG pattern in the JSDoclibs/components/switch/src/switch.ts — the pattern is named and linked in the class comment
MeasuredKeyboard mapapps/sandbox-e2e/src/switch.spec.ts — Space measured in three engines, because "the platform does it" is a claim about three of them
Measuredaxe auditapps/sandbox-e2e/src/a11y.spec.ts — the /switch view and /all, in both directions
MeasuredVisual screenshotapps/sandbox-e2e/src/visual.spec.ts — switch-states and switch-states-rtl
Measuredforced-colors: activeapps/sandbox-e2e/src/forced-colors.spec.ts — the state is the thumb's position, so the two states are deliberately the same colour after the swap and the case asserts exactly that; plus the disabled row, which says GrayText like the rest
Measuredprefers-reduced-motionapps/sandbox-e2e/src/preferences.spec.ts — the thumb's travel is a transition, so it joins the cross-control case
MeasuredTouch target ≥ 24×24 pxapps/sandbox-e2e/src/switch.spec.ts — the track is 40×24 and carries the floor itself (max(…, --pct-switch-target-min)), so what a finger gets is what an eye sees less the 1 px border, at every size; the checkbox needs the other technique because its box is 18 px (lesson-14). The knob painted over the control is no hole in that area: it takes no pointer events, and the same spec presses its centre and watches the state turn — before that line the one part that looked most like the thing to press was the one part that did nothing (lesson-158)
n/aSize axisnot applicable — the bare variant does not align heights (0004)
GapDensity axisnone — gap. Elevated risk: the track is exactly at the touch threshold, so density has nowhere to go down
MeasuredRTLapps/sandbox-e2e/src/switch.spec.ts — the thumb's travel measured as geometry in both directions, plus the switch-states-rtl baseline
MeasuredSSR + hydrationapps/sandbox-e2e/src/hydration.spec.ts — the /switch view is in SBX_ROUTES
MeasuredFormslibs/components/switch/src/switch.spec.ts — signal forms, [formControl] and [(ngModel)]; a form's reset — reset() keeping the value, reset(value) showing it, the form pristine: libs/components/switch/src/switch.spec.ts › signal forms
MeasuredMessage announcedtools/check-aria.mjs (point 7, target check-aria) — the error part carries role="alert", over every template of the library
MeasuredParts in the inventorylibs/components/parts.snapshot.md, tools/check-parts.mjs (target check-parts)
MeasuredTokens + contrast.policy.jsonlibs/tokens/src/contrast.policy.json — 13 pairs, track/thumb added to the name dictionary
MeasuredStrings through PCT_TEXTStools/check-texts.mjs — no strings of its own
MeasuredSize budgetlibs/components/size.snapshot.md, tools/check-bundle.mjs (target check-bundle)
MeasuredScreen-reader logRead 2026-09-28 from docs/acr/at/ — three readers, whole sandbox. All three give the role and the hint; they disagree about the words for the state. NVDA Wi-Fi, switch, on, Turns off when you leave the house; VoiceOver Backups Runs every night at 03:00 required invalid data off switch; Orca Backups · switch not pressed. · Runs every night at 03:00. — pressed/not pressed, the toggle-button vocabulary, where the other two say on and off. And the READ-ONLY switch is switch, on to NVDA — aria-readonly is on the control and the reader does not say it, as with the checkbox; VoiceOver left this view after one stop and did not reach it
Measureddocs page/components/switch on the published site — prerendered, the demo's own source is the code tab
Evidence

What the repository holds for this component

Read at build time from the tracked snapshots, not typed here.

96.77%mutation score60 of 62 mutants killed
7 × 3e2e cases × enginesplus the shared audits: axe, forced colors, preferences, size, hydration
14colour pairs measuredlight and dark, at every token build
2visual baselinesChromium, the sandbox's own pictures
7elements in the preview4 listeners · 1 render to settle · 3210 µs in jsdom, held exactly but for the clock

Decisions behind it

  • 0039A state the platform publishes is not ours to write
  • 0005Signal forms without ControlValueAccessor
  • 0003Wrapper and control
  • lesson-12FormCheckboxControl requires checked, not value
  • lesson-112An ARIA attribute over a native state is written by us and read by nobody
  • lesson-14Formal conformance does not mean good quality
  • lesson-158A box painted over a transparent control is the box the pointer lands on

Known limitations

  • No third state. ARIA gives switch true and false and nothing else, so there is no indeterminate here — and no audit would have said so, which is why the absence of the input is the gate rather than a rule somebody has to remember.
  • No on/off text inside the track. A word drawn there would have to be aria-hidden (the role already says the state) and would need a string per language for something the position already tells the eye. It is a limitation rather than a decision: a consumer who wants it today has to draw it themselves.