Drawer

The <smart-drawer> component provides a side panel overlay (left/right) with optional header, close button, and backdrop. It follows the Base + Standard + Wrapper pattern with an InjectionToken-based extension mechanism. The abstract DrawerBaseComponent defines the shared API — open (two-way ModelSignal<boolean>), title, optional IDrawerOptions, cssClass (alias class), a closed output, and a close() method that sets open to false and emits closed. DrawerStandardComponent is a barebones placeholder concrete implementation. DrawerComponent is the public wrapper that renders DrawerStandardComponent by default and accepts a custom replacement via DRAWER_STANDARD_COMPONENT_TOKEN.


Usage

<button type="button" (click)="open.set(true)">View cart</button>

<smart-drawer
  [(open)]="open"
  title="Shopping cart"
  [options]="options"
  (closed)="onClosed()"
>
  <ul>
    <li>Throwback Hip Bag, $90.00</li>
    <li>Medium Stuff Satchel, $32.00</li>
  </ul>
</smart-drawer>

Components

DrawerComponent (<smart-drawer>)

Main wrapper component. Renders DrawerStandardComponent by default. When DRAWER_STANDARD_COMPONENT_TOKEN is provided, renders the injected component via NgComponentOutlet.

DrawerStandardComponent (<smart-drawer-standard>)

Barebones placeholder concrete implementation. When open() is true, renders an optional overlay <div class="drawer-overlay"> (when options.withOverlay) and an <aside role="dialog" aria-modal="true"> with a data-position attribute (left or right, default right). When a title is provided, the aside includes a <header> with an <h2 id="smart-drawer-title"> and a close <button aria-label="Close">. Projected content is rendered via <ng-content />. It does not include Tailwind UI styling — it exists solely as the default structural placeholder until a custom implementation is registered through the token.

DrawerPresetComponent (<smart-drawer-preset>)

Fully-styled variation that extends DrawerBaseComponent and is a drop-in replacement for DrawerStandardComponent. Register it via DRAWER_STANDARD_COMPONENT_TOKEN to restyle every <smart-drawer>, or use the <smart-drawer-preset> selector directly. It renders the translated Preline offcanvas look: a fixed, sliding side panel (role="dialog", aria-modal="true", tabindex="-1") with a header (title + circular close button with the Preline X icon), a <ng-content /> body, and an optional dimmed backdrop. It consumes options.position (left/right, default right → data-position + start/end placement), options.wide (max-w-xs → max-w-md), options.withOverlay (renders a click-to-close backdrop), and options.brandedHeader (blue header bar with inverted title/close styling). Open/close is Angular-driven via the open model + @if and close() (no Preline JS runtime). All classes are smart:-prefixed Tailwind with explicit dark: variants. The class recipes live in preset/preset-classes.util.ts (getDrawerPanelClasses, getDrawerBackdropClasses, getDrawerHeaderClasses, getDrawerTitleClasses, getDrawerCloseClasses, getDrawerBodyClasses).

Because DrawerComponent renders injected components via NgComponentOutlet (which passes inputs by canonical name), DrawerPresetComponent overrides cssClass as input<string>('') without the class alias. Bind it as [cssClass] when using the <smart-drawer-preset> selector directly, or just pass class on <smart-drawer> (the wrapper forwards it).

The preset uses <ng-content /> for its body, which works with the <smart-drawer-preset> selector directly but is not propagated when rendered through DRAWER_STANDARD_COMPONENT_TOKEN (see "Content Projection Limitation"). It does not consume options.stickyFooter (no footer slot in the base API) or options.variant (content-type variants are projected via <ng-content />, not built into the offcanvas shell). Top/bottom placements from the Preline reference are not expressible — IDrawerOptions.position is left | right only.

DrawerBaseComponent (abstract)

Abstract base directive for extending custom drawer implementations. Exposes open as a two-way ModelSignal<boolean> (default false), title as an InputSignal<string | undefined>, options as an InputSignal<IDrawerOptions | undefined>, cssClass as an InputSignal<string> (with alias class), a closed output, and a close() method that sets open to false and emits closed.

API

Inputs

InputTypeDefaultDescription
openModelSignal<boolean>falseWhether the drawer is open (two-way bindable)
titleInputSignal<string | undefined>-Optional drawer title (renders header when set)
optionsInputSignal<IDrawerOptions | undefined>-Optional configuration
classInputSignal<string>''External CSS classes (alias for cssClass)

Outputs

OutputTypeDescription
closedoutput<void>Emitted when the drawer is closed (programmatic or button)

IDrawerOptions

The standard component only consumes position (mapped to the data-position attribute on the aside, default 'right') and withOverlay (toggles the backdrop overlay). The remaining properties — wide, brandedHeader, stickyFooter, and variant — are reserved for custom implementations registered through DRAWER_STANDARD_COMPONENT_TOKEN and are ignored by DrawerStandardComponent.

DRAWER_STANDARD_COMPONENT_TOKEN

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

Extending the base class

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

import {
  DRAWER_STANDARD_COMPONENT_TOKEN,
  DrawerBaseComponent,
  DrawerComponent,
  IDrawerOptions,
} from '@smartsoft001/angular';

@Component({
  selector: 'docs-custom-drawer',
  template: `
    @if (open()) {
      @if (options()?.withOverlay) {
        <div class="docs-drawer__overlay" (click)="close()"></div>
      }
      <aside
        role="dialog"
        aria-modal="true"
        [class]="panelClasses()"
        [attr.data-position]="options()?.position ?? 'right'"
      >
        @if (title()) {
          <header class="docs-drawer__header">
            <h2>{{ title() }}</h2>
            <button
              type="button"
              class="docs-drawer__close"
              aria-label="Close"
              (click)="close()"
            >
              &times;
            </button>
          </header>
        }
        <!--
          smart-drawer renders a custom implementation through NgComponentOutlet,
          which forwards neither projected content nor extra inputs. Only open,
          title, options and cssClass arrive here, so a custom drawer renders its
          own body instead of relying on ng-content.
        -->
        <p class="docs-drawer__body">Your cart is empty.</p>
      </aside>
    }
  `,
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomDrawerComponent extends DrawerBaseComponent {
  // NgComponentOutlet passes 'cssClass' by canonical name, not the 'class' alias.
  override cssClass = input<string>('');

  panelClasses = computed(() => {
    const classes = ['docs-drawer__panel'];
    if (this.options()?.wide) classes.push('docs-drawer__panel--wide');
    const extra = this.cssClass();
    if (extra) classes.push(extra);
    return classes.join(' ');
  });
}

@Component({
  selector: 'docs-drawer-custom-example',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [DrawerComponent],
  // The token swaps the standard drawer for the custom one everywhere below
  // this component, so consumers keep writing `<smart-drawer>`.
  providers: [
    {
      provide: DRAWER_STANDARD_COMPONENT_TOKEN,
      useValue: CustomDrawerComponent,
    },
  ],
  template: `
    <smart-drawer [open]="true" title="Shopping cart" [options]="options" />
  `,
})
export class DrawerCustomExampleComponent {
  options: IDrawerOptions = { position: 'right', withOverlay: true };
}

Accessibility

  • The aside is rendered with role="dialog" and aria-modal="true" so assistive technologies treat it as a modal dialog.
  • When a title is provided, the aside is labelled via aria-labelledby="smart-drawer-title" (matching the <h2 id="smart-drawer-title"> inside the header). Without a title, no label is set — supply one through class-scoped styling or wrap the drawer in a labelled landmark if needed.
  • The header close button has aria-label="Close".
  • Positioning is exposed declaratively via the data-position="left|right" attribute on the aside, allowing CSS to style left- vs right-anchored drawers without runtime branching. Default is right.
  • The overlay (when options.withOverlay is true) closes the drawer on click.

Source

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