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
SidebarNavigationComponent (<smart-sidebar-navigation>)
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.
SidebarNavigationStandardComponent (<smart-sidebar-navigation-standard>)
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">(whenhrefprovided)<button class="item-button">(when nohref, emitsitemClick)<button class="item-toggle">followed by<ul class="children">(whenexpandable === true, emitsitemToggle)
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.
SidebarNavigationBaseComponent (abstract)
Abstract base directive. Exposes:
options: InputSignal<ISidebarNavOptions | undefined>cssClass: InputSignal<string>(aliasclass)itemClick: OutputEmitterRef<ISidebarNavItemClick>itemToggle: OutputEmitterRef<ISidebarNavItemToggle>- protected
resolvedGroups: Signal<ISidebarNavGroup[]>— normalizesoptions.itemsinto a single-group structure alongsideoptions.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
| Input | Type | Default | Description |
|---|---|---|---|
options | InputSignal<ISidebarNavOptions | undefined> | - | Sidebar navigation configuration |
class | InputSignal<string> | '' | External CSS classes (alias for cssClass) |
Outputs
| Output | Type | Description |
|---|---|---|
itemClick | OutputEmitterRef<ISidebarNavItemClick> | Emitted when a button-type nav item or child is clicked |
itemToggle | OutputEmitterRef<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.