Toggle
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.
Default
Transparent at rest. Background appears on hover and when pressed.
← unpressed vs pressed
<button class="toggle" aria-pressed="false"
aria-label="Bookmark">
<svg aria-hidden="true" width="16" height="16"
viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"/>
</svg>
</button>
Outline
Visible border and subtle shadow at rest.
<button class="toggle" data-variant="outline"
aria-pressed="true" aria-label="Toggle italic">
<svg aria-hidden="true" width="16" height="16" ...>...</svg>
</button>
<button class="toggle" data-variant="outline"
aria-pressed="false" aria-label="Toggle bold">
<svg aria-hidden="true" width="16" height="16" ...>...</svg>
</button>
With Text
Icon and label together.
<button class="toggle" aria-pressed="false">
<svg aria-hidden="true" width="16" height="16" ...>...</svg>
Italic
</button>
Size
Small, default, and large via data-size.
<button class="toggle" data-variant="outline" data-size="sm"
aria-pressed="false">Small</button>
<button class="toggle" data-variant="outline"
aria-pressed="false">Default</button>
<button class="toggle" data-variant="outline" data-size="lg"
aria-pressed="false">Large</button>
Disabled
Non-interactive at 50% opacity.
<button class="toggle" data-variant="outline"
aria-pressed="false" disabled>Disabled</button>
<button class="toggle" data-variant="outline"
aria-pressed="true" disabled>Disabled</button>
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 {
background-color: var(--muted);
color: var(--muted-foreground);
}
&[aria-pressed="true"] {
background-color: var(--accent);
color: var(--accent-foreground);
&:hover {
background-color: color-mix(in oklch, var(--accent) 85%, var(--foreground));
}
}
&:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 2px;
}
&:disabled {
pointer-events: none;
opacity: 0.5;
}
& 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 {
background-color: var(--accent);
color: var(--accent-foreground);
}
&[aria-pressed="true"] {
background-color: var(--accent);
color: var(--accent-foreground);
&:hover {
background-color: color-mix(in oklch, var(--accent) 85%, var(--foreground));
}
}
}
/* -- Sizes ----------------------------------------------- */
&[data-size="sm"] { height: 2rem; padding: 0 0.375rem; min-width: 2rem; }
&[data-size="lg"] { height: 2.5rem; padding: 0 0.625rem; min-width: 2.5rem; }
}
@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.
// Skips toggles inside .toggle-group — those are managed by toggle-group.js.
function init() {
document.querySelectorAll('.toggle:not([data-init]):not(.toggle-group .toggle)').forEach((toggle) => {
toggle.dataset.init = '';
toggle.addEventListener('click', () => {
const pressed = toggle.getAttribute('aria-pressed') === 'true';
toggle.setAttribute('aria-pressed', !pressed);
});
});
}
init();
new MutationObserver(init).observe(document, { childList: true, subtree: true });