Layout & navigation

AppTabs

Switches between sibling views in the same context — one strip of tabs, one active view at a time.

<app-tabs><app-tab title="Overview">…</app-tab></app-tabs>

Tabs let a user move between sibling views of the same thing — the sections of a record, the filters on a list — without leaving the page. <app-tabs> renders the strip; exactly one tab is active at a time.

The active tab is marked by a sliding ink bar that springs between tabs with an elastic overshoot — a moving pill in the pills variant, a moving edge when vertical. It's measured, so it fits variable-width tabs, and it snaps instantly under prefers-reduced-motion. The look comes entirely from the shared @webority/theme, so the Razor <app-tabs> and the React <AppTabs> animate identically.

Playground

Every attribute, live. Each change re-renders the real tag helpers on the server, so the markup underneath is the exact call that produced the strip above it.

Overview.
Activity.
Settings.
Archived.
<app-tabs aria-label="Playground tabs">
    <app-tab title="Overview">Overview.</app-tab>
    <app-tab title="Activity">Activity.</app-tab>
    <app-tab title="Settings">Settings.</app-tab>
    <app-tab title="Archived">Archived.</app-tab>
</app-tabs>

The tab strip

A row of tabs with one active. Compose the strip from its tabs; the active tab shows its pane, and Bootstrap drives the switching.

A tab strip with panes
Each <app-tab> carries a title and its content; the first is active.
Overview — the high-level summary of the project.
Activity — the running log of recent changes.
Settings — configuration for this project.
Show code
<app-tabs>
    <app-tab title="Overview">Overview — the high-level summary of the project.</app-tab>
    <app-tab title="Activity">Activity — the running log of recent changes.</app-tab>
    <app-tab title="Settings">Settings — configuration for this project.</app-tab>
</app-tabs>
Tabs are for peers, not stepsEvery tab shows a different view of the same subject, and the user can visit them in any order. If the views are ordered steps that must be completed in sequence, that's a wizard, not tabs.

Emphasis & state

Beyond a plain label, a tab can signal more — extra emphasis, a special or unavailable state, or a different visual treatment — so the strip communicates state, not just names.

Icons, counts, tone, and states
icon, count and tone add signal to a label; danger marks a destructive view; disabled can't be selected. Set variant="pills" on the strip to restyle it.
Overview.
Activity.
A destructive view.
Archived records.
Show code
<app-tabs>
    <app-tab title="Overview" icon="folder-open" count="12" tone="success">Overview.</app-tab>
    <app-tab title="Activity" icon="bell" count="3" tone="warning">Activity.</app-tab>
    <app-tab title="Delete" danger="true">A destructive view.</app-tab>
    <app-tab title="Archived" disabled="true">Archived records.</app-tab>
</app-tabs>
Icon by name & custom image
icon takes a Bootstrap-Icons name; image takes a picture URL (a product logo/avatar) and wins over icon.
Home.
Settings.
Acme workspace.
Globex workspace.
Show code
<app-tabs>
    <app-tab title="Home" icon="home">Home.</app-tab>
    <app-tab title="Settings" icon="setting-2">Settings.</app-tab>
    <app-tab title="Acme" image="/logos/acme.png" image-alt="Acme">Acme workspace.</app-tab>
    <app-tab title="Globex" image="/logos/globex.png" image-alt="Globex">Globex workspace.</app-tab>
</app-tabs>
Per-tab tooltip
tooltip adds a hover/focus hint on a tab — the same branded look <app-tooltip> renders, drawn by the CSS-only tip so it never collides with the tab's own Bootstrap instance. Use it to expand a terse label or explain an icon-only tab. Hover (or tab to) a tab to see it.
Overview.
Activity.
Settings.
Show code
<app-tabs>
    <app-tab title="Overview" tooltip="The high-level summary of the project">Overview.</app-tab>
    <app-tab title="Activity" tooltip="Every recent change, newest first">Activity.</app-tab>
    <app-tab title="Settings" tooltip="Configuration for this project">Settings.</app-tab>
</app-tabs>

Variant, size & fit

One underline strip, one pill strip — same behaviour, different chrome. Sizes and a fitted (equal-width) layout tune it to the space.

