Skip to content
ShineUI
Install

Tabs

Underline or pills.

<sui-tabs><sui-tab>PanelsStable

About

Tab bar drawn from child sui-tab elements, with only the active panel visible. underline marks in-page sections with the accent indicator; pills switches between views of the same data. A long bar scrolls under arrows over its ends and keeps the active tab in view. Reach for it when one region shows one of several panels.

Usage

<sui-tabs active="0" aria-label="Company settings"> <sui-tab label="Users"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Six seats are active. Two invitations are outstanding.</div></sui-tab> <sui-tab label="Roles"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Bookkeeper, Reviewer and Owner. Roles are assigned per company.</div></sui-tab> <sui-tab label="Audit trail"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Append only, kept for seven years.</div></sui-tab> </sui-tabs> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-tabs"); el.addEventListener("suiActiveChange", (e) => console.log(e.detail)); </script>
import { SuiTabs } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { return ( <SuiTabs active="0" aria-label="Company settings" onSuiActiveChange={(e) => console.log(e.detail)}> <sui-tab label="Users"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Six seats are active. Two invitations are outstanding.</div></sui-tab> <sui-tab label="Roles"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Bookkeeper, Reviewer and Owner. Roles are assigned per company.</div></sui-tab> <sui-tab label="Audit trail"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Append only, kept for seven years.</div></sui-tab> </SuiTabs> ); }
<script setup> import { SuiTabs } from "@shineui/vue"; import "@shineui/core/shineui.css"; const onChange = (e) => console.log(e.detail); </script> <template> <SuiTabs active="0" aria-label="Company settings" @sui-active-change="onChange"><sui-tab label="Users"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Six seats are active. Two invitations are outstanding.</div></sui-tab> <sui-tab label="Roles"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Bookkeeper, Reviewer and Owner. Roles are assigned per company.</div></sui-tab> <sui-tab label="Audit trail"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Append only, kept for seven years.</div></sui-tab></SuiTabs> </template>
import { Component } from "@angular/core"; import { SuiTabs } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiTabs], template: `<sui-tabs aria-label="Company settings" [(active)]="active"><sui-tab label="Users"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Six seats are active. Two invitations are outstanding.</div></sui-tab> <sui-tab label="Roles"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Bookkeeper, Reviewer and Owner. Roles are assigned per company.</div></sui-tab> <sui-tab label="Audit trail"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Append only, kept for seven years.</div></sui-tab></sui-tabs>`, }) export class Example { active = 0; }

Basic

Underline tabs are for in-page sections. The host aria-label names the tablist, which a screen with two bars needs; an unnamed host leaves it unnamed.

Six seats are active. Two invitations are outstanding.
Bookkeeper, Reviewer and Owner. Roles are assigned per company.
Append only, kept for seven years.
<sui-tabs active="0" aria-label="Company settings"> <sui-tab label="Users"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Six seats are active. Two invitations are outstanding.</div></sui-tab> <sui-tab label="Roles"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Bookkeeper, Reviewer and Owner. Roles are assigned per company.</div></sui-tab> <sui-tab label="Audit trail"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Append only, kept for seven years.</div></sui-tab> </sui-tabs>

Pills and stretch

Pills are for view switches. stretch shares the full width equally, which suits a bar over a table.

One line per account.
This period against the same period last year.
Twelve columns, one per month.
14 invoices, $47,630.00.
3 invoices, $5,455.00.
62 invoices this quarter.
<div style="display:flex;flex-direction:column;gap:18px"> <sui-tabs variant="pills" aria-label="Report shape"> <sui-tab label="Summary"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">One line per account.</div></sui-tab> <sui-tab label="Detailed comparative"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">This period against the same period last year.</div></sui-tab> <sui-tab label="By month"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Twelve columns, one per month.</div></sui-tab> </sui-tabs> <sui-tabs stretch aria-label="Invoice status"> <sui-tab label="Open"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">14 invoices, $47,630.00.</div></sui-tab> <sui-tab label="Overdue"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">3 invoices, $5,455.00.</div></sui-tab> <sui-tab label="Paid"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">62 invoices this quarter.</div></sui-tab> </sui-tabs> </div>

Pills with counts

A badge on a pill counts what waits behind that view. On the active pill the count takes the accent, with the ink fitted to it, so it stays readable on the brand fill in both modes.

14 bills, $47,630.00, waiting on a second signature.
Six bills queue for the Friday run.
62 bills this quarter.
<sui-tabs variant="pills" active="0" aria-label="Payment run"> <sui-tab label="To approve" badge="14"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">14 bills, $47,630.00, waiting on a second signature.</div></sui-tab> <sui-tab label="Scheduled" badge="6"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Six bills queue for the Friday run.</div></sui-tab> <sui-tab label="Paid"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">62 bills this quarter.</div></sui-tab> </sui-tabs>

Scrollable

A bar wider than its box scrolls on its own, and a new active tab scrolls into view. An arrow floats over each end you can still travel towards, on a fade of the card's surface; on another ground set --sui-tabs-ground, to var(--canvas) say. The arrows stay out of the tab order; swiping and scrolling work too.

Silver Hills Cafe, customer since 2021.
14 open, $47,630.00.
6 bills due this month.
Twelve statements on file.
Last reconciled Aug 31, 2026.
FY2026 budget, approved.
Profit and loss, balance sheet, cash flow.
Semimonthly, 12 employees.
Nine fixed assets under depreciation.
Fiscal year, tax rates, numbering.
Sep 1 to Sep 5, 2026.
Aug 1 to Aug 31, 2026.
Jul 1 to Sep 30, 2026.
Apr 1 to Jun 30, 2026.
Jan 1 to Sep 5, 2026.
Jan 1 to Dec 31, 2025.
<div style="display:flex;flex-direction:column;gap:18px;max-width:440px"> <sui-tabs active="4" aria-label="Company"> <sui-tab label="Overview"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Silver Hills Cafe, customer since 2021.</div></sui-tab> <sui-tab label="Invoices" badge="14"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">14 open, $47,630.00.</div></sui-tab> <sui-tab label="Bills"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">6 bills due this month.</div></sui-tab> <sui-tab label="Statements"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Twelve statements on file.</div></sui-tab> <sui-tab label="Reconcile"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Last reconciled Aug 31, 2026.</div></sui-tab> <sui-tab label="Budgets"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">FY2026 budget, approved.</div></sui-tab> <sui-tab label="Reports"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Profit and loss, balance sheet, cash flow.</div></sui-tab> <sui-tab label="Payroll"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Semimonthly, 12 employees.</div></sui-tab> <sui-tab label="Assets"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Nine fixed assets under depreciation.</div></sui-tab> <sui-tab label="Settings"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Fiscal year, tax rates, numbering.</div></sui-tab> </sui-tabs> <sui-tabs variant="pills" aria-label="Report period"> <sui-tab label="This month"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Sep 1 to Sep 5, 2026.</div></sui-tab> <sui-tab label="Last month"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Aug 1 to Aug 31, 2026.</div></sui-tab> <sui-tab label="This quarter"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Jul 1 to Sep 30, 2026.</div></sui-tab> <sui-tab label="Last quarter"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Apr 1 to Jun 30, 2026.</div></sui-tab> <sui-tab label="Year to date"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Jan 1 to Sep 5, 2026.</div></sui-tab> <sui-tab label="Last year"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Jan 1 to Dec 31, 2025.</div></sui-tab> </sui-tabs> </div>

Badges, icons and disabled

badge counts what waits inside a tab. A disabled tab grays out, refuses clicks, and the arrow keys step over it.

Six bills need a second pair of eyes.
Approved bills queue for the next payment run.
<sui-tabs active="0" aria-label="Inbox"> <sui-tab label="To review" icon="fa-regular fa-clock" badge="6"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Six bills need a second pair of eyes.</div></sui-tab> <sui-tab label="Approved" icon="fa-regular fa-circle-check"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Approved bills queue for the next payment run.</div></sui-tab> <sui-tab label="Archive" icon="fa-regular fa-folder" disabled></sui-tab> </sui-tabs>

Events

suiActiveChange carries the new active index; writing active from script moves the bar and emits nothing. It does not bubble, so nested tabs never report as the outer ones, while a field's suiChange inside a panel does. Roles are tablist, tab and tabpanel, with aria-selected and aria-controls.

Every posted line for 1000 Operating Checking.
Last reconciled Aug 31, 2026.
Twelve statements on file.
Register is active.
<div style="display:flex;flex-direction:column;gap:12px"> <sui-tabs id="tab-events" active="0" aria-label="Ledger views"> <sui-tab label="Register" badge="14"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Every posted line for 1000 Operating Checking.</div></sui-tab> <sui-tab label="Reconcile"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Last reconciled Aug 31, 2026.</div></sui-tab> <sui-tab label="Statements"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Twelve statements on file.</div></sui-tab> </sui-tabs> <div style="display:flex;gap:8px;align-items:center"> <sui-button id="tab-jump" variant="outline" size="sm" label="Open Statements from script"></sui-button> <span id="tab-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Register is active.</span> </div> </div>
// setup(root): properties and events setup(root) { const tabs = root.querySelector('#tab-events'); const log = root.querySelector('#tab-log'); tabs.addEventListener('suiActiveChange', (e) => { log.textContent = `suiActiveChange: index ${e.detail}, ${tabs.querySelectorAll('sui-tab')[e.detail].label}.`; }); root.querySelector('#tab-jump').addEventListener('click', () => { tabs.active = 2; log.textContent = 'Statements opened by script. No event fired.'; }); }

