Accessibility pattern · Navigation

Vertical tabs

A settings-style rail: one tab stop, the Up and Down arrows inside it, and aria-orientation="vertical" so assistive technology knows which arrows to offer. When the space is narrow the rail becomes a scrolling row, and the script switches the orientation, and the arrow keys, to match.

WCAG criteria
6
Keyboard rules
4
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Settings

Account

How you appear in the shop, and where your orders go.

Name
Asha Rao
Member since
March 2021
Delivery address
Koregaon Park, Pune
Language
English (India)

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
TabMoves into the tab list, onto the selected tab; the next Tab moves into its panel.
Arrow Down or Arrow UpMoves to the next or previous tab, wrapping at the ends, and shows its panel.
Home or EndMoves to the first or last tab.
Arrow Right or Arrow LeftDo the same as Down and Up when the rail has become a row in a narrow space.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Tab reaches the tab listSettings, tab list. Account, tab, selected, 1 of 5
Arrow Down moves to the next tabNotifications, tab, selected, 2 of 5
Tab moves into the panelNotifications, tab panel
A setting in the panel is turned offPrice drops, checkbox, not checked

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-vertical-tabs" data-ap-vertical-tabs>
  <div class="ap-vertical-tabs__frame">
    <div class="ap-vertical-tabs__rail">
      <h3 class="ap-vertical-tabs__label" id="vtabs-label">Settings</h3>
      <div class="ap-vertical-tabs__list" role="tablist" aria-labelledby="vtabs-label" aria-orientation="vertical">
        <button type="button" class="ap-vertical-tabs__tab" role="tab" id="vtabs-tab-account" aria-selected="true" aria-controls="vtabs-panel-account">
          <svg class="ap-vertical-tabs__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg>
          <span>Account</span>
        </button>
        <button type="button" class="ap-vertical-tabs__tab" role="tab" id="vtabs-tab-notifications" aria-selected="false" aria-controls="vtabs-panel-notifications" tabindex="-1">
          <svg class="ap-vertical-tabs__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 16V11a6 6 0 0 1 12 0v5l2 2H4Z"/><path d="M10 21h4"/></svg>
          <span>Notifications</span>
        </button>
        <button type="button" class="ap-vertical-tabs__tab" role="tab" id="vtabs-tab-privacy" aria-selected="false" aria-controls="vtabs-panel-privacy" tabindex="-1">
          <svg class="ap-vertical-tabs__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 3 5 6v5c0 5 3 8.5 7 10 4-1.5 7-5 7-10V6Z"/><path d="m9 12 2 2 4-4"/></svg>
          <span>Privacy</span>
        </button>
        <button type="button" class="ap-vertical-tabs__tab" role="tab" id="vtabs-tab-appearance" aria-selected="false" aria-controls="vtabs-panel-appearance" tabindex="-1">
          <svg class="ap-vertical-tabs__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
          <span>Appearance</span>
        </button>
        <button type="button" class="ap-vertical-tabs__tab" role="tab" id="vtabs-tab-billing" aria-selected="false" aria-controls="vtabs-panel-billing" tabindex="-1">
          <svg class="ap-vertical-tabs__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 10h18M7 15h4"/></svg>
          <span>Billing</span>
        </button>
      </div>
    </div>

    <div class="ap-vertical-tabs__panel" role="tabpanel" id="vtabs-panel-account" aria-labelledby="vtabs-tab-account" tabindex="0">
      <h4 class="ap-vertical-tabs__name">Account</h4>
      <p class="ap-vertical-tabs__lead">How you appear in the shop, and where your orders go.</p>
      <dl class="ap-vertical-tabs__facts">
        <div><dt>Name</dt><dd>Asha Rao</dd></div>
        <div><dt>Member since</dt><dd>March 2021</dd></div>
        <div><dt>Delivery address</dt><dd>Koregaon Park, Pune</dd></div>
        <div><dt>Language</dt><dd>English (India)</dd></div>
      </dl>
    </div>

    <div class="ap-vertical-tabs__panel" role="tabpanel" id="vtabs-panel-notifications" aria-labelledby="vtabs-tab-notifications" tabindex="0" hidden>
      <h4 class="ap-vertical-tabs__name">Notifications</h4>
      <p class="ap-vertical-tabs__lead">Choose what we send you. Security messages always come through.</p>
      <div class="ap-vertical-tabs__options">
        <div class="ap-vertical-tabs__option">
          <input type="checkbox" id="vtabs-orders" aria-describedby="vtabs-orders-hint" checked />
          <label for="vtabs-orders">Order updates</label>
          <p class="ap-vertical-tabs__hint" id="vtabs-orders-hint">Dispatch, delivery and returns.</p>
        </div>
        <div class="ap-vertical-tabs__option">
          <input type="checkbox" id="vtabs-prices" aria-describedby="vtabs-prices-hint" checked />
          <label for="vtabs-prices">Price drops</label>
          <p class="ap-vertical-tabs__hint" id="vtabs-prices-hint">When something you saved costs less.</p>
        </div>
        <div class="ap-vertical-tabs__option">
          <input type="checkbox" id="vtabs-news" aria-describedby="vtabs-news-hint" />
          <label for="vtabs-news">New arrivals</label>
          <p class="ap-vertical-tabs__hint" id="vtabs-news-hint">One email a week, on Fridays.</p>
        </div>
      </div>
    </div>

    <div class="ap-vertical-tabs__panel" role="tabpanel" id="vtabs-panel-privacy" aria-labelledby="vtabs-tab-privacy" tabindex="0" hidden>
      <h4 class="ap-vertical-tabs__name">Privacy</h4>
      <p class="ap-vertical-tabs__lead">Decide what other shoppers see and what we use to suggest things.</p>
      <div class="ap-vertical-tabs__options">
        <div class="ap-vertical-tabs__option">
          <input type="checkbox" id="vtabs-reviews" aria-describedby="vtabs-reviews-hint" checked />
          <label for="vtabs-reviews">Show my name on reviews</label>
          <p class="ap-vertical-tabs__hint" id="vtabs-reviews-hint">Otherwise reviews show your initials.</p>
        </div>
        <div class="ap-vertical-tabs__option">
          <input type="checkbox" id="vtabs-history" aria-describedby="vtabs-history-hint" />
          <label for="vtabs-history">Use my orders for suggestions</label>
          <p class="ap-vertical-tabs__hint" id="vtabs-history-hint">Recommendations based on what you bought.</p>
        </div>
      </div>
    </div>

    <div class="ap-vertical-tabs__panel" role="tabpanel" id="vtabs-panel-appearance" aria-labelledby="vtabs-tab-appearance" tabindex="0" hidden>
      <h4 class="ap-vertical-tabs__name">Appearance</h4>
      <p class="ap-vertical-tabs__lead">How the shop looks on this device.</p>
      <fieldset class="ap-vertical-tabs__group">
        <legend>Theme</legend>
        <label class="ap-vertical-tabs__choice"><input type="radio" name="vtabs-theme" value="system" checked /> Match the system</label>
        <label class="ap-vertical-tabs__choice"><input type="radio" name="vtabs-theme" value="light" /> Light</label>
        <label class="ap-vertical-tabs__choice"><input type="radio" name="vtabs-theme" value="dark" /> Dark</label>
      </fieldset>
    </div>

    <div class="ap-vertical-tabs__panel" role="tabpanel" id="vtabs-panel-billing" aria-labelledby="vtabs-tab-billing" tabindex="0" hidden>
      <h4 class="ap-vertical-tabs__name">Billing</h4>
      <p class="ap-vertical-tabs__lead">Your membership renews on its own; you can cancel at any time.</p>
      <dl class="ap-vertical-tabs__facts">
        <div><dt>Plan</dt><dd>Nila Plus, yearly</dd></div>
        <div><dt>Next payment</dt><dd>₹1,499 on 12 March</dd></div>
        <div><dt>Pay with</dt><dd>Card ending 4242</dd></div>
      </dl>
    </div>
  </div>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    Roles tie the tabs to their panels, and aria-orientation says the list runs top to bottom, so the structure is exposed, not just drawn.

  • 1.4.1 Use of Color Level A

    The selected tab is raised, set in heavier text and marked with a bar, so it does not depend on color.

  • 2.1.1 Keyboard Level A

    The arrow keys, Home and End move between tabs, and every setting inside the panels is a native field.

  • 2.4.3 Focus Order Level A

    Only the selected tab is in the Tab order, so the next Tab goes straight into its panel.

  • 2.4.7 Focus Visible Level AA

    The focused tab shows a ring that clears 3:1 against the rail.

  • 4.1.2 Name, Role, Value Level A

    Each tab has role=tab and aria-selected, and each panel is labelled by its tab.

