Sidebar Layout

The <smart-sidebar-layout> component provides a two-column application shell with a sidebar (left or right) and a main content region, with an optional top header. It follows the Base + Standard + Wrapper pattern with an InjectionToken-based extension mechanism. The abstract SidebarLayoutBaseComponent defines the shared API — optional ISidebarLayoutOptions and cssClass (alias class). SidebarLayoutStandardComponent is a barebones placeholder concrete implementation. SidebarLayoutComponent is the public wrapper that renders SidebarLayoutStandardComponent by default and accepts a custom replacement via SIDEBAR_LAYOUT_STANDARD_COMPONENT_TOKEN.


Usage

<ng-template #sidebar>
  <nav aria-label="Main">
    <a href="/dashboard">Dashboard</a>
    <a href="/projects">Projects</a>
    <a href="/team">Team</a>
  </nav>
</ng-template>

<smart-sidebar-layout [options]="options()">
  <h1>Dashboard</h1>
  <p>Welcome back. Here is what changed since yesterday.</p>
</smart-sidebar-layout>

Components

Main wrapper component. Renders SidebarLayoutStandardComponent by default. When SIDEBAR_LAYOUT_STANDARD_COMPONENT_TOKEN is provided, renders the injected component via NgComponentOutlet. Accepts main content via projected <ng-content>.

Barebones placeholder concrete implementation. Renders a wrapper <div> containing an optional <header> (when options.headerTpl is provided), an <aside> for the sidebar, and a <main> element that projects <ng-content>. The order of <aside> and <main> swaps based on options.sidebarPosition ('left' default or 'right'). The external cssClass is applied to the wrapper. It does not include Tailwind UI styling — it exists solely as the default structural placeholder until a custom implementation is registered through the token.

Abstract base directive for extending custom sidebar-layout implementations. Exposes options as an InputSignal<ISidebarLayoutOptions | undefined> and cssClass as an InputSignal<string> (with alias class).

API

Inputs

InputTypeDefaultDescription
optionsInputSignal<ISidebarLayoutOptions | undefined>-Optional configuration (sidebar template, position, header)
classInputSignal<string>''External CSS classes (alias for cssClass)

ISidebarLayoutOptions

The default SidebarLayoutStandardComponent consumes sidebarTpl, headerTpl, and sidebarPosition. title, mobileBreakpoint, and condensed are reserved for custom implementations registered via SIDEBAR_LAYOUT_STANDARD_COMPONENT_TOKEN and are ignored by SidebarLayoutStandardComponent.

InjectionToken that allows replacing the default SidebarLayoutStandardComponent with a custom implementation. Provide a Type<SidebarLayoutBaseComponent> to override.

Extending the base class

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

import {
  ISidebarLayoutOptions,
  SIDEBAR_LAYOUT_STANDARD_COMPONENT_TOKEN,
  SidebarLayoutBaseComponent,
  SidebarLayoutComponent,
} from '@smartsoft001/angular';

@Component({
  selector: 'docs-custom-sidebar-layout',
  template: `
    <div
      [class]="containerClasses()"
      [attr.data-position]="options()?.sidebarPosition ?? 'left'"
    >
      <aside class="docs-sidebar-layout__sidebar">
        @if (sidebarTpl()) {
          <ng-container [ngTemplateOutlet]="sidebarTpl()!" />
        }
      </aside>

      <main class="docs-sidebar-layout__main">
        @if (headerTpl()) {
          <header class="docs-sidebar-layout__header">
            <ng-container [ngTemplateOutlet]="headerTpl()!" />
          </header>
        } @else if (options()?.title) {
          <h1 class="docs-sidebar-layout__title">{{ options()?.title }}</h1>
        }

        <!--
          smart-sidebar-layout renders a custom implementation through
          NgComponentOutlet, which drops projected content, so <ng-content />
          would stay empty here. A custom layout either renders its own body or
          takes it from a template passed inside the options object.
        -->
        <p class="docs-sidebar-layout__body">
          Main content rendered by the custom layout.
        </p>
      </main>
    </div>
  `,
  imports: [NgTemplateOutlet],
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomSidebarLayoutComponent extends SidebarLayoutBaseComponent {
  // NgComponentOutlet passes 'cssClass' by canonical name, not the 'class' alias.
  override cssClass = input<string>('');

  readonly sidebarTpl = computed(
    () => this.options()?.sidebarTpl as TemplateRef<unknown> | undefined,
  );

  readonly headerTpl = computed(
    () => this.options()?.headerTpl as TemplateRef<unknown> | undefined,
  );

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

@Component({
  selector: 'docs-sidebar-layout-custom-example',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [SidebarLayoutComponent],
  // The token swaps the standard layout for the custom one everywhere below
  // this component, so consumers keep writing `<smart-sidebar-layout>`.
  providers: [
    {
      provide: SIDEBAR_LAYOUT_STANDARD_COMPONENT_TOKEN,
      useValue: CustomSidebarLayoutComponent,
    },
  ],
  template: `
    <ng-template #sidebar>
      <nav aria-label="Main">
        <a href="#">Overview</a>
        <a href="#">Team</a>
        <a href="#">Projects</a>
      </nav>
    </ng-template>

    <smart-sidebar-layout [options]="buildOptions(sidebar)" />
  `,
})
export class SidebarLayoutCustomExampleComponent {
  // A factory because an Angular template expression cannot spread the
  // TemplateRef declared above into an object literal.
  buildOptions(sidebar: TemplateRef<unknown>): ISidebarLayoutOptions {
    return {
      title: 'Dashboard',
      sidebarTpl: sidebar,
      sidebarPosition: 'left',
      condensed: false,
    };
  }
}

Preset

SidebarLayoutPresetComponent (smart-sidebar-layout-preset) is a styled, drop-in replacement for SidebarLayoutStandardComponent. It extends the standard component (reusing isRightSidebar) and applies a Tailwind shell where every utility is smart:-prefixed with explicit smart:dark:* variants.

Structure:

  • Root: full-height gray page (smart:min-h-full smart:bg-gray-50 smart:dark:bg-gray-900); cssClass is merged onto it.
  • Optional header zone (data-role="header"): rendered only when options.headerTpl or options.title is set. Styled like the stacked-layout preset header (white bg, smart:border-b, dark gray-800). Falls back to an <h1> (data-role="title") when only title is given.
  • Row (data-role="row"): a flex container. sidebarPosition: 'right' adds smart:flex-row-reverse.
  • Sidebar <aside> (data-role="sidebar"): white, smart:shrink-0, smart:w-64 (or smart:w-16 when options.condensed). Border is smart:border-e (left sidebar) or smart:border-s (right sidebar).
  • Content <main> (data-role="content"): gray page surface with padding; projects <ng-content>.

No new ISidebarLayoutOptions fields — the preset consumes the existing title, headerTpl, sidebarTpl, sidebarPosition, and condensed.

Because the wrapper forwards inputs canonically through NgComponentOutlet, the preset does override cssClass = input<string>('') (drops the inherited class alias).

Class recipes live in preset/preset-classes.util.ts (getSidebarLayout*Classes); it is intentionally NOT barrel-exported.

Register it to restyle every <smart-sidebar-layout>:

Documented gap: options.mobileBreakpoint is not consumed — the standard component the preset extends does not act on it either.

Source

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