Tabs

The <smart-tabs> component renders a tabbed navigation strip (anchor-based for routing or button-based for in-page selection) with a <select> fallback for mobile. It follows the Base + Standard + Wrapper pattern with an InjectionToken-based extension mechanism. The abstract TabsBaseComponent defines the shared API — options (ITabsOptions), selectedId (two-way ModelSignal<string | null>), cssClass (alias class), and the tabChange output. TabsStandardComponent is a barebones placeholder using native <nav>, <ul>, <a>, <button> and <select> elements. TabsComponent is the public wrapper that renders TabsStandardComponent by default and accepts a custom replacement via TABS_STANDARD_COMPONENT_TOKEN.


Usage

<smart-tabs
  [options]="options"
  [(selectedId)]="selectedTab"
  (tabChange)="onTabChange($event)"
/>

Components

TabsComponent (<smart-tabs>)

Main wrapper. Delegates to TabsStandardComponent by default. When TABS_STANDARD_COMPONENT_TOKEN is provided, renders the injected component via NgComponentOutlet. Re-emits tabChange and forwards two-way selectedId.

TabsStandardComponent (<smart-tabs-standard>)

Barebones placeholder using native HTML. Renders an outer wrapper with cssClass, an optional mobile <select> (rendered when options.showMobileSelect is true or unset and items is non-empty), and a <nav class="tabs-desktop"> containing a <ul class="tabs-list"> of items. Each item renders as <a class="tab-link"> when href is provided, otherwise <button class="tab-button"> emitting tabChange and updating selectedId. Adds current class and aria-current="page" when item.id === selectedId(). Supports optional iconTpl and badge per item.

TabsPresetComponent (<smart-tabs-preset>)

Styled variation that extends TabsBaseComponent and is a drop-in replacement for TabsStandardComponent. Register it via TABS_STANDARD_COMPONENT_TOKEN to restyle every <smart-tabs>, or use the <smart-tabs-preset> selector directly. It renders the translated Preline tab nav for every SmartTabsLayout (underline, underline-with-icons, underline-with-badges, underline-full-width, pills, pills-on-gray, pills-with-brand-color, bar-with-underline, simple), selected through options.layout (default 'underline'). Honors options.items (rendering <a role="tab"> when href is set, otherwise <button role="tab">), options.ariaLabel, per-item iconTpl and badge, and options.showMobileSelect (default true → renders a <select> shown only below sm and hides the nav below sm). The active tab is driven by the selectedId model (falling back to the first item) via (click) — Preline's Tabs JS plugin is not used. ARIA (role=tablist/tab, aria-selected, aria-controls, aria-orientation) is kept on the markup. All classes are smart:-prefixed Tailwind with explicit dark: variants; the per-layout class recipes live in preset/preset-classes.util.ts.

Because TabsComponent renders injected components via NgComponentOutlet (which passes inputs by canonical name), TabsPresetComponent overrides cssClass as input<string>('') without the class alias. Bind it as [cssClass] when using the <smart-tabs-preset> selector directly, or just pass class on <smart-tabs> (the wrapper forwards it). Note: NgComponentOutlet does not wire outputs, so tabChange does not bubble through <smart-tabs> when the preset is registered via the token; subscribe to it on <smart-tabs-preset> directly if you need the event.

TabsBaseComponent (abstract)

Abstract base directive. Exposes:

  • options: InputSignal<ITabsOptions | undefined>
  • selectedId: ModelSignal<string | null> (default null)
  • cssClass: InputSignal<string> (alias class)
  • tabChange: OutputEmitterRef<ITabChange>

ITabChange = { tabId: string }.

API

Inputs

InputTypeDefaultDescription
optionsInputSignal<ITabsOptions | undefined>-Tabs configuration
selectedIdModelSignal<string | null>nullTwo-way bindable currently selected tab id
classInputSignal<string>''External CSS classes (alias for cssClass)

Outputs

OutputTypeDescription
tabChangeOutputEmitterRef<ITabChange>Emitted when a button-type tab is clicked

Extending the base class

import {
  ChangeDetectionStrategy,
  Component,
  computed,
  input,
  ViewEncapsulation,
} from '@angular/core';

import {
  ITabsOptions,
  TABS_STANDARD_COMPONENT_TOKEN,
  TabsBaseComponent,
  TabsComponent,
} from '@smartsoft001/angular';

@Component({
  selector: 'docs-custom-tabs',
  template: `
    <nav
      [class]="containerClasses()"
      [attr.aria-label]="options()?.ariaLabel ?? 'Tabs'"
    >
      @for (item of options()?.items ?? []; track item.id) {
        <button
          type="button"
          [class]="tabClasses(item.id)"
          [attr.aria-current]="isCurrent(item.id) ? 'page' : null"
          (click)="select(item.id)"
        >
          {{ item.label ?? item.id }}
          @if (item.badge !== undefined && item.badge !== null) {
            <span class="docs-tabs__badge">{{ item.badge }}</span>
          }
        </button>
      }
    </nav>
  `,
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomTabsComponent extends TabsBaseComponent {
  // NgComponentOutlet passes 'cssClass' by canonical name, not the 'class' alias.
  override cssClass = input<string>('');

  containerClasses = computed(() =>
    ['docs-tabs', this.cssClass()].filter(Boolean).join(' '),
  );

  // selectedId is a model on the base: the implementation updates it and
  // announces the change through the tabChange output.
  select(tabId: string): void {
    this.selectedId.set(tabId);
    this.tabChange.emit({ tabId });
  }

  protected isCurrent(tabId: string): boolean {
    return this.selectedId() === tabId;
  }

  protected tabClasses(tabId: string): string {
    return this.isCurrent(tabId)
      ? 'docs-tabs__tab docs-tabs__tab--current'
      : 'docs-tabs__tab';
  }
}

@Component({
  selector: 'docs-tabs-custom-example',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [TabsComponent],
  // The token swaps the standard tabs for the custom one everywhere below this
  // component, so consumers keep writing `<smart-tabs>`.
  providers: [
    {
      provide: TABS_STANDARD_COMPONENT_TOKEN,
      useValue: CustomTabsComponent,
    },
  ],
  // NgComponentOutlet forwards inputs only, so the wrapper's (tabChange) and
  // (selectedIdChange) stay silent here: selection lives in the custom
  // component and `selectedId` below is the initial value.
  template: `<smart-tabs [options]="options" [selectedId]="selectedId" />`,
})
export class TabsCustomExampleComponent {
  selectedId = 'billing';

  options: ITabsOptions = {
    layout: 'underline',
    ariaLabel: 'Account sections',
    showMobileSelect: false,
    items: [
      { id: 'account', label: 'Account' },
      { id: 'billing', label: 'Billing' },
      { id: 'members', label: 'Members', badge: 12 },
    ],
  };
}

Source

The component lives in packages/shared/angular/src/lib/components/tabs and is documented for Claude Code by the angular-components-tabs skill.