CngxTabNav
projects/ui/tabs/tab-nav.component.ts
Import#
import { CngxTabNav } from '@cngx/ui/tabs'
Description#
CNGX tab-nav organism.
A role="navigation" landmark that wraps consumer-authored
<a cngxTabLink routerLink> anchors into a tab bar. Native parallel of
[cngxMatTabNav] in @cngx/ui/mat-tabs, and the link-driven sibling of
the programmatic <cngx-tab-group> tablist: a navigation of natively
focusable links is a different WAI-ARIA pattern than an automatic-
activation tablist, so it is a separate organism, not a skin flag.
Thin shell over CngxTabGroupPresenter via hostDirectives - the
registry, activeIndex, and activeId come from the same brain the
tablist organism and the Material bridge compose. There is no keyboard
model and no commit gating: links are natural Tab stops, Enter
activates the routerLink, and the router runs CanDeactivate natively.
Add [cngxTabsRouteSync] to drive the active link from the route - it
reflects NavigationEnd onto activeIndex; its commit-action stays
dormant because nothing here calls presenter.select().
CngxLiveRegion is mounted as a child <span> rather than a host
directive: its role="status" would clobber the host's role="navigation"
landmark. Its content is the active link's label (Pillar 2 - the active
change is announced).
<cngx-tab-nav cngxTabsRouteSync aria-label="Sections">
<a cngxTabLink id="overview" routerLink="overview">Overview</a>
<a cngxTabLink id="profile" routerLink="profile">Profile</a>
</cngx-tab-nav>
<router-outlet></router-outlet>See CngxTabGroup for the programmatic tablist organism.
Metadata#
Host#
Dependencies#
CNGX_TAB_GROUP_HOSTinject()presenterRelationships
Depends on1
Index#
Inputs#
Visual skin. Cascade input ?? config ?? 'line', reflected onto
[data-skin]. Resolved inline (single axis) rather than via
createTabsHostAttrs: that helper resolves five tablist axes the nav
does not use, so pulling it here would fabricate dead inputs - inline
resolution is Komposition statt Konfiguration (Pillar 3).
HostBindings#
| Binding | Expression |
|---|---|
[attr.data-skin] | resolvedSkin() |
[attr.data-orientation] | presenter.orientation() |
[attr.aria-orientation] | presenter.orientation() |
[attr.aria-label] | ariaLabel() |
[attr.aria-labelledby] | ariaLabelledBy() |
Structural CSS shared by every tab-group skin.
Layout-only - no colors, sizes, borders, or fonts beyond what is controlled via
--cngx-tab-* custom properties. CngxTabGroup (in @cngx/ui/tabs)references this file via styleUrls; ng-packagr inlines it at
publish time so consumers of @cngx/ui/tabs do not need
@cngx/common/tabs at runtime.
Headless directive use ([cngxTabGroup] on consumer-authored DOM) deliberately gets no
styling here - consumers bring their own visual opinion. Selectors
are class-prefixed (.cngx-tabs__*) and apply globally under the
component's encapsulation: None.
State modifiers
Driven by ARIA attributes set by the organism:
[aria-selected="true"]- active tab, paints the underline indicator via an inset box-shadow so layout does not shift between resting and active[aria-disabled="true"]- cursor: not-allowed, opacity dim via--cngx-tab-disabled-opacity[aria-busy="true"]- cursor: progress while a commit is pending -:focus-visible- outline ring via--cngx-tab-focus-ring.cngx-tab--rejected- persistent failed-target signal: outline ring + one-shot background pulse
Orientation
[data-orientation="vertical"]on the host - flips the strip into a left-column grid and changes scroll-snap to the Y axis
Slots
.cngx-tabs__strip-wrapper- outer row container.cngx-tabs__strip- the clip box (programmatic scroll only; the<cngx-tab-overflow>molecule is the user access path to tabs that do not fit).cngx-tabs__tab- one tab button.cngx-tabs__label- shrinkable label inside the button.cngx-tabs__badge- inline error badge; ships its own real-error-tone background so it does not collapse tocurrentColor(mirrors the stepper precedent).cngx-tabs__busy-spinner- pending-commit spinner.cngx-tabs__rejection-icon- persistent!glyph in a circle.cngx-tabs__panels/__panel- panel container + body.cngx-sr-only- locally-duplicated screen-reader utility
Inheritance
--cngx-tab-active-indicator-color->--cngx-color-primary(also pinned via:rootdelegation so the brand accent paints the underline in both light and dark modes without a 4-block)- Strip / tab geometry tokens (
--cngx-tab-gap,--cngx-tab-padding,--cngx-tab-strip-padding) - leaf - State-tone tokens (
--cngx-tab-error-badge-color,--cngx-tab-rejection-icon-color,--cngx-tab-rejected-outline,--cngx-tab-rejected-pulse-bg) - leaf, all pinned at the foundation's danger tone so they read against either surface
Reduced motion
Two hooks: under prefers-reduced-motion: reduce the busy spinner is force-stopped; the rejection pulse is gated behind
prefers-reduced-motion: no-preference so it never runs when the user has opted out.
Structural + line-skin styling for <cngx-tab-nav>. Pairs with the
shared tabs-base.css token vocabulary, loaded first via styleUrls
(see tab-nav.component.ts), where the --cngx-tab-* tokens are
@property-registered. A registered property is never unset, so a
use-site var(--token, fallback) fallback is dead - the registered
initial wins. The nav therefore SETs its base spacing tokens
(--cngx-tab-gap / --cngx-tab-padding / --cngx-tab-affordance-gap)
on :scope, anchored to the --cngx-space-* density scale, exactly as
<cngx-tab-group> does - they are inherits:true (or unregistered), so
the host SET cascades to the .cngx-tab-nav__link anchors and a root
[data-density] swap re-scales them. The inherits:false
--cngx-tab-focus-offset is instead SET on the consuming
.cngx-tab-nav__link element (a :scope SET never reaches it). The
per-skin @scope blocks below still read unregistered skin-local tokens
whose use-site fallbacks do fire.
The links are consumer-authored <a cngxTabLink> anchors; this file
styles the .cngx-tab-nav__link class the atom puts on each one.
--active mirrors aria-current="page", --error mirrors
aria-invalid. All five CngxTabsSkin values ship (line default,
plus pill, pill-outline, segmented, contained, each horizontal
and vertical), reflected onto [data-skin] and painted in the @scope
blocks below - matching <cngx-tab-group>. The nav has no inner strip
and no owned panel, so each skin is re-expressed on the host :scope
and the .cngx-tab-nav__link anchors rather than the group's
strip/tab/panel structure.
Index#
Layout
State / Active
State / Disabled
State / Focus
State / Error
State / Rejected
Layout
*0.5remGap between adjacent tab buttons and between a tab's icon / label / badge slots.
*0.5rem 0Padding shorthand of the tab strip (outer container).
*1.4Line height of the primary label line. Also drives the leading height the row-layout icon aligns to, so a stacked sub-label grows downward while the icon stays beside the primary label.
*0.8125remFont size of a tab's optional secondary label line. inherits: true so a consumer override set on the cngx-tab-group host
cascades down to the .cngx-tabs__sublabel descendant - with
inherits: false the host-level override is silently dropped.
Surface
*color-mix(in oklab, currentColor 65%, transparent)Text color of a tab's optional secondary label line. Defaults to a translucent tint of the tab's own text colour so it reads as a muted second line on any surface and in either colour scheme.
State / Active
<color>oklch(0.66 0.19 50)Underline color of the active tab - a 2px box-shadow inset stripe along the inline-end edge of the tab button.
Falls back to --cngx-color-primary so the brand accent paints the selection indicator without competing with text contrast.
See: [[--cngx-color-primary]]
<length>2pxThickness of the active-tab underline indicator.
State / Disabled
State / Focus
State / Error
<color>oklch(0.45 0.15 25)Background color of the inline error badge. inherits: true so a
themed value set on the cngx-tab-group / cngx-tab-nav host (the
Material bridge sets it there) reaches the descendant badge - with
inherits: false the host value never cascades down and the badge
keeps its initial tone. The default theme is unchanged: the initial
value inherits identically when nothing sets the token above.
State / Busy
*1remDiameter of the busy spinner shown while a tab's commit is pending.
State / Rejected
*1.25remDiameter of the persistent rejection icon (! glyph in a circle).
<color>oklch(0.45 0.15 25)Background color of the rejection icon disc. inherits: true so the
themed disc tone set on the host (the Material bridge) reaches the
descendant .cngx-tabs__rejection-icon - mirrors
--cngx-tab-error-badge-color. Default theme unchanged.
<color>oklch(1 0 0)Text color of the rejection icon glyph.
<color>oklch(0.45 0.15 25 / 0.1)Background wash of a rejected tab - a soft danger tint that settles in after the pulse, so the failed target stays legible without a harsh box. Static by default so it reads against either surface; the Material bridge remaps it onto the error role.
<length>0.375remCorner radius of the rejected-tab wash, so the tint reads as a soft highlight rather than a hard block.
<length>2pxThickness of the rejected tab's bottom accent bar - echoes the active indicator's language in the danger tone instead of a full outline ring.
*noneOutline shorthand of the rejected tab. none by default - the wash plus
the bottom accent carry the signal; set this to restore a full ring.
<length>1pxOutline offset of the rejected-tab outline.
<time>600msDuration of the rejected-tab pulse animation.
<color>oklch(0.45 0.15 25 / 0.18)Background tint used at the peak of the pulse animation.
State / Dismissable
*1.25remHit-target size of a tab's close button and the add-tab button.
*color-mix(in oklch, currentColor 14%, transparent)Hover/focus fill of the close / add affordance buttons. Defaults to a translucent tint of the current text colour, so it adapts to any surface and either colour scheme without a 4-block.