Anatomy of a React component
My vision for how every React component should look
421
8 min.
The SST standard defines requirements for the semantics, accessibility, and logic of tabs.
overflow-x: auto. Only the strip scrolls horizontally, not the entire page; the open panel's content must reflow to the viewport width.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.-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.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.
role="tablist" and an accessible name, using a visible heading with aria-labelledby or aria-label.role="tab", a unique, stable id, and aria-controls="panel-id" pointing to its associated panel.role="tabpanel", a unique id, and aria-labelledby="tab-id" referencing its controlling tab.aria-selected="true"; all other tabs have aria-selected="false". Inactive panels are hidden, for example with the hidden attribute.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.#tab-id. Use URL-safe identifiers, or consistently encode and decode them when reading the hash.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.history.pushState and synchronize the tab state on popstate.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.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.← 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.aria-selected, the aria-controls and aria-labelledby relationships, and panel visibility remain synchronized.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.prefers-reduced-motion: reduce.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.
My vision for how every React component should look
421
8 min.
The SSF-U standard defines requirements for the semantics, accessibility, and logic of fullscreen
248
2 min.
The SSA standard defines requirements for the semantics, accessibility, and logic of accordion
258
2 min.
An Analysis of Critical Web Design Mistakes. Why Sliders, Autoplay, and Slow-Loading Pages Reduce Conversion Rates and Rankings on Google
232
2 min.
The SSP standard defines requirements for the semantics, accessibility, and logic of pagination
240
1 min.
The SSPS standard defines requirements for the structure and naming of files and folders in a project
345
2 min.