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.
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.
<app-tab> carries a title and its content; the first is active.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>
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.
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.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 takes a Bootstrap-Icons name; image takes a picture URL (a product logo/avatar) and wins over icon.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>
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.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>
Link tabs (navigation)
Give a tab an href and it renders as a real <a>. The browser then gives right-click “Open in new tab”, Ctrl/⌘-click and middle-click for free — the standard, accessible way to open a tab on the next screen. Mark the current one with active. target="_blank" opens it on the next screen on a plain click too (and gets rel="noopener noreferrer" unless you set your own).
No refresh: when a link tab also carries content (a pane), webority-ui.js switches it on a plain click without a page reload — the ink bar slides and the URL updates via history.pushState, just like the React version. Right-click / Ctrl-⌘-click still opens the real href in a new tab, and a direct load or refresh of that URL renders the correct tab active server-side (so the tab is genuinely deep-linkable). A link tab with no content stays a plain navigation link (a full page load), for when a tab really is a separate page.
<a href> that also carries content, so a plain click switches the pane without a reload (the URL updates + ink bar slides). Right-click / Ctrl-⌘-click still opens it in a new tab; the “Docs ↗” tab has no content, so it navigates out (target="_blank").?section=overview. Right-click a tab to open it in a new tab; refresh and the server renders the right tab active.?section=members.Show code
@{ var section = Request.Query["section"].FirstOrDefault() ?? "overview"; }
<app-tabs>
@* A link tab WITH content = deep-linkable, switches with no reload on a plain click *@
<app-tab title="Overview" href="?section=overview" icon="folder-open" active="@(section == "overview")">Overview content…</app-tab>
<app-tab title="Members" href="?section=members" count="8" tone="info" active="@(section == "members")">Members content…</app-tab>
<app-tab title="Billing" href="?section=billing" active="@(section == "billing")">Billing content…</app-tab>
@* No content = plain navigation link (full page load) *@
<app-tab title="Docs ↗" href="https://webority.dev" target="_blank"></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.
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.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.
aria-orientation is set to vertical and the arrow keys switch to Up/Down. The panes render beside the rail.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.
Switching content
Each tab owns the content it reveals — keep the tab set small so every view is one click away.
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.
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.Show code
<app-tabs activation="manual">
<app-tab title="Overview">…</app-tab>
<app-tab title="Activity">…</app-tab>
</app-tabs>
<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
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"
variant="pills"
size
size="sm"
size="md"
size="lg"
fitted
(default) — does not stretch
fitted="true"
orientation
orientation="horizontal" (default)
orientation="vertical"
activation
activation="automatic" (default) — arrows also select
activation="manual" — arrows move focus only, Enter/Space selects
per-tab: icon, count & tone
icon="folder-open"
count="12"
count="12" tone="success"
count="3" tone="warning"
count="1" tone="danger"
count="5" tone="info"
icon + count + tone
per-tab: state
danger="true" — as the active tab (ink bar turns red)
disabled="true" — not the first (active) tab
disabled="true" — as the first tab (falls back to the first enabled tab)
<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
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
| variant | underline | pills | underline | The visual treatment of the tab strip. |
| orientation | horizontal | vertical | horizontal | Vertical renders a side rail; arrows switch to Up/Down. |
| size | sm | md | lg | md | Tab padding and font size. |
| fitted | bool | false | Tabs share the width equally instead of scrolling. |
| activation | automatic | manual | automatic | Whether arrow keys also select, or only move focus. |
| (app-tab) title | string | - | The label of a tab. Each <app-tab> child is one tab. |
| (app-tab) disabled | bool | false | Renders the tab as unavailable — it can't be selected. |
| (app-tab) icon | string | - | Library icon name shown before the label. |
| (app-tab) tooltip | string | - | 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) count | int? | - | A count badge shown after the label. |
| (app-tab) tone | success | warning | danger | info | - | Tone tint for the count badge. |
| (app-tab) danger | bool | false | Destructive styling for the tab. |
| (app-tab) href | string | - | 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) target | string | - | Anchor target for a link tab; _blank opens it on the next screen on a plain click (React target). |
| (app-tab) rel | string | - | Anchor rel for a link tab; defaults to noopener noreferrer when target="_blank" (React rel). |
| (app-tab) active | bool | false | Marks 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 attrs | string | - | 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-label | string | - | 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. |