Components · Navigation

Tabs

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.

Design Tokens

Resolved from the live token package.

StatePartTokenValue

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).


Sizes

SizeMobile and tablet (below 769px)Desktop (769px+)
LargeLabel 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)
MediumLabel 16/16, 0.32px · gap 8px · tab 54px (bar 27px)Label 18/18, 0.18px · gap 12px · tab 60px (bar 33px)
SmallLabel 14/14, 0.42px · gap 8px · tab 52px (bar 25px)Label 16/16, 0.48px · gap 8px · tab 54px (bar 27px)
Icon onlyTab 30px · icon 20px · gap 8pxTab 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.


Measurements

Dimensions

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.

Breakpoint
A tab at true size (1:1). Change the Breakpoint to see the mobile values.
AttributeValue
Tab, label only (Large)56px with an icon, 29px bar without (mobile); 60px and 33px from 769px
Padding below the label or iconGap 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, PrimaryNone. 24px between tabs
Padding, left and right, Secondary16px, no gap between tabs
Icon-only tabSame bottom padding (10px mobile, 14px desktop for Large and Medium); no side padding
Icon20px, 8px above the label
Leading icon in the label24px (20px on Small below 769px), 4px before the text
Active line3px Primary, 2px Secondary
Bar divider1px
Focus ring2px, 4px clear above, left and right; 6px above the tab’s bottom edge

Spacing

The same distances as spacing tokens.

PartValue
Between Primary tabsspace-24
Secondary tab padding, left and rightspace-16
Icon to label, status to label, leading icon to textspace-8 and space-4
Label to line8px, 10px or 12px (see Sizes): literals
Focus ring room4px: 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.


States

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.

Type
Size
Breakpoint
StateLabel + iconLabelIcon onlyRule

Typography

SizeTypeMobile (below 769px)Desktop (769px+)
Largelabel-lg16/16, 0.32px18/18, 0.18px
Mediumlabel-lg16/16, 0.32px18/18, 0.18px
Smalllabel-md14/14, 0.42px16/16, 0.48px
Number badge—10/10, no letter spacing10/10, no letter spacing

Breuer Condensed, weight 600 (Figma bold 700 − 100), uppercase, with ss02 and ss03.


Structure

Elements

ElementClassNotes
Bartab-barThe row plus the 1px divider (border-primary).
Tabtab, tab--active, tab--icon-onlyA button (content) or a (navigation).
Icontab__icon with ms20px, above the label.
Labeltab__labelUppercase. Optional leading ms icon.
Statustab__dot, tab__count6px dot, or a red pill with a number (16px high).
Sliding linetab-bar__indicatorAdded by tabs.js.
Paneltab-panelContent of a content tab. role="tabpanel".

Types

ModifierEffect
.tab-bar--primary24px gap between tabs; line spans the label only; active line 3px
.tab-bar--secondaryNo gap, each tab has 16px side padding, line spans label and padding; active line 2px
.tab-bar--md, .tab-bar--smMedium and Small. Default (no class) is Large.
.tab-bar--on-greyHover line surface-40 instead of surface-10
.tab-bar--pageOn-page navigation: the bar is sticky and the tabs are #anchor links (scrollspy and jump in tabs.js)
--tab-sticky-topCustom property: distance from the top of the window to a tab-bar--page bar, for a fixed header above it (default 0)
--tab-line-activeCustom property: colour of the active line (default surface-100)

Width

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.


Motion

PropertyDurationEasing
Sliding active line: transform and widthduration-medium-2ease-standard
Hover line grows from the bottomduration-medium-2ease-decelerate-generic
Hover line colour fadesduration-medium-2ease-standard
Label and icon colourInstant—

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.


Behavior

TopicRule
KeyboardTab 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 tabsThe selected tab has tabindex="0", the others -1. Exactly one tab is active.
Navigation tabsLinks in a nav, the current page has aria-current="page". Link tabs.js for the sliding line.
Accent colourThe 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.
OverflowThe bar scrolls sideways with the scrollbar hidden; the active tab is scrolled into view. The divider runs the full width.
On-page navigationnav.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.

Accessibility Requirements

TopicRequirement
Content tabsrole="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 tabsLinks in a nav with an aria-label; the current page has aria-current="page".
Icon onlyNeeds an aria-label. A status dot or number needs a text alternative where its meaning is not clear.
Focus2px ring border-focus on :focus-visible (WCAG 2.4.7, 1.4.11). The active line stays visible.
MotionThe sliding line is off under prefers-reduced-motion (WCAG 2.3.3).
On-page navigationThe 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”.
NeverA tab as a div with a click handler.