Sidebar Navigation

The <smart-sidebar-navigation> component renders a full-height application sidebar with optional logo, vertical navigation groups, expandable sub-sections, and a profile footer. It follows the Base + Standard + Wrapper pattern with an InjectionToken-based extension mechanism. The abstract SidebarNavigationBaseComponent defines the shared API — options (ISidebarNavOptions), cssClass (alias class), and outputs itemClick and itemToggle. SidebarNavigationStandardComponent is a barebones placeholder using native <nav>, <ul>, <li>, <a>, <button> elements and <img> for logo/avatar. SidebarNavigationComponent is the public wrapper that renders SidebarNavigationStandardComponent by default and accepts a custom replacement via SIDEBAR_NAVIGATION_STANDARD_COMPONENT_TOKEN.


Usage

<smart-sidebar-navigation
  [options]="options"
  (itemClick)="onItemClick($event)"
  (itemToggle)="onItemToggle($event)"
/>

Components

Main wrapper. Delegates to SidebarNavigationStandardComponent by default. When SIDEBAR_NAVIGATION_STANDARD_COMPONENT_TOKEN is provided, renders the injected component via NgComponentOutlet. Re-emits itemClick and itemToggle.

Barebones placeholder using native HTML. Renders an outer wrapper with cssClass, an optional <div class="sidebar-logo"> (with <img> plus optional dark variant <img> and optional <a> link), a <nav class="sidebar-navigation"> (with aria-label from options.ariaLabel or default "Sidebar"), and a <ul> of groups. Each group renders an optional <div class="group-title"> and a <ul> of items.

Items render in three forms:

  • <a class="item-link"> (when href provided)
  • <button class="item-button"> (when no href, emits itemClick)
  • <button class="item-toggle"> followed by <ul class="children"> (when expandable === true, emits itemToggle)

Items get current class and aria-current="page" when item.current === true. The toggle button manages local expanded state (initial value taken from item.expanded). Supports iconTpl, initial (e.g. team letter), label, and badge.

When options.profile is present, renders a <li class="profile"> at the bottom containing <a class="profile-link"> with <img class="profile-avatar">, optional .sr-only text, and .profile-name.

Abstract base directive. Exposes:

  • options: InputSignal<ISidebarNavOptions | undefined>
  • cssClass: InputSignal<string> (alias class)
  • itemClick: OutputEmitterRef<ISidebarNavItemClick>
  • itemToggle: OutputEmitterRef<ISidebarNavItemToggle>
  • protected resolvedGroups: Signal<ISidebarNavGroup[]> — normalizes options.items into a single-group structure alongside options.groups
  • protected expandedOverrides: WritableSignal<Record<string, boolean>> — internal toggle state per item id
  • protected isExpanded(item) / toggleExpanded(item) — helpers for managing expandable items

ISidebarNavItemClick = { itemId: string }. ISidebarNavItemToggle = { itemId: string; expanded: boolean }.

API

Inputs

InputTypeDefaultDescription
optionsInputSignal<ISidebarNavOptions | undefined>-Sidebar navigation configuration
classInputSignal<string>''External CSS classes (alias for cssClass)

Outputs

OutputTypeDescription
itemClickOutputEmitterRef<ISidebarNavItemClick>Emitted when a button-type nav item or child is clicked
itemToggleOutputEmitterRef<ISidebarNavItemToggle>Emitted when an expandable item is toggled

ISidebarNavOptions

When both items and groups are provided, items is rendered first as a leading single-group section.

Extending the base class

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

import {
  ISidebarNavOptions,
  SIDEBAR_NAVIGATION_STANDARD_COMPONENT_TOKEN,
  SidebarNavigationBaseComponent,
  SidebarNavigationComponent,
} from '@smartsoft001/angular';

@Component({
  selector: 'docs-custom-sidebar-navigation',
  template: `
    <nav
      [class]="containerClasses()"
      [attr.aria-label]="options()?.ariaLabel ?? 'Sidebar'"
    >
      <!--
        resolvedGroups() is the protected helper of the base class: it merges
        the flat items list and the named groups into a single list.
      -->
      @for (group of resolvedGroups(); track $index) {
        <div class="docs-sidebar-navigation__group">
          @if (group.title) {
            <p class="docs-sidebar-navigation__group-title">
              {{ group.title }}
            </p>
          }

          <ul>
            @for (item of group.items; track item.id) {
              <li>
                @if (item.expandable) {
                  <button
                    type="button"
                    class="docs-sidebar-navigation__toggle"
                    [attr.aria-expanded]="isExpanded(item)"
                    (click)="toggleExpanded(item)"
                  >
                    {{ item.label }}
                  </button>

                  @if (isExpanded(item)) {
                    <ul class="docs-sidebar-navigation__children">
                      @for (child of item.children ?? []; track child.id) {
                        <li>
                          <a
                            class="docs-sidebar-navigation__child-link"
                            [href]="child.href"
                            (click)="itemClick.emit({ itemId: child.id })"
                          >
                            {{ child.label }}
                          </a>
                        </li>
                      }
                    </ul>
                  }
                } @else {
                  <a
                    class="docs-sidebar-navigation__link"
                    [href]="item.href"
                    [attr.aria-current]="item.current ? 'page' : null"
                    (click)="itemClick.emit({ itemId: item.id })"
                  >
                    @if (item.initial) {
                      <span class="docs-sidebar-navigation__initial">
                        {{ item.initial }}
                      </span>
                    }

                    <span>{{ item.label }}</span>

                    @if (item.badge !== undefined) {
                      <span class="docs-sidebar-navigation__badge">
                        {{ item.badge }}
                      </span>
                    }
                  </a>
                }
              </li>
            }
          </ul>
        </div>
      }

      @if (options()?.profile) {
        <a
          class="docs-sidebar-navigation__profile"
          [href]="options()?.profile?.href"
        >
          {{ options()?.profile?.name }}
        </a>
      }
    </nav>
  `,
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomSidebarNavigationComponent extends SidebarNavigationBaseComponent {
  // NgComponentOutlet passes 'cssClass' by canonical name, not the 'class' alias.
  override cssClass = input<string>('');

  readonly containerClasses = computed(() => {
    const classes = [
      'docs-sidebar-navigation',
      `docs-sidebar-navigation--${this.options()?.layout ?? 'light'}`,
    ];
    const extra = this.cssClass();
    if (extra) classes.push(extra);
    return classes.join(' ');
  });
}

@Component({
  selector: 'docs-sidebar-navigation-custom-example',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [SidebarNavigationComponent],
  // The token swaps the standard navigation for the custom one everywhere
  // below this component, so consumers keep writing
  // `<smart-sidebar-navigation>`.
  providers: [
    {
      provide: SIDEBAR_NAVIGATION_STANDARD_COMPONENT_TOKEN,
      useValue: CustomSidebarNavigationComponent,
    },
  ],
  template: ` <smart-sidebar-navigation [options]="options" /> `,
})
export class SidebarNavigationCustomExampleComponent {
  readonly options: ISidebarNavOptions = {
    layout: 'light',
    ariaLabel: 'Sidebar',
    items: [
      { id: 'dashboard', label: 'Dashboard', href: '#', current: true },
      { id: 'team', label: 'Team', href: '#', badge: 5 },
      { id: 'projects', label: 'Projects', href: '#', badge: 12 },
      {
        id: 'teams',
        label: 'Teams',
        expandable: true,
        children: [
          { id: 'engineering', label: 'Engineering', href: '#' },
          { id: 'human-resources', label: 'Human Resources', href: '#' },
        ],
      },
    ],
    groups: [
      {
        id: 'your-teams',
        title: 'Your teams',
        items: [
          {
            id: 'engineering-team',
            label: 'Engineering',
            initial: 'E',
            href: '#',
          },
          { id: 'marketing-team', label: 'Marketing', initial: 'M', href: '#' },
        ],
      },
    ],
    profile: { name: 'Tom Cook', href: '#', srOnlyText: 'Your profile' },
  };
}

Source

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