Usage

When to use it

Use it

  • Settings and preferences with five or more sections, where a side rail has room for longer labels.
  • Labels that are too long, or too many, to sit in a row of tabs.

Use something else

  • Moving between pages: use a sidebar navigation of links marked with aria-current.
  • Two or three sections: a row of tabs or a segmented control is lighter.

Common failures

How it usually goes wrong

  • aria-orientation left out

    Without it, a tab list is taken to be horizontal: screen readers suggest Left and Right while the rail on screen invites Up and Down.

  • Arrow keys that do not follow the layout

    A rail that becomes a row on a phone but still answers only to Up and Down confuses everyone. Here the orientation, and the keys, follow the layout.

  • Links dressed as tabs

    If each item loads a new page, it is navigation: links in a nav landmark with aria-current, not a tab list.

  • Icons without words

    A rail of icons alone saves space and leaves people guessing. Each tab here has its word; the icon is decoration.

  • Selection shown only by color

    A tinted background is easy to miss. The selected tab here is raised, bolder and marked with a bar.

Notes

Building it

  • The script reads the rail's layout when the component is resized and sets aria-orientation to vertical or horizontal; the arrow keys follow that value.
  • Each panel starts with a heading and plain text, so every panel has tabindex=0 and the next Tab reaches it.
  • Settings here apply as they change. If yours need a Save button, put it inside each panel so it is clear what it saves.
  • The icons are aria-hidden SVG drawn with currentColor, so they follow the text color in both themes.

Sources: WAI-ARIA Authoring Practices: Tabs

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction