Skip to main content
cngx-src documentation

CngxToggle

cngx-toggle

ComponentPrimaryOnPushNo encapsulationv0.1.0WCAG AA

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>

https://cngxjs.github.io/cngx/examples/#/common/interactive/toggle/basic-two-way-binding https://cngxjs.github.io/cngx/examples/#/common/interactive/toggle/custom-thumb-glyph https://cngxjs.github.io/cngx/examples/#/common/interactive/toggle/disabled-with-reason https://cngxjs.github.io/cngx/examples/#/common/interactive/toggle/label-position https://cngxjs.github.io/cngx/examples/#/forms/field/form-primitives/coming-in-a-follow-up https://cngxjs.github.io/cngx/examples/#/forms/field/form-primitives/reactive-forms-same-atom-just-bind-formcontrol https://cngxjs.github.io/cngx/examples/#/forms/field/form-primitives/signal-forms-drop-the-atom-into-cngx-form-field

Metadata#

Host#

Providers#

CNGX_CONTROL_VALUE
useExisting CngxToggle
CNGX_FORM_FIELD_CONTROL
useExisting CngxToggle

Relationships

Index#

Inputs#

disabled#boolean
model()
default false
disabledReason#string
input()
default ''
errorMessageId#string | null
input()

Optional 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 null skips the attribute entirely. Note: WAI-ARIA dictates that AT ignores this attribute when aria-invalid is absent or "false", so a stable always-emitted id is harmless when the field is valid.
default null
invalid#boolean
model()

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.

default false
labelPosition#"before" | "after"
input()
default 'after'
thumbGlyph#TemplateRef | null
default null
model()
default false

Outputs#

disabled#boolean
model()
invalid#boolean
model()

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.

model()

Instance Properties#

describedId#unknown
ProtectedReadonly
nextUid('cngx-toggle-desc')
focused#unknown
Readonly

Whether the host element currently has DOM focus.

this.focusedState.asReadonly()
hasProjectedLabel#unknown
ProtectedReadonly

True 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()
id#unknown
signal()Readonly

Stable per-instance id used for <label for> wiring.

signal(nextUid('cngx-toggle-')).asReadonly()
labelId#unknown
ProtectedReadonly
nextUid('cngx-toggle-label')

Methods#

handleClick#void
Protected
handleFocusIn#void
Protected
handleFocusOut#void
Protected
handleKeydown#void
Protected
handleKeydown(event: Event)
@parameventEvent

HostBindings#

BindingExpression
[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#

EventHandler
(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-on and translates the thumb by --cngx-toggle-thumb-travel
  • --disabled - cursor: not-allowed + opacity dim via --cngx-toggle-disabled-opacity
  • --label-before - flex-direction: row-reverse so 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 to currentColor)

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"]
  • .dark class

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

--cngx-toggle-gap#*
Default value 0.5rem

Gap between the track and the label slot. Falls back to --cngx-space-sm.

--cngx-toggle-track-width#*
Default value 2.25rem

Width of the track pill.

--cngx-toggle-track-height#*
Default value 1.25rem

Height of the track pill.

--cngx-toggle-track-radius#<length>
Default value 9999px

Corner radius of the track. Defaults to a full pill.

--cngx-toggle-thumb-inset#*
Default value 0.125rem

Inset of the thumb inside the track (resting position).

--cngx-toggle-thumb-size#*
Default value 1rem

Diameter of the circular thumb.

Surface

--cngx-toggle-color#*
Default value currentColor

Text color of the host shell. Defaults to currentColor.

--cngx-toggle-thumb-bg#<color>
Default value oklch(1 0 0)

Background of the thumb.

--cngx-toggle-thumb-shadow#*
Default value 0 1px 2px oklch(0 0 0 / 0.25)

Drop-shadow applied to the thumb so it lifts off the track.

State / Disabled

--cngx-toggle-disabled-opacity#<number>
Default value 0.5

Opacity multiplier applied by .cngx-toggle--disabled.

State / Focus

--cngx-toggle-focus-outline#*
Default value 2px solid currentColor

Focus-ring outline shorthand.

--cngx-toggle-focus-offset#*
Default value 2px

Outline offset of the focus ring.

--cngx-toggle-focus-radius#*
Default value 0.25rem

Corner radius applied while focused.

Motion

--cngx-toggle-transition#*
Default value 150ms ease

Transition duration + easing for the track/thumb animation.

State / Off

--cngx-toggle-track-bg-off#<color>
Default value 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

--cngx-toggle-track-bg-on#<color>
Default value oklch(0.66 0.19 50)

Background of the on-state track. Falls back to --cngx-color-primary.

See: [[--cngx-color-primary]]

--cngx-toggle-thumb-travel#*
Default value 1rem

Horizontal translation applied to the thumb when checked.