Pills, sizes, fitted
variant="pills" turns the underline into a segmented control (the ink bar becomes the moving pill); size is sm | md | lg; fitted stretches the tabs to share the width equally.
Overview.
Activity.
Settings.
Small strip.
Compact.
Large strip.
Roomy.
One.
Two.
Three.
Show code
<app-tabs variant="pills">…</app-tabs>
<app-tabs size="sm">…</app-tabs>
<app-tabs size="lg">…</app-tabs>
<app-tabs variant="pills" fitted="true">…</app-tabs>

Vertical orientation

orientation="vertical" stands the strip on its side — a settings-style rail with the ink bar as a moving left edge. Arrow Up/Down move between tabs.

A side rail
aria-orientation is set to vertical and the arrow keys switch to Up/Down. The panes render beside the rail.
Overview — the high-level summary of the project.
Activity — the running log of recent changes.
Settings — configuration for this project.
Show code
<app-tabs orientation="vertical">
    <app-tab title="Overview">Overview.</app-tab>
    <app-tab title="Activity">Activity.</app-tab>
    <app-tab title="Settings">Settings.</app-tab>
</app-tabs>

Overflow

When the tabs are wider than the strip, they don't just crop — the edges fade, chevron buttons appear, and selecting a tab (or arrowing to it) scrolls it into view.

Too many tabs to fit
No wrap, no silent cut-off. The active tab is kept in view; the chevrons appear only on the side that overflows.
Overview.
Activity.
Settings.
Members.
Integrations.
Billing.
Security.
API.
Webhooks.
Audit log.
Exports.
Danger zone.
Prefer few tabs anywayOverflow is a safety net, not a licence for twenty tabs. If a strip routinely overflows, group the sections or move the rarely-used ones into a menu.

Switching content

Each tab owns the content it reveals — keep the tab set small so every view is one click away.

Tabs above the panel they control
The content of the active tab shows beneath the strip.
The project's core details live here.
Everyone with access to the project.
Show code
<app-tabs>
    <app-tab title="Details">The project's core details live here.</app-tab>
    <app-tab title="Members">Everyone with access to the project.</app-tab>
</app-tabs>

Keyboard & activation

<app-tabs> is a full WAI-ARIA tablist — the arrow keys roam the strip and skip disabled tabs. How selection follows focus is your choice.

Manual activation
activation="manual": arrow keys move focus without selecting; Enter or Space commits. Use it when rendering a pane is expensive, so arrowing past tabs doesn't fire each one. The default, automatic, selects as focus moves.
Overview — arrow to me, then press Enter.
Activity — selected only on Enter/Space.
Settings — focus moved here without selecting.
Show code
<app-tabs activation="manual">
    <app-tab title="Overview">…</app-tab>
    <app-tab title="Activity">…</app-tab>
</app-tabs>
Keyboard support is built inArrow Left/Right (or Up/Down when vertical) move between tabs, Home/End jump to the first/last, and disabled tabs are skipped. Don't wire your own key handling — <app-tabs> already carries it.

When to use

Use it when

  • A record or page has a few peer sections — Overview, Activity, Settings — the user switches between.
  • A list needs quick filters (All / Active / Archived) that swap the rows in place.
  • The set of views is small and stable, so every one stays a single click away.

Reach for something else when

  • The views are ordered steps that must be completed in sequence. → a wizard / stepper
  • There are many options to choose from. → AppSelect
  • Each view is a full destination with its own URL. → router navigation
  • You are toggling a single on/off setting. → AppSwitch

Best practices

Do
Overview.
Activity.
Settings.
A few short, peer labels — the user sees every view at a glance.
Don't
Overview.
Activity.
Settings.
Members.
Integrations.
Billing.
Advanced.
Too many tabs wrap and become a scanning chore — group them or move some into a menu.
Do
All records.
Only open records.
Short, distinct labels keep the strip scannable at a glance.
Don't
All.
Open.
Sentence-long labels break the scannable rhythm a tab strip depends on.

Every option

The whole surface of the component, one cell per value — variant, orientation, size, fitted, activation, and every per-tab decoration.

variant

variant="underline"
Overview.
Activity.
Settings.
variant="pills"
Overview.
Activity.
Settings.

