Headless switch
Headless switch keeps the interaction surface as a real <button> with role="switch". You own the surrounding layout, the thumb markup, and the label treatment while TailNG provides the checked, disabled, and focus-visible state contract.
checkedanddata-statestay in sync for visual and ARIA state.disabledblocks interaction and exposes a clean styling hook.ariaLabelgives the switch an accessible name when visible text lives outside the button.
Installation
Import the primitive when you want TailNG to own only the switch semantics.
Primitive import
ts
import { TngSwitch } from '@tailng-ui/primitives';
Basic usage
The primitive attaches to button[tngSwitch]. When the visible copy is separate from the button, provide ariaLabel so screen readers hear the same label.
Minimal attachment
html
<button
tngSwitch
[checked]="releaseReady()"
ariaLabel="Release ready"
(click)="releaseReady.update(value => !value)"
>
<span class="switch-thumb" aria-hidden="true"></span>
</button>
Recommended labeled row
html
<div class="headless-switch-row">
<button
tngSwitch
class="headless-switch-row__control"
[checked]="releaseReady()"
ariaLabel="Release ready"
(click)="releaseReady.update(value => !value)"
>
<span class="headless-switch-row__thumb" aria-hidden="true"></span>
</button>
<div class="headless-switch-row__copy">
<span class="headless-switch-row__title">Release ready</span>
<span class="headless-switch-row__meta">Ship after QA sign-off.</span>
</div>
</div>
Style variants
The same primitive switch behavior can sit inside a plain CSS shell or a Tailwind utility shell without changing the API.
Release switch shell (Plain-CSS)
Release ready
Release switch shell (Tailwind CSS)
Auto publishShip after review approval.
Accessibility baseline
role="switch"andaria-checkedare handled by the directive.- Use
ariaLabelwhenever the visible copy is not inside the button itself. - The primitive keeps keyboard focus on the button, so
:focus-visiblestyles stay simple.