Bar without panels

Empty sui-tab children leave the panel area collapsed, so the bar works as a menu. Listen to suiActiveChange and route; the panels live elsewhere.

Route: /customers/silver-hills/overview
<div style="display:flex;flex-direction:column;gap:12px"> <sui-tabs id="tab-menu" active="0" aria-label="Customer"> <sui-tab label="Overview"></sui-tab> <sui-tab label="Invoices" badge="3"></sui-tab> <sui-tab label="Statements"></sui-tab> <sui-tab label="Notes"></sui-tab> </sui-tabs> <span id="tab-menu-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Route: /customers/silver-hills/overview</span> </div>
// setup(root): properties and events setup(root) { const log = root.querySelector('#tab-menu-log'); const routes = ['overview', 'invoices', 'statements', 'notes']; root.querySelector('#tab-menu').addEventListener('suiActiveChange', (e) => { log.textContent = `Route: /customers/silver-hills/${routes[e.detail]}`; }); }

Attributes are livesui-tab

The bar is drawn from each tab's label, icon, badge and disabled, and it redraws when one of them changes from script. A count can tick up as work arrives and a tab can stay locked until an earlier step is done, with no call on the parent.

Bills that need a second signature.
Approved bills queue for the next payment run.
Opens once the run has gone out.
<div style="display:flex;flex-direction:column;gap:12px"> <sui-tabs id="tab-live" active="0" aria-label="Approvals"> <sui-tab id="tab-live-queue" label="Waiting" badge="2"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Bills that need a second signature.</div></sui-tab> <sui-tab label="Approved"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Approved bills queue for the next payment run.</div></sui-tab> <sui-tab id="tab-live-paid" label="Paid" disabled><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Opens once the run has gone out.</div></sui-tab> </sui-tabs> <div style="display:flex;gap:8px;flex-wrap:wrap"> <sui-button id="tab-live-arrive" variant="outline" size="sm" label="A bill arrives"></sui-button> <sui-button id="tab-live-run" variant="outline" size="sm" label="Send the run"></sui-button> </div> </div>
// setup(root): properties and events setup(root) { const queue = root.querySelector('#tab-live-queue'); const paid = root.querySelector('#tab-live-paid'); root.querySelector('#tab-live-arrive').addEventListener('click', () => { queue.badge = Number(queue.badge || 0) + 1; }); root.querySelector('#tab-live-run').addEventListener('click', () => { paid.disabled = false; paid.label = 'Paid today'; }); }

Keyboard

KeyAction
Tab sui-tabsLands on the active tab, the only tab stop in the bar, and leaves the bar on the next press. The overflow arrows are not tab stops: the arrow keys already reach every tab and scroll it into view.
ArrowRight sui-tabsActivates the next enabled tab, wrapping to the first, and scrolls it into view. Under dir="rtl" the pair swaps, so the arrow that points along the reading direction is always the forward one.
ArrowLeft sui-tabsActivates the previous enabled tab, wrapping to the last, and scrolls it into view.
Home sui-tabsActivates the first enabled tab.
End sui-tabsActivates the last enabled tab.

API

<sui-tabs>

Properties

PropertyAttributeTypeDefaultDescription
activeactivenumber0Index of the active tab. Seeds the selection (the reference defaultActive) and tracks it afterwards; writing it from script moves the bar and emits nothing.
stretchstretchbooleanfalseUnderline tabs share the full width equally. Reflected.
variantvariant"pills" | "underline"'underline'underline for in-page sections, pills for view switchers.

Events

EventDetailReactVueDescription
suiActiveChangenumberonSuiActiveChange@sui-active-changeFires when the user picks another tab, or when tabs are removed and the element moves active back inside the count; detail is the new active index. Writing active from script emits nothing. Does not bubble, so tabs inside a panel never report as the outer tabs.

Methods

MethodSignatureDescription
refresh()refresh() => Promise<void>Re-reads the child tabs and redraws the bar. The bar already redraws when tabs are added or removed, and a sui-tab calls this itself after one of its own properties changes.
setFocus()setFocus() => Promise<void>Moves keyboard focus to the active tab, the bar's one tab stop.

Slots

SlotDescription
(default)sui-tab children, in order.

<sui-tab>

Properties

PropertyAttributeTypeDefaultDescription
badgebadgenumber | string | undefinedCount chip beside the label.
disableddisabledbooleanfalseGreys the tab and blocks activation.
iconiconstringstring | undefinedFont Awesome class drawn before the label, e.g. "fa-regular fa-user".
labellabelstring''Tab title shown in the bar.

Slots

SlotDescription
(default)Panel content, shown while this tab is active. Leave it empty to use the bar as a menu; the panel area then collapses. The children stay where you put them: the panel moves nothing, so a host may empty it and refill it at any time.

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

See also