Press / to search

    No results

    Search failed

    npm GitHub

    <toolbar-elemental>

    A bar of six buttons is six tab stops between the reader and whatever comes after it. The APG Toolbar pattern is the answer: one tab stop for the bar, arrow keys between the controls inside it. That is all this element is โ€” the role, the axis, and the single tabindex="0" that moves.

    <toolbar-elemental aria-label="Formatting">
      <button type="button" aria-pressed="true">Bold</button>
      <button type="button" aria-pressed="false">Italic</button>
      <button type="button" aria-pressed="false">Code</button>
      <button type="button">Link</button>
      <button type="button" disabled>Undo</button>
    </toolbar-elemental>

    Tab in, then walk it with โ† and โ†’. Tab again and you are past the whole bar, not one button further along it. Undo is skipped โ€” the platform will not focus a disabled control, so the cursor does not stop where focus cannot follow.

    The markup

    The buttons you would have written anyway, wrapped, and named:

    <toolbar-elemental aria-label="Formatting">
      <button type="button">Bold</button>
      <button type="button">Italic</button>
    </toolbar-elemental>

    Name it. A toolbar takes its name from aria-label, or aria-labelledby where something on the page already says it. The element cannot invent one and does not pretend to โ€” an unnamed toolbar is announced as "toolbar" and nothing more, and where a page has two of them, naming is not optional.

    <button>, <a href> and <select> are walked. The select spends arrows on itself, so the bar splits the axes the way the APG toolbar's own spin button does: the bar's axis and Home/End walk, the cross axis stays the select's โ€” on a bar across the page, โ†“ still opens and steps its list. The split has a cost worth knowing: an engine that spends the bar's axis on a closed select โ€” the arrows on Windows, Home/End to first and last option โ€” loses those to the walk, and a select on a vertical bar loses โ†“/โ†‘; Space and Enter open the list everywhere. A text field has no axis to spare โ€” every arrow is the caret's โ€” so it stays out of the walk and a tab stop of its own, which is MDN's advice for a control like that: keep it out of a toolbar, or put it last.

    Both the pattern and MDN say a toolbar is for grouping three or more controls. Nothing here enforces it โ€” two buttons are two tab stops, and a bar that saved one of them would be ceremony rather than a saving.

    Groups

    Related controls go in a role="group" with its own label. The arrows run straight through it โ€” six controls in two groups are one sequence, not two the keyboard has to enter and leave โ€” and a screen reader announces the group on the way past.

    <toolbar-elemental aria-label="Formatting">
      <div role="group" aria-label="Text">
        <button type="button">Bold</button>
        <button type="button">Italic</button>
        <button type="button">Code</button>
      </div>
      <div role="group" aria-label="Blocks">
        <button type="button">Quote</button>
        <button type="button">List</button>
      </div>
      <div role="group" aria-label="History">
        <button type="button">Undo</button>
        <button type="button" disabled>Redo</button>
      </div>
    </toolbar-elemental>

    Walk it with โ†’ from Bold. The group boundaries are not stops โ€” the cursor crosses from Code to Quote on one press, and End reaches Undo, because Redo is disabled and the platform will not focus it.

    The element needs nothing told to it: the controls are found wherever they sit, so a group โ€” or a <tooltip-elemental> wrapped round a button โ€” is a layer the walk sees through. The theme draws a rule between one group and the next, and never off either end.

    Attributes

    Attribute Type Default What it does
    vertical boolean false The bar runs down the page. Swaps the arrow keys with it, stacks the controls, and writes aria-orientation="vertical".

    aria-orientation is written only when it is true, because horizontal is the role's own default and a second copy of a fact is a second thing to keep in step.

    Keyboard

    Key Horizontal Vertical
    โ†’ โ† previous / next control left to the page
    โ†“ โ†‘ left to the page previous / next control
    Home End first / last control first / last control
    Tab past the whole bar past the whole bar
    Enter Space presses the control presses the control

    The ends do not wrap. Running off one is not how you get anywhere here โ€” Tab is โ€” and a bar that looped would be a bar a reader can walk forever without noticing they had.

    The off-axis arrows are not merely unused. A โ†“ on a horizontal bar is the page scrolling, and a toolbar that swallowed it would pin the page under a reader who is passing through.

    The tab stop follows focus. Click the last button and the arrows carry on from there rather than jumping back to the start.

    Disabled controls

    A disabled button keeps its place and is skipped by the arrows, because the platform will not focus one and a cursor that lands where focus cannot follow is a bar that stops moving.

    To keep a control reachable and merely inert โ€” announced, explainable, not pressable โ€” use aria-disabled="true" instead. It stays focusable, so the arrows still reach it, and the theme dims it the same way.

    Buttons that enable and disable as the document changes are the ordinary case, not an exotic one, so the element watches for it. Nothing has to call a refresh, and forgetting one would be a bar whose only tab stop had just gone disabled.

    Hidden controls

    A control that is not on screen is stepped over for the same reason a disabled one is: focus() on it does nothing, so an arrow that lands there is an arrow that moves nothing. That covers a button your own stylesheet hides โ€” the one for a feature this browser turned out not to have โ€” and a whole region folded away on a narrow screen, which is how a crowded bar sheds its rarely-used half.

    The tab stop goes with it. A stop left on a control the reader cannot see is a bar Tab enters and lands nowhere in, so it moves to the first control still on screen. A bar hidden whole keeps the stop it had, because that bar is waiting on something rather than gone.

    Fading is not hiding. A control row that drops to opacity: 0 over a video is still a row the keyboard reaches, and focus arriving in it is usually what brings it back โ€” so opacity is no part of the question.

    Before Safari 17.4 there is no checkVisibility to ask, and only the hidden attribute is seen. A control hidden by a CSS rule there behaves as it did before: the arrows still stop on it.

    Without script

    Buttons, each its own tab stop. That is the state the pattern improves on rather than a broken one โ€” nothing is authored tabindex="-1", so nothing is lost when the script never arrives.

    What it will not do

    No overflow menu, no wrapping, no aria-pressed bookkeeping, and no opinion about what the buttons do. A toggle button's state is yours to set; the theme reads it off aria-pressed rather than off a class, so there is one place it lives.

    Styling

    The structure stylesheet lays the bar out on its axis and nothing else โ€” that part is not decoration, because an aria-orientation that disagrees with which way the buttons are drawn is a keyboard that disagrees with the screen. The theme is optional and draws the bar and the controls in it.

    Custom property Default What it does
    --toolbar-elemental-gap 0.25rem Between the controls
    --toolbar-elemental-inset 0.25rem Padding inside the bar
    --toolbar-elemental-radius 0.375rem Corners of the bar, and of a control in it
    --toolbar-elemental-border color-mix(in srgb, currentcolor 20%, transparent) The bar's outline
    --toolbar-elemental-hover color-mix(in srgb, currentcolor 10%, transparent) A control under the pointer
    --toolbar-elemental-pressed color-mix(in srgb, currentcolor 18%, transparent) A control whose aria-pressed is true
    @use "book-of-elementals/toolbar/style.scss";
    @use "book-of-elementals/toolbar/theme.scss"; // optional
    import 'book-of-elementals/toolbar';