Toolbar

Select some text and use the toolbar. Use the arrow keys to move between the buttons.
@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'));
        }
      });
    });
  });
})();

Toolbar

On this page

An accessible toolbar that groups controls (like a text editor's formatting buttons) into a single tab stop.

Technical Details

  • It follows the WAI-ARIA Toolbar pattern: role="toolbar" with a label, groups with their own labels, and a roving tabindex, so the whole toolbar is one stop in the tab order.
  • ←/→ move between the buttons (mirrored in RTL), Home/End jump to the first/last; the toolbar remembers the last focused button.
  • Toggle buttons (bold, italic) use aria-pressed; the alignment buttons behave like a radio group: one of them is always pressed.
  • The editor here is a demo; wire the buttons to your own editor.

Documentation

Learn about Spruce CSS through our extensive documentation.

Components

Explore our UI library of accessible components built with Spruce CSS.

Blocks and Pages

Full-width page blocks and complete page templates, ready to copy.

Find us on GitHub

Did you find a bug? Have an idea or a question? Please open an issue to help us develop the project.