SST - Single Standard for Tabs

SST - Single Standard for Tabs
Standards

62

7 min.

Definitions

The SST standard defines requirements for the semantics, accessibility, and logic of tabs.

Structure and Behavior

Container and scrolling

  • Tabs remain on a single line. Under the SST-A convention, they are not wrapped onto a second line or replaced with a dropdown on narrow screens.
  • The tab strip uses a dedicated container with overflow-x: auto. Only the strip scrolls horizontally, not the entire page; the open panel's content must reflow to the viewport width.
  • A second row is not created. Tabs retain their width, while the container provides horizontal scrolling when the list overflows.
  • Where possible, leave part of the next tab visible at the container edge — approximately 10–20px. This indicates that the list continues and may remove the need for decorative arrow indicators. Do not clip the active tab's label or focus indicator; this visual cue does not replace keyboard navigation.
  • On mobile, the system scrollbar may be hidden if scrolling remains discoverable through a partially visible next tab or another indicator. Hiding the scrollbar must not make scrolling unavailable to keyboard users.
  • -webkit-overflow-scrolling: touch is not a baseline requirement for smooth scrolling. Current iOS and iPadOS versions accelerate scrolling in overflow areas without this property; on iPhone, it can also create a stacking context. Add it only for explicitly supported older versions after testing on devices.

Active tab and centering

  • The active tab has a clear visual state, such as an accent line or a changed background. The distinction must not rely on color alone.

  • When a tab is selected, keyboard focus moves, or the page opens from a direct link, scroll the strip so the selected tab is visible and, where possible, near the center.

  • Centering is constrained by the available scroll range: the first and last tabs cannot always be positioned exactly in the center when they are at the container's edges.

  • For left-to-right layouts, the target can be calculated relative to the start of the scrollable content:

    targetScrollLeft = clamp(tabStart + tabWidth / 2 - viewportWidth / 2, 0, scrollWidth - viewportWidth)

    Here, tabStart is the tab's position relative to the start of the scroll track. RTL layouts must account for reading direction and differences in horizontal scrolling behavior; use logical element scrolling where possible.

Identification and URL

Tab and panel relationships

  • Give the container role="tablist" and an accessible name, using a visible heading with aria-labelledby or aria-label.
  • Each panel control has role="tab", a unique, stable id, and aria-controls="panel-id" pointing to its associated panel.
  • Each panel has role="tabpanel", a unique id, and aria-labelledby="tab-id" referencing its controlling tab.
  • The active tab has aria-selected="true"; all other tabs have aria-selected="false". Inactive panels are hidden, for example with the hidden attribute.
  • Implement roving tabindex: the active tab has tabindex="0", and all others have tabindex="-1". A panel without focusable content may use tabindex="0" so it can be reached in the keyboard sequence.
  • Assign every tab a unique, stable identifier that is safe to use in a URL fragment, such as #tab-id. Use URL-safe identifiers, or consistently encode and decode them when reading the hash.
  • On initial page load, match the hash to a tab. If a matching tab is found, activate it and its panel, then scroll the strip to it. If the hash is unknown, keep the default tab active.
  • Update the URL on tab selection according to the chosen history strategy. history.replaceState updates the current entry without adding a separate Back step for every tab selection. The application must update the tab state in the same handler rather than relying on a hashchange event.
  • If Back and Forward should restore previous tab selections, use history.pushState and synchronize the tab state on popstate.
  • Updating the hash must not remove the path or query parameters. If the hash is unknown or invalid, the interface must retain a working default tab.

Accessibility and Keyboard

  • Use tablist, tab, and tabpanel only for tabs that switch panels on the same page. Use links for navigation between pages, even when they are styled to look like tabs.
  • Pressing Tab moves focus to the active tab in the strip; the next Tab moves focus out of the tab list to the panel content or the next page element.
  • In a horizontal list, ← and → move focus between tabs and wrap from one end of the list to the other. In RTL interfaces, key direction must match the visual order and reading direction.
  • Home and End may move focus to the first and last tabs. With manual activation, activate the focused tab with Enter or Space; with automatic activation, selection follows focus.
  • Use automatic activation when the associated panel appears without noticeable delay. If panels load slowly, prefer manual activation so moving focus through the tabs does not trigger lagging operations.
  • When focus moves, scroll the active tab into view. The container must not clip the visible focus indicator.

