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 (9)
§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). |
§API
Generated from toggle.ts and the shared State API - the descriptions are their JSDoc, the types are checked by the compiler.
States
type ToggleState = 'default' | 'pressed' - setState(name, config) takes the config of the state it names.
| State | Description |
|---|---|
default | Not pressed (aria-pressed="false"). No config. |
pressed | Pressed (aria-pressed="true"). No config. |
Every element
| Member | Description | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|
el.api.setState<S extends ToggleState>(name: S, config?: ToggleStateConfigs[S]): unknown | Enter a state: the DOM work runs (also when it is the current state), the store records it.
Returns | |||||||||
el.api.getState(): { name: ToggleState; config: ToggleStateConfigs[ToggleState]; model?: ElementModel } | The state the element shows now - read back from the DOM, so it includes what the user changed. Returns | |||||||||
el.api.render(state?: { name: ToggleState; config: ToggleStateConfigs[ToggleState]; model?: ElementModel }): string | The element's markup in a state - the authored markup with that state applied; a pure function of the state.
Returns | |||||||||
el.api.settled(): Promise<void> | Wait for the last state's DOM work (async states: a diagram rendering, a chart mounting). Returns | |||||||||
el.store: Store<{ name: ToggleState; config: ToggleStateConfigs[ToggleState] }> | A defuss-store store of the element's state - subscribe to follow every change (also the user's), set it to drive the component. |
Registry
| Member | Description | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
df$.shadcn.toggleApi.setState<S extends ToggleState>(el: HTMLElement, name: S, config?: ToggleStateConfigs[S]): unknown | Enter a state: the DOM work runs (also when it is the current state), the store records it.
Returns | ||||||||||||
df$.shadcn.toggleApi.getState(el: HTMLElement): { name: ToggleState; config: ToggleStateConfigs[ToggleState]; model?: ElementModel } | The state the element shows now - read back from the DOM, so it includes what the user changed.
Returns | ||||||||||||
df$.shadcn.toggleApi.render(state: { name: ToggleState; config: ToggleStateConfigs[ToggleState]; model?: ElementModel }): string | The element's markup in a state - the authored markup with that state applied; a pure function of the state.
Returns | ||||||||||||
df$.shadcn.toggleApi.store(el: HTMLElement): Store<{ name: ToggleState; config: ToggleStateConfigs[ToggleState] }> | The element's store (bindComponent made it).
Returns | ||||||||||||
df$.shadcn.toggleApi.commit<S extends ToggleState>(el: HTMLElement, name: S, config?: ToggleStateConfigs[S]): void | Record a state the element reached on its own (no DOM work) - for a component's own handlers.
| ||||||||||||
df$.shadcn.toggleStates: ToggleState[] | The declared states, 'default' first: default, pressed. |
§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.// The State API runs on a store per button (el.store, AGENTS.md "State// through stores"); render(state) reproduces the authored markup 1:1.// 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, defussQuery, componentState, bindComponent } from '../../../shared/state-api.js';const df$ = defussGlobals();const dfDollar = defussQuery();const toggleStates = ['default', 'pressed'];// VERIFIED: (verify's API docs gate) the states below are exactly the declared ones, each// described, and every config field typed, described and named in the code./** setState() configs per state - the toggle's states take none. */export interface ToggleStateConfigs { /** Not pressed (aria-pressed="false"). */ default: {}; /** Pressed (aria-pressed="true"). */ pressed: {};}/** * The markup of a state - the one place a state becomes attributes. setState * runs it on the live button, render() on a detached copy of the authored * markup. 'default' is the authored aria-pressed value. */function applyMarkup(toggle, stateName, defaultPressed) { dfDollar(toggle).attr('aria-pressed', stateName === 'pressed' ? 'true' : defaultPressed);}/** UI side of setState (AGENTS.md "State API"): the state's markup. */function triggerStateChange(toggle, stateName, _config) { applyMarkup(toggle, stateName, toggle._defaultPressed ?? 'false');}/** Registry-level API; pass the toggle button explicitly. Unknown names throw. */export const toggleApi = componentState({ component: 'toggle', states: toggleStates, apply: (toggle, state) => triggerStateChange(toggle, state.name, state.config), // reflect reality: user clicks change aria-pressed without setState() read: (toggle, state) => ({ name: dfDollar(toggle).attr('aria-pressed') === 'true' ? 'pressed' : 'default', config: state.config }), markup: (toggle, state) => { const authored = state.model.attrs.find(([name]) => name === 'aria-pressed'); applyMarkup(toggle, state.name, authored ? authored[1] : 'false'); },});df$.toggleApi = toggleApi;df$.toggleStates = toggleStates;function init() { dfDollar('.toggle:not([data-init]):not(.toggle-group .toggle)').each((_i, toggle) => { dfDollar(toggle).data('init', ''); // remember the authored pressed state so setState('default') can restore it toggle._defaultPressed = dfDollar(toggle).attr('aria-pressed') || 'false'; // el.store + el.api: `$('#my-toggle').api.setState('pressed')` bindComponent(toggle, toggleApi, { name: toggle._defaultPressed === 'true' ? 'pressed' : 'default', config: {} }); dfDollar(toggle).on('click', () => { dfDollar(toggle).attr('aria-pressed', String(dfDollar(toggle).attr('aria-pressed') !== 'true')); }); });}init();new MutationObserver(init).observe(document, { childList: true, subtree: true });Comments, ideas or improvements? Edit this page's source on GitHub