CngxToggle
cngx-toggle
projects/common/interactive/toggle/toggle.component.ts
Import#
import { CngxToggle } from '@cngx/common/interactive'
Description#
Single-value boolean switch with W3C role="switch" semantics. Click,
Space, and Enter all flip value; the directive form ([cngxToggle])
applies the contract to a host element that owns its own markup, while
the element form (<cngx-toggle>) renders a track + thumb skin and
accepts label content via <ng-content>.
value is a ModelSignal<boolean> - consumers two-way-bind via
[(value)] and the directive emits valueChange on every flip.
disabled is also a ModelSignal<boolean> so the
CngxFormBridge (Phase 7) can drive it via setDisabledState
without a separate write path; consumers usually only read or set
one direction.
disabledReason is consumer-supplied free text (no library default
per feedback_en_default_locale) - when present it lands in a hidden
<span> referenced by the host's aria-describedby. Per Pillar 2,
the description span is always in the DOM (visibility toggles via
aria-hidden); the host's aria-describedby is only emitted when a
reason is set so AT does not announce an empty description.
Typical usage prefers the <div>/<cngx-toggle> element form because
role="switch" on a native <button> would conflict with the
browser's own Space → click synthesis and produce double-toggles.
<cngx-toggle [(value)]="emailNotifications">
E-mails empfangen
</cngx-toggle>
<div
cngxToggle
[(value)]="dark"
[disabled]="systemPreferenceLocked()"
disabledReason="Locked by your OS preference"
>Dark mode</div><ng-template #icon><span aria-hidden="true">★</span></ng-template>
<cngx-toggle [(value)]="featured" [thumbGlyph]="icon">Featured</cngx-toggle>Metadata#
Host#
Providers#
CNGX_CONTROL_VALUE- useExisting
CngxToggle CNGX_FORM_FIELD_CONTROL- useExisting
CngxToggle
Relationships
Index#
Derived State
HostBindings
HostListeners
Inputs#
string | nullOptional id of an external error message element (e.g. a sibling
rendered by <cngx-form-field> or a consumer-owned <span>).
When set, the host emits aria-errormessage="<id>" so AT can
locate the message; consumers MUST render an element with that id
- passing an id without a matching element produces a dangling
AT reference. Default
nullskips the attribute entirely. Note: WAI-ARIA dictates that AT ignores this attribute whenaria-invalidis absent or"false", so a stable always-emitted id is harmless when the field is valid.
nullBridge-writable invalid state. model<boolean> mirrors disabled
so external integrations (RF/Signal-Forms bridges, custom validity
adapters) can drive it without a parallel API path - consumers
typically read only.
falseTemplateRef | null nullOutputs#
Bridge-writable invalid state. model<boolean> mirrors disabled
so external integrations (RF/Signal-Forms bridges, custom validity
adapters) can drive it without a parallel API path - consumers
typically read only.
Instance Properties#
unknownWhether the host element currently has DOM focus.
this.focusedState.asReadonly()unknownTrue when static content is projected into the label span. Gates
aria-labelledby: Chrome's name-from-contents for role="switch"
does not recurse into the nested .cngx-toggle__label, so a
projected label would otherwise yield an empty accessible name
(Pillar 2). Emitting aria-labelledby only when a label is present
leaves an icon-only toggle's consumer aria-label in force -
pointing labelledby at an empty span would blank the name instead.
this.hasProjectedLabelState.asReadonly()unknownStable per-instance id used for <label for> wiring.
signal(nextUid('cngx-toggle-')).asReadonly()Methods#
HostBindings#
| Binding | Expression |
|---|---|
[attr.id] | id() |
[attr.aria-checked] | value() ? "true" : "false" |
[attr.aria-disabled] | disabled() ? "true" : null |
[attr.aria-invalid] | (invalid() || errorState()) ? "true" : null |
[attr.aria-errormessage] | errorMessageId() |
[attr.aria-labelledby] | hasProjectedLabel() ? labelId : null |
[attr.aria-describedby] | describedById() |
[attr.tabindex] | disabled() ? -1 : 0 |
[class.cngx-toggle--checked] | value() |
[class.cngx-toggle--disabled] | disabled() |
[class.cngx-toggle--label-before] | labelPosition() === "before" |
HostListeners#
| Event | Handler |
|---|---|
(click) | click() |
(keydown.space) | keydown.space() |
(keydown.enter) | keydown.enter() |
(focusin) | focusin() |
(focusout) | focusout() |
Default visuals for CngxToggle. The host carries .cngx-toggle
and renders a .cngx-toggle__track pill with a circular
.cngx-toggle__thumb inside, plus a .cngx-toggle__label slot for
the projected label and a .cngx-toggle__sr-only slot for
assistive-only copy. A projected [thumbGlyph] child drops the
default white thumb so the consumer's icon stands alone.
State modifiers
--checked- swaps the track to--cngx-toggle-track-bg-onand translates the thumb by--cngx-toggle-thumb-travel--disabled-cursor: not-allowed+ opacity dim via--cngx-toggle-disabled-opacity--label-before-flex-direction: row-reverseso the label reads before the track
:focus-visible applies the focus ring via
--cngx-toggle-focus-outline / --cngx-toggle-focus-offset /
--cngx-toggle-focus-radius. prefers-reduced-motion: reduce
disables the track + thumb transitions.
Slots
.cngx-toggle__track- the pill background.cngx-toggle__thumb- the sliding indicator (custom glyph drops the default white circle via:has(*)).cngx-toggle__label- projected label content.cngx-toggle__sr-only- visually-hidden copy for AT
Inheritance
Layout and surface tokens cascade through the foundation; the
on-state track delegates to the semantic primary token so a brand
override in @layer cngx.theme propagates:
--cngx-toggle-gap->--cngx-space-sm--cngx-toggle-focus-radius->--cngx-radius-sm--cngx-toggle-track-radius->--cngx-radius-pill--cngx-toggle-track-bg-on->--cngx-color-primary--cngx-toggle-color- direct (defaults tocurrentColor)
Dark mode
Three hooks flip the off-state track from oklch(0 0 0 / 0.25) to
oklch(1 0 0 / 0.25) so the unchecked track still reads as a muted
pill against a dark page:
prefers-color-scheme: dark[data-color-scheme="dark"].darkclass
A matching [data-color-scheme="light"] / .light block pins the
light value so explicit-light overrides win over the media query.
Pair with
@cngx/themes/material/interactive-controls-theme- Material 3 switch surface
Index#
Layout
State / Disabled
Motion
State / Off
Layout
*0.5remGap between the track and the label slot. Falls back to
--cngx-space-sm.
<length>9999pxCorner radius of the track. Defaults to a full pill.
*0.125remInset of the thumb inside the track (resting position).
Surface
*currentColorText color of the host shell. Defaults to currentColor.
*0 1px 2px oklch(0 0 0 / 0.25)Drop-shadow applied to the thumb so it lifts off the track.
State / Disabled
<number>0.5Opacity multiplier applied by .cngx-toggle--disabled.
State / Focus
Motion
*150ms easeTransition duration + easing for the track/thumb animation.
State / Off
<color>oklch(0 0 0 / 0.25)Background of the off-state track - neutral muted by default.
inherits: true so the :root dark-mode override reaches the
track inside the toggle host.
State / On
<color>oklch(0.66 0.19 50)Background of the on-state track. Falls back to
--cngx-color-primary.
See: [[--cngx-color-primary]]
*1remHorizontal translation applied to the thumb when checked.