NS Design System
v3.0.0 · 257 tokens

48Tabs

Hairline underline, never a filled pill — the current tab is not an action, you are already there. The 2px underline is drawn transparent at rest so selecting never shifts the row.

Use it for

  • Peer views of one thing: Overview / Curriculum / Resources

Not for

  • Steps in a sequence — Steps or a wizard
  • Navigation between pages — links

Vertical

aria-orientation="vertical" is not decoration — tabs.js reads it to swap the arrow keys, and a vertical tablist answering to Left/Right is a keyboard trap in slow motion.

Name, avatar and the public bits.

Password, sessions, two-factor.

Plan and invoices.

markup
<div class="ns-tabs-layout">
  <div class="ns-tabs ns-tabs--vertical" role="tablist" aria-orientation="vertical" aria-label="Settings">
    <button class="ns-tab" role="tab" aria-selected="true" aria-controls="vt-1" id="vtab-1">Profile</button>
    <button class="ns-tab" role="tab" aria-selected="false" aria-controls="vt-2" id="vtab-2">Security</button>
    <button class="ns-tab" role="tab" aria-selected="false" aria-controls="vt-3" id="vtab-3">Billing</button>
  </div>
  <div role="tabpanel" id="vt-1" aria-labelledby="vtab-1" tabindex="0"><p class="ns-card__text">Name, avatar and the public bits.</p></div>
  <div role="tabpanel" id="vt-2" aria-labelledby="vtab-2" tabindex="0"><p class="ns-card__text">Password, sessions, two-factor.</p></div>
  <div role="tabpanel" id="vt-3" aria-labelledby="vtab-3" tabindex="0"><p class="ns-card__text">Plan and invoices.</p></div>
</div>

Pill

For switching a RENDERING rather than a section of content. If the thing being switched is a view, consider Segmented control instead.

Cards in a grid.

Rows in a list.

markup
<div class="ns-tabs ns-tabs--pill" role="tablist" aria-label="View">
  <button class="ns-tab" role="tab" aria-selected="true" aria-controls="pt-1" id="ptab-1">Grid</button>
  <button class="ns-tab" role="tab" aria-selected="false" aria-controls="pt-2" id="ptab-2">List</button>
</div>
<div role="tabpanel" id="pt-1" aria-labelledby="ptab-1" tabindex="0"><p class="ns-card__text">Cards in a grid.</p></div>
<div role="tabpanel" id="pt-2" aria-labelledby="ptab-2" tabindex="0"><p class="ns-card__text">Rows in a list.</p></div>

Icons and counts

The label always stays — an icon-only tab row is a memory test. A count is the one extra thing a tab can carry without becoming a menu.

Twelve comments.

Three flagged.

markup
<div class="ns-tabs" role="tablist" aria-label="Inbox">
  <button class="ns-tab" role="tab" aria-selected="true" aria-controls="it-1" id="itab-1"><i class="ph ph-chat-circle ns-tab__icon" aria-hidden="true"></i>Comments<span class="ns-tab__count">12</span></button>
  <button class="ns-tab" role="tab" aria-selected="false" aria-controls="it-2" id="itab-2"><i class="ph ph-flag ns-tab__icon" aria-hidden="true"></i>Flagged<span class="ns-tab__count">3</span></button>
</div>
<div role="tabpanel" id="it-1" aria-labelledby="itab-1" tabindex="0"><p class="ns-card__text">Twelve comments.</p></div>
<div role="tabpanel" id="it-2" aria-labelledby="itab-2" tabindex="0"><p class="ns-card__text">Three flagged.</p></div>

Default

markup
<div class="ns-tabs" role="tablist" style="max-inline-size:26rem">
  <button class="ns-tab" role="tab" aria-selected="true">Overview</button>
  <button class="ns-tab" role="tab" aria-selected="false" tabindex="-1">Curriculum</button>
  <button class="ns-tab" role="tab" aria-selected="false" tabindex="-1">Resources</button>
</div>

Accessibility contract

  • Roving tabindex: only the selected tab is tabbable; arrows move between tabs, Tab leaves to the panel (the React Tabs wires this)