Tabs organise content across screens, data sets and interactions at the same level of hierarchy. A tab bar lets the user move between groups of related content without leaving the page. For content that must be read in order, use steps instead.
Tokens, sizes, measurements, states and requirements. Values come from the live token package and from Figma: Tab items, Tab bar sizes, Tab info.
Resolved from the live token package.
| State | Part | Token | Value |
|---|
Open points: the enabled icon colour has no token in the Figma export; text-secondary is used. Figma shows the Primary active line at 3px and the Secondary at 2px; both are built as shown. Large and Medium are identical on desktop in Figma. The overflow “more” button is not built yet (scrolling is).
| Size | Mobile and tablet (below 769px) | Desktop (769px+) |
|---|---|---|
| Large | Label 16/16, 0.32px · gap to line 10px · tab 56px (label only: bar 29px) | Label 18/18, 0.18px · gap 12px · tab 60px (bar 33px) |
| Medium | Label 16/16, 0.32px · gap 8px · tab 54px (bar 27px) | Label 18/18, 0.18px · gap 12px · tab 60px (bar 33px) |
| Small | Label 14/14, 0.42px · gap 8px · tab 52px (bar 25px) | Label 16/16, 0.48px · gap 8px · tab 54px (bar 27px) |
| Icon only | Tab 30px · icon 20px · gap 8px | Tab 34px (Large, Medium) or 30px (Small) · gap 12px or 8px |
Tab = icon 20px + 8px + label + gap + the 2px line; a bar adds the 1px divider. Label only has no icon. Primary: 24px between tabs. Secondary: 16px padding each side, no gap. sm (640–768px) still has the mobile size.
Padding of one tab. Below the label (or the icon) sits the gap to the line plus the 2px line: 14px on desktop Large and Medium. Secondary tabs also have 16px on each side, so the line is wider than the label. Primary tabs have no side padding: the gap between them is 24px. The numbers are read from the component.
| Attribute | Value |
|---|---|
| Tab, label only (Large) | 56px with an icon, 29px bar without (mobile); 60px and 33px from 769px |
| Padding below the label or icon | Gap to the line plus the 2px line: Large 10 + 2 mobile, 12 + 2 desktop; Medium 8 + 2 mobile, 12 + 2 desktop; Small 8 + 2 |
| Padding, left and right, Primary | None. 24px between tabs |
| Padding, left and right, Secondary | 16px, no gap between tabs |
| Icon-only tab | Same bottom padding (10px mobile, 14px desktop for Large and Medium); no side padding |
| Icon | 20px, 8px above the label |
| Leading icon in the label | 24px (20px on Small below 769px), 4px before the text |
| Active line | 3px Primary, 2px Secondary |
| Bar divider | 1px |
| Focus ring | 2px, 4px clear above, left and right; 6px above the tab’s bottom edge |
The same distances as spacing tokens.
| Part | Value |
|---|---|
| Between Primary tabs | space-24 |
| Secondary tab padding, left and right | space-16 |
| Icon to label, status to label, leading icon to text | space-8 and space-4 |
| Label to line | 8px, 10px or 12px (see Sizes): literals |
| Focus ring room | 4px: the bar has padding: 4px 4px 0; margin: -4px -4px 0 |
Because of the focus-ring room the bar’s divider sticks out 4px past the content column on each side.
Enabled, hover, active and focus. Focus is a 2px ring around icon and label, on an enabled or an active tab alike; the line stays visible below it. Hover and focus are forced on this page. Figma has no disabled state, so none is built.
| State | Label + icon | Label | Icon only | Rule |
|---|
| Size | Type | Mobile (below 769px) | Desktop (769px+) |
|---|---|---|---|
| Large | label-lg | 16/16, 0.32px | 18/18, 0.18px |
| Medium | label-lg | 16/16, 0.32px | 18/18, 0.18px |
| Small | label-md | 14/14, 0.42px | 16/16, 0.48px |
| Number badge | — | 10/10, no letter spacing | 10/10, no letter spacing |
Breuer Condensed, weight 600 (Figma bold 700 − 100), uppercase, with ss02 and ss03.
| Element | Class | Notes |
|---|---|---|
| Bar | tab-bar | The row plus the 1px divider (border-primary). |
| Tab | tab, tab--active, tab--icon-only | A button (content) or a (navigation). |
| Icon | tab__icon with ms | 20px, above the label. |
| Label | tab__label | Uppercase. Optional leading ms icon. |
| Status | tab__dot, tab__count | 6px dot, or a red pill with a number (16px high). |
| Sliding line | tab-bar__indicator | Added by tabs.js. |
| Panel | tab-panel | Content of a content tab. role="tabpanel". |
| Modifier | Effect |
|---|---|
.tab-bar--primary | 24px gap between tabs; line spans the label only; active line 3px |
.tab-bar--secondary | No gap, each tab has 16px side padding, line spans label and padding; active line 2px |
.tab-bar--md, .tab-bar--sm | Medium and Small. Default (no class) is Large. |
.tab-bar--on-grey | Hover line surface-40 instead of surface-10 |
.tab-bar--page | On-page navigation: the bar is sticky and the tabs are #anchor links (scrollspy and jump in tabs.js) |
--tab-sticky-top | Custom property: distance from the top of the window to a tab-bar--page bar, for a fixed header above it (default 0) |
--tab-line-active | Custom property: colour of the active line (default surface-100) |
Figma bars hug their content (the divider ends at the last tab). The component fills its container, per the overflow information in Figma. Say if hugging is wanted.
| Property | Duration | Easing |
|---|---|---|
Sliding active line: transform and width | duration-medium-2 | ease-standard |
| Hover line grows from the bottom | duration-medium-2 | ease-decelerate-generic |
| Hover line colour fades | duration-medium-2 | ease-standard |
| Label and icon colour | Instant | — |
Content tabs: the line slides to the clicked tab (tabs.js moves one .tab-bar__indicator). Link tabs slide across the page change: a click keeps the hover look, the next page starts the line under the old tab and slides it to the new one (the old index is kept in sessionStorage for 5 s; the bar needs the same aria-label on every page). The tab the line is travelling to keeps its grey hover line until the sliding line has arrived over it. All of it is off under reduced motion.
| Topic | Rule |
|---|---|
| Keyboard | Tab moves into the bar, Left and Right switch tab (they wrap), Home and End go to the first and last. A tab opens as soon as it is focused. tabs.js does this. |
| Content tabs | The selected tab has tabindex="0", the others -1. Exactly one tab is active. |
| Navigation tabs | Links in a nav, the current page has aria-current="page". Link tabs.js for the sliding line. |
| Accent colour | The active line is black (surface-100) by default. Where a product uses its accent colour, set --tab-line-active on the bar: Swedol --color-accent-default; Tools is a primitive in the package (--color-primitive-color-brand-tools-accent-brand). The My Pages prototype pages use var(--brand), their own accent alias. Never restyle .tab::after or .tab-bar__indicator. |
| Overflow | The bar scrolls sideways with the scrollbar hidden; the active tab is scrolled into view. The divider runs the full width. |
| On-page navigation | nav.tab-bar.tab-bar--page with a.tab whose href is #section-id. The bar is sticky (--tab-sticky-top). Scrollspy: the current tab is the last section whose top is above a line 25% down the visible area; at the end of the page it is the last section. The tab gets aria-current="location" and the line slides to it. Click: scrolls so the section starts under the bar (plus the section’s scroll-margin-top), 150 / 200 / 300 / 450 ms by distance (duration-fast-3, fast-4, medium-2, slow-1), ease-decelerate-generic; instant under reduced motion; wheel, touch or a key stops it. Focus moves to the section (tabindex="-1") and the #hash is added (data-tabs-hash="off" keeps the address). A scrolling box instead of the window works too (the nearest scrollable parent of the sections). Any number of sections: the bar scrolls sideways and keeps the current tab in view. |
| Topic | Requirement |
|---|---|
| Content tabs | role="tablist" on the bar, role="tab" on each tab (button) with aria-selected and aria-controls, role="tabpanel" with aria-labelledby on the content. The selected tab has tabindex="0", the others -1. |
| Navigation tabs | Links in a nav with an aria-label; the current page has aria-current="page". |
| Icon only | Needs an aria-label. A status dot or number needs a text alternative where its meaning is not clear. |
| Focus | 2px ring border-focus on :focus-visible (WCAG 2.4.7, 1.4.11). The active line stays visible. |
| Motion | The sliding line is off under prefers-reduced-motion (WCAG 2.3.3). |
| On-page navigation | The current tab has aria-current="location". A click moves focus to the section, so a keyboard user continues reading there. The bar has an aria-label such as “On this page”. |
| Never | A tab as a div with a click handler. |