Testing

  • At mobile widths, tabs stay on one line, the strip can be swiped, and horizontal scrolling does not extend to the entire page.
  • A partially visible next tab or another indicator clearly signals that the strip can scroll. Hiding the scrollbar must not remove useful feedback.
  • The active tab is indicated visually and programmatically; aria-selected, the aria-controls and aria-labelledby relationships, and panel visibility remain synchronized.
  • The tab list has an accessible name, and each tab has a clear label; verify their relationships with a screen reader.
  • Keyboard navigation follows the selected activation mode, works at both ends of the list, and accounts for RTL direction.
  • The selected tab remains visible after a click, keyboard navigation, and loading from a hash. Verify the centering limits for the first and last tabs.
  • Test direct links with both valid and unknown hashes on initial load and page refresh.
  • With replaceState, selection updates the address without adding a history entry for every tab. With pushState, Back and Forward keep the URL, active tab, and panel synchronized.
  • Hide the scrollbar only when touch and keyboard scrolling remain available. Check its appearance and behavior in target browsers, including iOS and Android.
  • Centering and smooth scrolling must not interfere with users who enable prefers-reduced-motion: reduce.

Examples

Tab strip with panels and ARIA relationships:

<div class="tabs__viewport">
  <div class="tabs__list" role="tablist" aria-label="Document sections">
    <button
      id="tab-overview"
      type="button"
      role="tab"
      aria-controls="panel-overview"
      aria-selected="true"
      tabindex="0"
    >
      Overview
    </button>
    <button
      id="tab-details"
      type="button"
      role="tab"
      aria-controls="panel-details"
      aria-selected="false"
      tabindex="-1"
    >
      Details
    </button>
  </div>
</div>

<section
  id="panel-overview"
  role="tabpanel"
  aria-labelledby="tab-overview"
  tabindex="0"
>
  Overview content.
</section>

<section
  id="panel-details"
  role="tabpanel"
  aria-labelledby="tab-details"
  tabindex="0"
  hidden
>
  Details content.
</section>

CSS for a single-line scrollable strip:

.tabs__viewport {
  overflow-x: auto;
  padding: 2px;
  scroll-behavior: smooth;
}

.tabs__list {
  display: flex;
  flex-wrap: nowrap;
  width: max-content;
  min-width: 100%;
}

.tabs__list [role="tab"] {
  flex: 0 0 auto;
  white-space: nowrap;
}

.tabs__list [role="tab"][aria-selected="true"] {
  border-block-end: 2px solid currentColor;
}

.tabs__list [role="tab"]:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 2px;
}

@media (max-width: 767px) {
  .tabs__viewport {
    scrollbar-width: none;
  }

  .tabs__viewport::-webkit-scrollbar {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .tabs__viewport {
    scroll-behavior: auto;
  }
}

On mobile, hiding the scrollbar is balanced by showing part of the next tab. The visible portion depends on the container and labels; the active tab and its focus indicator remain fully visible.

The 767px media-query breakpoint is an example, not an SST-A requirement. Replace it with the breakpoint used by the project's responsive system.

Centering a tab and synchronizing the URL:

const centerTab = (tab) => {
  tab.scrollIntoView({
    block: 'nearest',
    inline: 'center'
  })
}

const findTabFromUrl = (tabs) => {
  const tabId = window.location.hash.slice(1)

  return tabs.find((tab) => tab.id === tabId) || null
}

const updateTabUrl = (tab) => {
  window.history.replaceState(null, '', '#' + tab.id)
}

On initialization, activate the tab returned by findTabFromUrl together with its panel, then pass it to centerTab. When a user selects a tab, first update aria-selected, roving tabindex, and panel visibility; then call centerTab and updateTabUrl. This snippet illustrates state, URL, and scroll synchronization; keyboard handlers must follow the rules above.

Similar categories:

Similar articles

  • Anatomy of a React component

    My vision for how every React component should look

    421

    8 min.

  • SSF-U - Single Standard for Fullscreen

    The SSF-U standard defines requirements for the semantics, accessibility, and logic of fullscreen

    248

    2 min.

  • SSA - Single Standard for Accordion

    The SSA standard defines requirements for the semantics, accessibility, and logic of accordion

    258

    2 min.

  • Bad Practices for Websites

    An Analysis of Critical Web Design Mistakes. Why Sliders, Autoplay, and Slow-Loading Pages Reduce Conversion Rates and Rankings on Google

    232

    2 min.

  • SSP - Single Standard for Pagination

    The SSP standard defines requirements for the semantics, accessibility, and logic of pagination

    240

    1 min.

  • SSPS - Single Standard for Project Structure

    The SSPS standard defines requirements for the structure and naming of files and folders in a project

    345

    2 min.

  • All articles

Contact me