Toolbar
Groups related controls into a single keyboard-navigable bar. Composes Toggle Groups, Buttons, and Separators with roving tabindex navigation.
Text formatting toolbar
Combines toggle groups with separators and a link button.
<div class="toolbar" role="toolbar" aria-label="Formatting">
<div class="toggle-group" data-type="multiple" ...>
<button class="toggle" aria-pressed="false">...</button>
...
</div>
<div class="separator" data-orientation="vertical" role="separator"></div>
<div class="toggle-group" data-type="single" ...>...</div>
<div class="separator" data-orientation="vertical" role="separator"></div>
<button class="btn" data-variant="ghost">Link</button>
</div>
Simple toolbar
Quick actions with icon buttons.
<div class="toolbar" role="toolbar" aria-label="Actions">
<button class="btn" data-variant="ghost" data-size="icon" aria-label="Undo">
<i data-lucide="undo-2"></i>
</button>
<button class="btn" data-variant="ghost" data-size="icon" aria-label="Redo">
<i data-lucide="redo-2"></i>
</button>
<div class="separator" data-orientation="vertical" role="separator"></div>
<button class="btn" data-variant="ghost" data-size="icon" aria-label="Copy">...</button>
<button class="btn" data-variant="ghost" data-size="icon" aria-label="Cut">...</button>
<button class="btn" data-variant="ghost" data-size="icon" aria-label="Paste">...</button>
</div>
CSS view file
/* -- Toolbar component ------------------------------------------ */
@layer components {
.toolbar {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem;
border: 1px solid var(--border);
border-radius: var(--radius-lg);
background: var(--background);
width: fit-content;
/* Vertical separators stretch to fill toolbar height */
& > .separator[data-orientation="vertical"] {
align-self: stretch;
height: auto;
margin: 0.25rem 0.25rem;
}
/* Vertical toolbar */
&[aria-orientation="vertical"] {
flex-direction: column;
& > .separator[data-orientation="horizontal"],
& > .separator:not([data-orientation]) {
height: 1px;
width: 1.5rem;
margin: 0.25rem 0;
}
}
}
@media (forced-colors: active) {
.toolbar {
border-color: ButtonText;
}
}
}
JavaScript view file
Roving tabindex for role="toolbar" containers. Arrow keys move focus between focusable children.
// -- Toolbar --------------------------------------------------
// Roving tabindex for role="toolbar" containers.
// Arrow keys move focus between focusable children.
function init() {
document.querySelectorAll('.toolbar[role="toolbar"]:not([data-init])').forEach((toolbar) => {
toolbar.dataset.init = '';
const items = Array.from(
toolbar.querySelectorAll('button:not(:disabled), a[href], [tabindex]:not([tabindex="-1"])')
);
if (items.length === 0) return;
items.forEach((item, i) => {
item.setAttribute('tabindex', i === 0 ? '0' : '-1');
});
toolbar.addEventListener('keydown', (e) => {
const current = items.indexOf(document.activeElement);
if (current === -1) return;
const vertical = toolbar.getAttribute('aria-orientation') === 'vertical';
const fwd = vertical ? 'ArrowDown' : 'ArrowRight';
const bwd = vertical ? 'ArrowUp' : 'ArrowLeft';
let next;
if (e.key === fwd) {
e.preventDefault();
next = (current + 1) % items.length;
} else if (e.key === bwd) {
e.preventDefault();
next = (current - 1 + items.length) % items.length;
} else if (e.key === 'Home') {
e.preventDefault();
next = 0;
} else if (e.key === 'End') {
e.preventDefault();
next = items.length - 1;
}
if (next !== undefined) {
items[current].setAttribute('tabindex', '-1');
items[next].setAttribute('tabindex', '0');
items[next].focus();
}
});
});
}
init();
new MutationObserver(init).observe(document, { childList: true, subtree: true });