ComponentServiceDirectivesCDK Overlay

Drawer

Side panel that slides in from any edge. Use the component when the markup lives in your template, or WrDrawerManager to open any component as a drawer and read its close result.

Installation

import {
  WrDrawer,
  WrDrawerTitle,
  WrDrawerContent,
  WrDrawerFooter,
  WrDrawerClose
} from 'ngwr/drawer';

Basic usage

<wr-btn (click)="open.set(true)">Open</wr-btn>

<wr-drawer [(open)]="open" position="right" width="22rem">
  <h2 wrDrawerTitle>Settings</h2>
  <div wrDrawerContent>…</div>
  <div wrDrawerFooter>
    <wr-btn wrDrawerClose>Close</wr-btn>
    <wr-btn color="primary" wrDrawerClose>Save</wr-btn>
  </div>
</wr-drawer>
Open drawer
dialog.open(...)

Opening a drawer from code

WrDrawerManager.open() renders any component as a drawer — no <wr-drawer> in the template. Reach for it when the caller doesn't own the markup (a toolbar action, an effect, a route guard) or when you need the close result. The content uses the same layout directives and injects WR_DRAWER_DATA / WrDrawerRef, exactly like a dialog.

import { WrDrawerManager } from 'ngwr/drawer';

@Component({...})
export class ToolbarComponent {
  private readonly drawers = inject(WrDrawerManager);

  async openChat(threadId: string): Promise<void> {
    const ref = this.drawers.open<ChatComponent, string, ChatData>(ChatComponent, {
      data: { thread: threadId },
      position: 'right',
      width: '24rem',
    });

    const sent = await ref.awaitClose(); // string | undefined
  }
}
// The opened component — same layout directives, no <wr-drawer> wrapper.
import { WR_DRAWER_DATA, WrDrawerRef } from 'ngwr/drawer';

@Component({...})
export class ChatComponent {
  protected readonly data = inject<ChatData>(WR_DRAWER_DATA);
  private readonly ref = inject(WrDrawerRef);

  send(): void {
    this.store.send(this.draft());
    this.ref.close('sent');        // the caller's awaitClose() resolves
  }
}

// The class token already types the generics — no cast, no WR_DRAWER_REF needed:
private readonly ref = inject<WrDrawerRef<ChatComponent, string>>(WR_DRAWER_REF);
Open chat drawer Last result: —
drawers.open(ChatComponent, { data, position: 'right', width: '24rem' })

Drawer API

NameDescriptionTypeDefault
openOpen state. Two-way bindable.booleanfalse
positionSide the drawer slides in from.'left' | 'right' | 'top' | 'bottom''right'
widthWidth when position is left/right.string'20rem'
heightHeight when position is top/bottom.string'16rem'
showHandleRender a grab handle on the leading edge and enable swipe-to-dismiss — drag it toward that edge and release past ~30% of the panel to close.booleanfalse
hasBackdropShow the dimming backdrop.booleantrue
closeOnBackdropClickClose when backdrop is clicked.booleantrue
closeOnEscapeClose on Escape.booleantrue
closableShow the built-in dismiss (×) in the top-right corner.booleantrue
closeLabelAccessible name for the dismiss button. Falls back to the drawer.close catalog key.string | nullnull

Focus and lifetime — the two forms differ

Both trap focus while open and both return it, on close, to whatever was focused when the drawer opened. Where the caret LANDS on open is not the same, and it follows from DOM order rather than from a rule: the focus trap takes cdkFocusInitial if the content marks one, and otherwise the first tabbable element in the panel.

<!-- Say where focus starts and the two forms stop differing. The attribute
     needs no import — the focus trap looks it up by name. -->
<wr-drawer [(open)]="open" position="right">
  <h2 wrDrawerTitle>Filters</h2>
  <div wrDrawerContent>
    <wr-form-field label="Search">
      <input wrInput cdkFocusInitial [(value)]="query" />
    </wr-form-field>
  </div>
</wr-drawer>

<!-- Unmarked:
       <wr-drawer>          -> the ✕, which its template renders first
       WrDrawerManager.open -> the first control, since its ✕ is appended last
     Either way, closing returns focus to whatever was focused when it opened. -->

In <wr-drawer> the ✕ is part of the component's own template and comes before your projected content, so it is the first tabbable element and takes the focus. WrDrawerManager appends its ✕ after the content instead, so focus lands on the first control in the drawer. Mark the element you want with cdkFocusInitial and the two behave identically.

Lifetime differs the same way, and here the component is the safer form. <wr-drawer> belongs to the template that declares it: destroy that view and the overlay goes with it, leaving nothing behind. A service-opened drawer outlives its caller exactly as a dialog does — closeOnNavigation (on by default) covers a route change, and everything else is yours: close the ref from DestroyRef.onDestroy when the opener can disappear on its own.

Service API

NameDescriptionTypeDefault
open(component, options?)Opens a component as a drawer. Returns a WrDrawerRef.(component, WrDrawerOptions) => WrDrawerRef

WrDrawerOptions

Mirrors the component inputs, minus showHandle — swipe-to-dismiss needs the component's own wrapper markup.

NameDescriptionTypeDefault
dataPayload exposed to the content via WR_DRAWER_DATA.D
positionSide the drawer slides in from.'left' | 'right' | 'top' | 'bottom''right'
widthWidth when position is left/right.string'20rem'
heightHeight when position is top/bottom.string'16rem'
maxHeightHeight cap for top/bottom positions.string | nullnull
roundedRound the leading corners.booleanfalse
safeAreaPad the trailing edge with the safe-area inset.booleanfalse
hasBackdropShow the dimming backdrop.booleantrue
closeOnBackdropClickClose when backdrop is clicked.booleantrue
closeOnEscapeClose on Escape.booleantrue
closableShow the built-in dismiss (×) in the top-right corner.booleantrue
closeLabelAccessible name for the dismiss button. Falls back to the drawer.close catalog key.string
panelClassExtra class(es) on the panel.string | readonly string[]

Layout directives

NameDescriptionTypeDefault
[wrDrawerTitle]Styles the title row.directive
[wrDrawerContent]Styles the scrollable body.directive
[wrDrawerFooter]Styles the footer row.directive
alignHorizontal alignment of the footer content.'start' | 'center' | 'end''end'
[wrDrawerClose]="value?"Closes the parent drawer on click — the component or the service-opened one. For service drawers the optional value becomes the close result.directive

Available inside a service-opened drawer

NameDescriptionTypeDefault
WR_DRAWER_DATAThe data payload passed to open(). undefined when you didn't pass any.InjectionToken<D>
WrDrawerRefThe open drawer’s own ref — call close(result) to dismiss it from the content.WrDrawerRef<unknown, unknown>
WR_DRAWER_REFThe same ref under a second key, used by [wrDrawerClose]. Prefer inject(WrDrawerRef) — it already supports { optional: true } and typed generics.InjectionToken<WrDrawerRef<C, R>>

CSS variables

Custom properties ngwr/drawer publishes. Each default below is declared on the component's own selector, so a :root override is shadowed by it — set them on that selector, on a wrapper you scope yourself, or inline on the element. Unlike the BEM class names, these are the supported way to restyle the component.

VariableDefaultDeclared on
--wr-drawer-bgvar(--wr-color-surface).wr-drawer__panel
--wr-drawer-shadowvar(--wr-shadow-modal).wr-drawer__panel