Tabs
Free6 visual styles (underline, pills, boxed, lifted, border-card, unstyled), dismissible and scrollable tabs, vertical layout, keyboard navigation and lazy panels.
</>sv-tabs tabsnavigationpanelsclosable
import { SvTabsComponent } from '@styloviz/tabs';Demo & usage
Basic usage
Usage example — open the gallery for the full interactive demo.
An interactive demo for this component lives in the gallery.
Open live demoMore examples
Pills, full-width
example.htmlAngular
1 lines
<sv-tabs [tabs]="tabs" tabsStyle="pills" [fullWidth]="true" activeId="overview" />
Dismissible content tabs
example.htmlAngular
1 lines
<sv-tabs [tabs]="panels()" [(activeId)]="activePanel" tabsStyle="boxed" (tabClose)="dismiss($event)" />
Scrollable overflow
example.htmlAngular
1 lines
<sv-tabs [tabs]="manyTabs" [(activeId)]="activeTab" [scrollable]="true" />
When to use
- Job: local panels — owns its panels and renders no links; whether the active tab lives in the URL is your decision (compare all seven in the “Choosing a navigation pattern” guide under Guides).
- Switching between sibling views of the same context without leaving the page.
- Detail panes, settings sections, dashboards with multiple perspectives.
- Letting the user dismiss a panel the author permits them to hide (`closable` + `tabClose`) — the author still owns the set.
- Not for multi-document or workspace bars where the user opens, closes, reorders and persists tabs — that is a separate document-tabs component (`NV-06-V1`, see `docs/navigation-catalog-adr.md`), not a configuration of this one.
- For a simple value-only toggle with no content panels, use the Segmented Control component instead.
Async states
None — this component has nothing to load, so it has no loading, empty or error state to drive.
Inputs
| Name | Type | Default | Description |
|---|---|---|---|
| tabs | TabItem[] | [] | Tab definitions in display order. |
| activeId | string (two-way) | '' | ID of the currently active tab. Use `[(activeId)]` for two-way binding. |
| tabsStyle | TabsStyle | 'underline' | Visual style variant. |
| size | TabsSize | 'md' | Size of the tab text and icons. |
| layout | TabsLayout | 'horizontal' | Layout direction. |
| fullWidth | boolean | false | Whether tab buttons stretch to fill all available width. Only applicable to the `'horizontal'` layout. |
| scrollable | boolean | false | Allows the tab list to scroll horizontally instead of wrapping/overflowing when there are more tabs than fit the available width. |
| customClass | string | '' | Additional CSS classes on the root wrapper. |
| activation | TabsActivation | null | null | Whether the arrow keys select the tab they move to (`'automatic'`) or only move focus, leaving Enter/Space to select (`'manual'`). Prefer `'manual'` when a panel costs something to show — `lazy` panels that fetch, heavy charts, anything a keyboard user should not trigger five times on the way past. Leaving it unset resolves to `'automatic'`, and logs a dev-only warning if any tab is `lazy`. |
| lazyRetention | TabsLazyRetention | 'retain' | What happens to a `lazy` panel the user switches away from: `'retain'` keeps it mounted with its state (the long-standing behaviour), `'destroy'` unmounts it and rebuilds it on the next visit. Only `lazy` tabs are affected — a panel that was never deferred is never destroyed. Choose `'destroy'` for panels that must re-read their data on every visit, or hold enough DOM that keeping several alive is the real cost; and know that it discards whatever the user typed. |
| urlKey | string | null | null | Put the active tab in the URL under this key, so a tab can be shared and survives a refresh. Needs `provideSvNavLocation()` at the app root; give a nested tab strip a different key. |
| surfaceId | string | null | null | Names this tabset in the shared navigation-analytics stream, when a page has more than one. Ignored unless `provideSvNavTelemetry()` is wired up. |
Outputs
| Event | Payload type | Description |
|---|---|---|
| tabClose | TabItem | Emitted when a closable tab's × is clicked or Delete/Backspace is pressed on it. The tab is NOT removed automatically — remove it from `tabs` yourself. |
| unresolved | SvUnresolvedValue<string> | Emitted when `activeId` names a tab this component cannot show. Reports; never corrects, because there is no nearest tab to correct to. |
| activeIdChange | string | Emits the new `activeId` whenever it changes — the output half of `[(activeId)]`. |
Customization & theming
- Underline, pills, boxed, lifted, border-card, or unstyled styles.
- Horizontal or vertical orientation.
- Optional icon, numeric badge, unread dot, and a dismiss (×) affordance per tab via `closable`.
- Automatic horizontal scroll for long tab lists — a horizontal tablist scrolls on overflow (e.g. many tabs on a phone) with no scrollbar while the tabs fit; `scrollable` keeps tabs at their natural width instead of shrinking to fit first.
- Lazy-rendered panels.
Best practices
- Do: Keep tab labels to one or two words and a manageable count.
- Do: Lazy-render heavy panels so only the active tab pays the cost.
- Do: When using `closable`, remove the tab from your own `tabs` array on `(tabClose)` — the component never mutates it for you.
- Don't: Don't use tabs for sequential steps — that's a Stepper.
- Tip: Reach for `unstyled` when you need full control over the visual treatment — every tab exposes `data-active`/`data-disabled` attributes for custom CSS.
Accessibility
- Tablist/tab/tabpanel roles with arrow-key navigation (←→/↑↓, Home, End).
- Active tab exposes selected state; roving tabindex.
- Active panel is focusable (tabindex 0) so keyboard users land in its content.
- Dismissible tabs close from the keyboard with Delete or Backspace on the focused tab (announced via `aria-keyshortcuts` and a visually hidden hint). The × is a pointer-only, `aria-hidden` affordance and is not focusable, because the ARIA tablist pattern leaves no valid place for a second focusable control inside a `role="tab"`.
Feedback on Tabs
Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.