What's new Report an issue
GIG Cymru NHS Wales, Digital Health and Care Wales

Tabs

Switch between views of the same record, without leaving the page.

Horizontal

The default. The selected tab is marked three ways — colour, weight and a 3px indicator — so selection never rests on colour alone. Click a tab, then use the arrow keys: the whole strip is one tab stop.

Patient summary content.
<div class="sr-tabs" role="tablist" aria-label="Patient record">
  <button type="button" class="sr-tabs__tab" role="tab" id="t-0"
          aria-selected="true" aria-controls="p-0" tabindex="0">Summary</button>
  <button type="button" class="sr-tabs__tab" role="tab" id="t-1"
          aria-selected="false" aria-controls="p-1" tabindex="-1">Results</button>
</div>
<div class="sr-tabs__panel" role="tabpanel" id="p-0" aria-labelledby="t-0" tabindex="0">…</div>
<div class="sr-tabs__panel" role="tabpanel" id="p-1" aria-labelledby="t-1" tabindex="0" hidden>…</div>

<!-- Arrow keys, Home/End and the roving tabindex are yours to wire up. -->

Counts, and a disabled tab

A count badge shows a quantity. It is hidden from assistive technology and the number is folded into the tab's name instead, so a screen reader announces "Results, 20 items" rather than just "Results". Arrow past the disabled tab — it is stepped over, never focused.

Patient summary content.
<div class="sr-tabs" role="tablist" aria-label="Patient record">
  <button type="button" class="sr-tabs__tab" role="tab" id="t-0"
          aria-selected="true" aria-controls="p-0" tabindex="0">Summary</button>
  <button type="button" class="sr-tabs__tab" role="tab" id="t-1"
          aria-selected="false" aria-controls="p-1" tabindex="-1">Results</button>
</div>
<div class="sr-tabs__panel" role="tabpanel" id="p-0" aria-labelledby="t-0" tabindex="0">…</div>
<div class="sr-tabs__panel" role="tabpanel" id="p-1" aria-labelledby="t-1" tabindex="0" hidden>…</div>

<!-- Arrow keys, Home/End and the roving tabindex are yours to wire up. -->

Vertical

For a side rail against a long record, and for labels too long to sit comfortably in a row. The indicator moves to the leading edge and the arrow keys become Up and Down.

Patient summary content.
<div class="sr-tabs" role="tablist" aria-label="Patient record">
  <button type="button" class="sr-tabs__tab" role="tab" id="t-0"
          aria-selected="true" aria-controls="p-0" tabindex="0">Summary</button>
  <button type="button" class="sr-tabs__tab" role="tab" id="t-1"
          aria-selected="false" aria-controls="p-1" tabindex="-1">Results</button>
</div>
<div class="sr-tabs__panel" role="tabpanel" id="p-0" aria-labelledby="t-0" tabindex="0">…</div>
<div class="sr-tabs__panel" role="tabpanel" id="p-1" aria-labelledby="t-1" tabindex="0" hidden>…</div>

<!-- Arrow keys, Home/End and the roving tabindex are yours to wire up. -->

Two levels

When a tab has sub-views, the second level is a separate, complete tablist inside the first tab's panel — not a tab with a chevron. A role="tab" controls exactly one panel and cannot have children, so a tab that opened a menu would announce itself as a tab and then do something else.

The second level is a pill so the two rows cannot be confused, and it is 32px against the parent's 40px — a child that outweighs its parent inverts the hierarchy. Arrow keys work within each level independently.

Blood results.
<div class="sr-tabs" role="tablist" aria-label="Patient record">
  <button type="button" class="sr-tabs__tab" role="tab" id="t-0"
          aria-selected="true" aria-controls="p-0" tabindex="0">Summary</button>
  <button type="button" class="sr-tabs__tab" role="tab" id="t-1"
          aria-selected="false" aria-controls="p-1" tabindex="-1">Results</button>
</div>
<div class="sr-tabs__panel" role="tabpanel" id="p-0" aria-labelledby="t-0" tabindex="0">…</div>
<div class="sr-tabs__panel" role="tabpanel" id="p-1" aria-labelledby="t-1" tabindex="0" hidden>…</div>

<!-- Arrow keys, Home/End and the roving tabindex are yours to wire up. -->

Sub-tabs and the Segmented control

Both are single-select and both fill the chosen option in brand blue. One line separates them:

Track = filter. No track = navigate.

The Segmented control sits in a grey track and sets an option within the view you are already looking at — it is a group of buttons with aria-pressed. A sub-tab pill has no track and changes which view you see — it is a role="tab" bound to a panel. The unselected pill carries a 1px Border/Strong outline, which is what tells you it is an option at all, so it clears the 3:1 that WCAG 2.2 SC 1.4.11 wants for a control boundary.

Switch between views of the same record, without leaving the page.


When to use

When not to use

How it works

One tablist, one panel region, exactly one selected tab. Selecting a tab shows its panel and hides the rest.

The selected tab is marked by colour, weight and a 3px indicator on its leading edge, under the label when horizontal, beside it when vertical. Three signals rather than one, so selection does not rest on colour alone.

Horizontal is the default. Vertical suits a side rail against a long record, and reads better when labels are long.

Options

OptionWhat it does
Orientationhorizontal (default) or vertical
Count badgeA small pill showing a quantity, 20 results, 3 tasks
DisabledRenders a tab unavailable. Prefer omitting it

A tab is a label, optionally with a count. There are no icon slots.

When a tab has sub-views

Use a second level, not a chevron. The outer tab's panel holds a complete second tablist of its own, Level=Secondary, the pill.

A tab cannot have children. role="tab" controls exactly one panel, so a tab that opened a menu would announce itself as a tab and then do something else. Two levels is the limit; a third means the structure needs rethinking, not another row.

Sub-tabs or the Segmented control? One line:

Track = filter. No track = navigate.

The Segmented control sits in a grey track and sets an option within the view you are looking at. A sub-tab pill has no track and changes which view you see. If choosing changes what is rendered below it, it is a sub-tab.

Do not use sub-tabs to anchor scrolling. A list that jumps to sections of a page that are all present at once is in-page navigation, not tabs, every panel but one is hidden in a tablist, which is the opposite of what a contents list does.


Do & don't

Accessibility

Content

Frameworks

FrameworkHow
HTML / CSS.sr-tabs with role="tablist". Port the keyboard behaviour, see the Storybook reference
React<Tabs tabs={[{ id, label, count, disabled, panel }]} /> from @dhcw/sr-react. Keyboard handling included
BlazorUse the shipped CSS classes; mirror the ARIA wiring and keyboard handling
.NET MAUINo tab strip is provided by the design system layer. Use MAUI's own tabbed shell with the SR tokens

The keyboard behaviour is the part that gets left out. A tablist that only responds to clicks is not a tablist, it is a row of buttons that a keyboard user cannot reach.

Clinical / DHCW notes

Related