ToggleATM
A two-state button that can be either on or off. Uses <button> with aria-pressed. Common for toolbar formatting buttons (bold, italic) or feature toggles.
On this page (8)
§Default
Transparent at rest. Background appears on hover and when pressed.
§Outline
Visible border and subtle shadow at rest.
§With Text
Icon and label together.
§Size
The full five-step scale via data-size - the default equals md.
§Disabled
Non-interactive at 50% opacity.
§States
Named states via the shared State API, driven per instance through the bound api:
default- unpressed (the authoredaria-pressedvalue is restored bysetState('default'))pressed-aria-pressed="true", accent background
The demo element carries data-state-demo; bun run screenshots drives it via el.api.setState(name) and captures screenshots/{mode}/toggle-{state}.png.
Machine contract - verified against toggle.schema.json by bun run verify:
| State | Type | Values | Default | Description |
|---|---|---|---|---|
pressed | boolean | true, false | false | Pressed state carried by aria-pressed (runtime writes it, CSS styles it). |
§CSS view file
/* -- Toggle component ------------------------------------------ */@layer components { .toggle { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; font-size: 0.875rem; font-weight: 500; font-family: var(--font-sans); line-height: 1; white-space: nowrap; border-radius: var(--radius-md); border: 1px solid transparent; background: transparent; color: var(--muted-foreground); cursor: pointer; transition: color 150ms ease, background-color 150ms ease, box-shadow 150ms ease; outline: none; flex-shrink: 0; height: 2.25rem; padding: 0 0.5rem; min-width: 2.25rem; &:hover:not(:disabled) { background-color: var(--muted); color: var(--muted-foreground); } &[aria-pressed="true"] { background-color: var(--accent); color: var(--accent-foreground); &:hover:not(:disabled) { background-color: color-mix(in oklch, var(--accent) 85%, var(--foreground)); } } &:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; } /* disabled: dimmed, with the not-allowed cursor - no pointer-events: none, which would hide the cursor too (a disabled button fires no click) */ &:disabled { opacity: 0.5; cursor: not-allowed; } & svg { pointer-events: none; flex-shrink: 0; &:not([class*="size-"]) { width: 1rem; height: 1rem; } } /* -- Variants -------------------------------------------- */ &[data-variant="outline"] { border: 1px solid var(--input); background: transparent; box-shadow: var(--shadow-xs); &:hover:not(:disabled) { background-color: var(--accent); color: var(--accent-foreground); } &[aria-pressed="true"] { background-color: var(--accent); color: var(--accent-foreground); &:hover:not(:disabled) { background-color: color-mix(in oklch, var(--accent) 85%, var(--foreground)); } } } /* -- Sizes ----------------------------------------------- */ &[data-size="xs"] { height: 1.75rem; padding: 0 0.25rem; min-width: 1.75rem; font-size: 0.75rem; } &[data-size="sm"] { height: 2rem; padding: 0 0.375rem; min-width: 2rem; } &[data-size="md"] { height: 2.25rem; padding: 0 0.5rem; min-width: 2.25rem; } &[data-size="lg"] { height: 2.5rem; padding: 0 0.625rem; min-width: 2.5rem; } &[data-size="xl"] { height: 3rem; padding: 0 0.75rem; min-width: 3rem; font-size: 1rem; } } @media (prefers-reduced-motion: reduce) { .toggle { transition: none; } } @media (prefers-contrast: more) { .toggle { border: 2px solid transparent; &[data-variant="outline"] { border-color: var(--foreground); } &[aria-pressed="true"] { border-color: currentColor; } } } @media (forced-colors: active) { .toggle { border: 1px solid ButtonText; &[aria-pressed="true"] { background: Highlight; color: HighlightText; } &:disabled { border-color: GrayText; color: GrayText; } } }}§JavaScript view file
Single click handler toggles aria-pressed between "true" and "false".
// -- Toggle ---------------------------------------------------// Toggles aria-pressed on .toggle buttons, plus the named-state API so// agents/tests can drive the pressed state by name (AGENTS.md "State API").// Skips toggles inside .toggle-group - those are managed by toggle-group.js.// Shared preamble (AGENTS.md "State API"); the implementation lives in core.js —// build.ts rewrites this import into a df$.shadcn.shared binding in dist/.import { defussGlobals } from '../../shared/state-api.js';const df$ = defussGlobals();const toggleStates = ['default', 'pressed'];/** * UI side of setState: 'pressed' sets aria-pressed=true, 'default' restores * the authored aria-pressed value (snapshotted at init per instance). */function triggerStateChange(toggle, stateName, _config) { switch (stateName) { case 'default': toggle.setAttribute('aria-pressed', toggle._defaultPressed ?? 'false'); break; case 'pressed': toggle.setAttribute('aria-pressed', 'true'); break; }}/** Registry-level API; pass the toggle button explicitly. Unknown names throw. */export const toggleApi = { setState(toggle, stateName, config = {}) { if (!toggleStates.includes(stateName)) { throw new Error(`toggle: unknown state "${stateName}" (supported: ${toggleStates.join(', ')})`); } triggerStateChange(toggle, stateName, config); // state lives on the ELEMENT, not the module (many toggles per page) toggle.dataset.stateName = stateName; toggle._stateConfig = config; }, getState(toggle) { // reflect reality: user clicks change aria-pressed without setState() const pressed = toggle.getAttribute('aria-pressed') === 'true'; return { name: toggle.dataset.stateName || (pressed ? 'pressed' : 'default'), config: toggle._stateConfig ?? {}, }; },};df$.toggleApi = toggleApi;df$.toggleStates = toggleStates;function init() { document.querySelectorAll('.toggle:not([data-init]):not(.toggle-group .toggle)').forEach((toggle) => { toggle.dataset.init = ''; // remember the authored pressed state so setState('default') can restore it toggle._defaultPressed = toggle.getAttribute('aria-pressed') || 'false'; // bind-scope the api per instance: `$('#my-toggle').api.setState('pressed')` toggle.api = { setState: (stateName, config) => toggleApi.setState(toggle, stateName, config), getState: () => toggleApi.getState(toggle), }; toggle.addEventListener('click', () => { const pressed = toggle.getAttribute('aria-pressed') === 'true'; toggle.setAttribute('aria-pressed', !pressed); // user interaction also moves the named state (keeps getState honest) toggle.dataset.stateName = !pressed ? 'pressed' : 'default'; });});}init();new MutationObserver(init).observe(document, { childList: true, subtree: true });Comments, ideas or improvements? Edit this page's source on GitHub