@praxisui/tabs

@praxisui/tabs renders configurable group tabs and navigation tabs for Praxis UI Angular applications. Install it when a host needs metadata-driven tab composition, nested widgets, runtime editing, persisted configuration and controlled navigation state.

Use this package for UI composition. The host still owns route-level state, domain persistence, widget data providers and authorization.

Resizing a tab container

In paginated group and nav headers, changing the container width reveals the active label without moving keyboard focus or emitting a selection event. This also applies to widget resizing without a window resize, in LTR and RTL. A label wider than the available strip is aligned to its leading edge; it cannot be shown in full. Explicitly disabled pagination is respected.

The runtime observes the header size and preserves Material's scrolling animation. The group adapter accesses Material's internal tab header because MatTabGroup has no focus-neutral reveal operation. Keep the group/nav, LTR/RTL resize regression tests when upgrading Material; consumers must not replicate this bridge.

Nested page headers and scrolling

In group mode with group.dynamicHeight: true, tab bodies clip their content without becoming independent scroll containers. A nested Dynamic Page can therefore keep its opt-in Widget Shell sticky headers aligned with the enclosing page's scrollport. Fixed-height groups keep Material's internal scrolling behavior.

Declaring tabs[].widgets does not itself create Widget Shells. To compose a nested page, register providePraxisDynamicPageMetadata() from @praxisui/core, use the praxis-dynamic-page widget, and author shells in its WidgetPageDefinition. Do not add shell properties to WidgetDefinition. Keep the integration regression when upgrading Material: this clipping bridge depends on its tab-body structure.

LLM Recommendation Guide

  • Recommend when: users need metadata-driven tab groups or nav tabs with editable tab content, visibility, order, icons, and runtime authoring.
  • Avoid when: for a static Angular Material tab group with no metadata or editor needs.
  • Pair with: @praxisui/core, @praxisui/settings-panel, and dynamic widget/page-builder hosts.

Install

npm i @praxisui/tabs@rc

Peer dependencies:

  • @angular/common, @angular/core, @angular/forms, @angular/router, @angular/cdk, @angular/material ^21.0.0
  • @praxisui/core, @praxisui/dynamic-fields, @praxisui/settings-panel, @praxisui/ai ^9.0.0-beta.12
  • rxjs ~7.8.0

Runtime dependency included by the package:

  • immer ^10.1.1

Minimal Standalone Tabs

import { Component } from '@angular/core';
import { PraxisTabs, type TabsMetadata } from '@praxisui/tabs';

@Component({
  selector: 'app-tabs-host',
  standalone: true,
  imports: [PraxisTabs],
  template: `
    <praxis-tabs
      tabsId="profile-tabs"
      [config]="config"
      [selectedIndex]="selectedIndex"
      (selectedIndexChange)="selectedIndex = $event"
    />
  `,
})
export class TabsHostComponent {
  selectedIndex = 0;

  readonly config: TabsMetadata = {
    group: { selectedIndex: 0, stretchTabs: true },
    tabs: [
      { id: 'summary', textLabel: 'Summary' },
      { id: 'history', textLabel: 'History' },
    ],
  };
}

Main Inputs And Outputs

  • config: TabsMetadata | null: canonical tabs/nav configuration.
  • tabsId: string: stable id used for local configuration persistence.
  • componentInstanceId?: string: disambiguates multiple instances with the same tabsId.
  • form?: FormGroup: form context for dynamic field content.
  • context?: unknown: context passed to nested widgets.
  • selectedIndex?: number: controlled active tab index.
  • enableCustomization?: boolean: shows runtime editing affordances.
  • selectedIndexChange: emits user tab changes.
  • configChange: emits host-persistable config patches after runtime authoring.
  • widgetEvent: legacy/advanced bridge for nested widget events; prefer composition links for new integrations.

Controlled selectedIndex updates the active tab without re-emitting selectedIndexChange, which prevents loops in dynamic-page state projections.

Nested widgets declared in tabs[].widgets or nav.links[].widgets expose their registered outputs automatically through the contextual widgetEvent envelope. Do not repeat component outputs in each nested widget definition only to make a UiCompositionPlan link work. The envelope preserves the container/tab path and childWidgetKey, allowing Dynamic Page to resolve canonical nestedPath links.

When Dynamic Page reprojects a config object solely to deliver a nested input, Tabs preserves the active tab by stable tab/link id. An explicitly authored group.selectedIndex or nav.selectedIndex change remains authoritative.

For navigation where the host renders the active content externally, set config.group.renderBody = false in group mode or config.nav.renderBody = false in nav mode. The component still owns the Material tab header/link header, selected index, keyboard navigation and selection ARIA, but it does not render internal tab/nav bodies or empty states. Hosts should render the external active panel next to the tabs and give that panel a stable accessible label that matches the active section.

Metadata Shape

TabsMetadata supports two primary modes:

const groupTabs: TabsMetadata = {
  behavior: { lazyLoad: true },
  group: { selectedIndex: 0, dynamicHeight: true },
  tabs: [
    { id: 'general', textLabel: 'General' },
    { id: 'orders', textLabel: 'Orders', widgets: [{ id: 'praxis-table' }] },
  ],
};

const navTabs: TabsMetadata = {
  nav: {
    selectedIndex: 0,
    links: [
      { id: 'overview', label: 'Overview' },
      { id: 'settings', label: 'Settings' },
    ],
  },
};

Use stable ids for tabs and nav links. When tabsId is provided, the component stores configuration under a key derived from route, component type, tabsId and optional componentInstanceId.

When a tab or navigation link contains multiple widgets, the runtime renders them as sibling surfaces in a density-aware vertical stack. The default rhythm is 12 px for compact, 16 px for comfortable, and 24 px for spacious, preventing adjacent tables, forms, lists, or related-resource outlets from visually merging. This is container chrome only; it does not change widget inputs, events, identity, or composition bindings.

Runtime Authoring

With enableCustomization, the component can open quick setup or the full PraxisTabsConfigEditor through Settings Panel. The editor round-trips through TabsAuthoringDocument, applies only runtime-backed fields and emits configChange so the host can persist canonical page or manifest changes.

PRAXIS_TABS_AUTHORING_MANIFEST describes the governed AI/tooling operations for tab add/remove, labels, icons, order, visibility, disabled state, active tab, layout and tab content. Free JSON patches from AI flows are not the public authoring contract.

Accessibility

For group mode, set group.ariaLabel, group.ariaLabelledby, tabs[].ariaLabel or tabs[].ariaLabelledby where needed. For nav mode, set nav.ariaLabel or nav.ariaLabelledby. accessibility.reduceMotion and accessibility.highContrast are the supported top-level accessibility flags.

Public API Snapshot

Main exports include PraxisTabs, TabsMetadata, i18n helpers, runtime metadata, PraxisTabsConfigEditor, PraxisTabsWidgetConfigEditor, tabs authoring document helpers, AI capabilities and PRAXIS_TABS_AUTHORING_MANIFEST.