size

size="sm"
Overview.
Activity.
Settings.
size="md"
Overview.
Activity.
Settings.
size="lg"
Overview.
Activity.
Settings.

fitted

(default) — does not stretch
Overview.
Activity.
Settings.
fitted="true"
Overview.
Activity.
Settings.

orientation

orientation="horizontal" (default)
Overview.
Activity.
Settings.
orientation="vertical"
Overview.
Activity.
Settings.

activation

activation="automatic" (default) — arrows also select
Overview.
Activity.
Settings.
activation="manual" — arrows move focus only, Enter/Space selects
Overview.
Activity.
Settings.

per-tab: icon, count & tone

icon="folder-open"
Overview.
Tagged.
Settings.
count="12"
Overview.
Tagged.
Settings.
count="12" tone="success"
Overview.
Tagged.
Settings.
count="3" tone="warning"
Overview.
Tagged.
Settings.
count="1" tone="danger"
Overview.
Tagged.
Settings.
count="5" tone="info"
Overview.
Tagged.
Settings.
icon + count + tone
Overview.
Tagged.
Settings.

per-tab: state

danger="true" — as the active tab (ink bar turns red)
Tagged — first, so it starts active.
Overview.
Settings.
disabled="true" — not the first (active) tab
Overview.
Tagged.
Settings.
disabled="true" — as the first tab (falls back to the first enabled tab)
Tagged.
Overview.
Settings.
The active tab skips a disabled first tabWhen the first <app-tab> is disabled="true", the tag helper marks the first non-disabled tab active instead — matching React's AppTabs, which falls back to the first enabled tab when value names a disabled one. If every tab is disabled there's nothing to fall back to, so the first tab stays active.

overflow — many tabs

12 tabs in a 360px strip — edges fade, chevrons appear
Overview.
Activity.
Settings.
Members.
Integrations.
Billing.
Security.
API.
Webhooks.
Audit log.
Exports.
Danger zone.

Attributes

AttributeTypeDefaultDescription
variantunderline | pillsunderlineThe visual treatment of the tab strip.
orientationhorizontal | verticalhorizontalVertical renders a side rail; arrows switch to Up/Down.
sizesm | md | lgmdTab padding and font size.
fittedboolfalseTabs share the width equally instead of scrolling.
activationautomatic | manualautomaticWhether arrow keys also select, or only move focus.
(app-tab) titlestring-The label of a tab. Each <app-tab> child is one tab.
(app-tab) disabledboolfalseRenders the tab as unavailable — it can't be selected.
(app-tab) iconstring-Library icon name shown before the label.
(app-tab) tooltipstring-A hover/focus hint on the tab, rendered as the branded CSS-only [data-wui-tip] bubble — not a Bootstrap tooltip, which cannot bind to a tab button that already owns a bs.tab instance (React tooltip, which uses AppTooltip).
(app-tab) countint?-A count badge shown after the label.
(app-tab) tonesuccess | warning | danger | info-Tone tint for the count badge.
(app-tab) dangerboolfalseDestructive styling for the tab.
(app-tab) hrefstring-Turns the tab into a real link (<a href>) instead of a panel-switching button — the browser gives right-click “Open in new tab”, Ctrl/⌘-click and middle-click for free (React href/to). A link tab navigates and owns no pane.
(app-tab) targetstring-Anchor target for a link tab; _blank opens it on the next screen on a plain click (React target).
(app-tab) relstring-Anchor rel for a link tab; defaults to noopener noreferrer when target="_blank" (React rel).
(app-tab) activeboolfalseMarks this tab as current — use with link tabs to flag the one matching the route. Panel tabs may omit it (first enabled is active). React derives this from value.
(app-tab content)markup-The pane shown when the tab is active (inner content).
style / class / other attrsstring-Unbound attributes on <app-tabs> pass through to the rendered outer div.app-tabs-group (matches React's style/className, but lands on the wrapper, not the role="tablist" element).
aria-labelstring-React-only equivalent is ariaLabel, which sets aria-label directly on the role="tablist" element. Razor has no bound equivalent — an aria-label on <app-tabs> lands on the outer wrapper div instead, so it does not name the tablist the way React's does.