@use 'sprucecss/scss/spruce' as *;
.toolbar {
align-items: center;
background-color: color('surface');
border: config('border-width-base', $display) solid color('border');
border-start-end-radius: radius('md');
border-start-start-radius: radius('md');
display: flex;
flex-wrap: wrap;
gap: spacer('xxs');
padding: spacer('xxs');
&__group {
display: flex;
gap: 2px;
& + & {
border-inline-start: config('border-width-base', $display) solid color('border');
padding-inline-start: spacer('xxs');
}
}
&__button {
@include clear-btn;
align-items: center;
block-size: 2.25rem;
border-radius: radius('sm');
color: color('text');
display: inline-flex;
inline-size: 2.25rem;
justify-content: center;
@include transition($property: 'background-color, color');
svg {
block-size: 1.1rem;
inline-size: 1.1rem;
}
&:hover {
background-color: color('surface-muted');
color: color('heading');
}
&[aria-pressed='true'] {
background-color: color-alpha('primary', 0.12);
color: color('primary');
}
&:focus-visible {
outline: 2px solid color('focus-ring');
outline-offset: -2px;
}
}
}
.toolbar-editor {
border: config('border-width-base', $display) solid color('border');
border-block-start: 0;
border-end-end-radius: radius('md');
border-end-start-radius: radius('md');
min-block-size: 8rem;
padding: spacer('s');
&:focus-visible {
outline: 2px solid color('focus-ring');
outline-offset: -2px;
}
}
<div class="toolbar" role="toolbar" aria-label="Text formatting" aria-controls="toolbar-editor" data-toolbar>
<div class="toolbar__group" role="group" aria-label="Style">
<button class="toolbar__button" type="button" aria-pressed="false" tabindex="0" aria-label="Bold"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M6 12h9a4 4 0 0 1 0 8H6V4h8a4 4 0 0 1 0 8"/></svg></button>
<button class="toolbar__button" type="button" aria-pressed="false" tabindex="-1" aria-label="Italic"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M19 4h-9M14 20H5M15 4 9 20"/></svg></button>
<button class="toolbar__button" type="button" aria-pressed="false" tabindex="-1" aria-label="Underline"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M6 4v6a6 6 0 0 0 12 0V4M4 20h16"/></svg></button>
</div>
<div class="toolbar__group" role="group" aria-label="Alignment">
<button class="toolbar__button" type="button" aria-pressed="true" tabindex="-1" aria-label="Align start"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M3 6h18M3 12h12M3 18h16"/></svg></button>
<button class="toolbar__button" type="button" aria-pressed="false" tabindex="-1" aria-label="Align center"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M3 6h18M6 12h12M4 18h16"/></svg></button>
<button class="toolbar__button" type="button" aria-pressed="false" tabindex="-1" aria-label="Align end"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M3 6h18M9 12h12M5 18h16"/></svg></button>
</div>
<div class="toolbar__group" role="group" aria-label="Insert">
<button class="toolbar__button" type="button" tabindex="-1" aria-label="Insert link"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7"/><path d="M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7"/></svg></button>
<button class="toolbar__button" type="button" tabindex="-1" aria-label="Insert image"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/></svg></button>
</div>
</div>
<div class="toolbar-editor" id="toolbar-editor" contenteditable="true" role="textbox" aria-multiline="true" aria-label="Message">Select some text and use the toolbar. Use the arrow keys to move between the buttons.</div>
(() => {
document.querySelectorAll('[data-toolbar]').forEach((toolbar) => {
const buttons = [...toolbar.querySelectorAll('button')];
let current = buttons.findIndex((button) => button.tabIndex === 0);
const focus = (index) => {
buttons[current].tabIndex = -1;
current = (index + buttons.length) % buttons.length;
buttons[current].tabIndex = 0;
buttons[current].focus();
};
toolbar.addEventListener('keydown', (event) => {
const isRtl = getComputedStyle(toolbar).direction === 'rtl';
const next = isRtl ? 'ArrowLeft' : 'ArrowRight';
const previous = isRtl ? 'ArrowRight' : 'ArrowLeft';
if (event.key === next) { event.preventDefault(); focus(current + 1); }
if (event.key === previous) { event.preventDefault(); focus(current - 1); }
if (event.key === 'Home') { event.preventDefault(); focus(0); }
if (event.key === 'End') { event.preventDefault(); focus(buttons.length - 1); }
});
buttons.forEach((button, index) => {
button.addEventListener('click', () => {
focus(index);
if (!button.hasAttribute('aria-pressed')) {
return;
}
const group = button.closest('[role="group"]');
const isExclusive = group?.getAttribute('aria-label') === 'Alignment';
if (isExclusive) {
group.querySelectorAll('[aria-pressed]').forEach((other) => other.setAttribute('aria-pressed', String(other === button)));
} else {
button.setAttribute('aria-pressed', String(button.getAttribute('aria-pressed') !== 'true'));
}
});
});
});
})();