Componenta11yUpdated in v15.1

Window

Draggable, resizable windows that you open from code with WrWindowManager.

Installation

Import the class, and @use the entry point in your global styles: without it the component renders unstyled.

import { Component } from '@angular/core';
import { WR_WINDOW_DATA, WR_WINDOW_REF, WrWindowManager, WrWindowTaskbar } from 'ngwr/window';

@Component({ imports: [WrWindowTaskbar] })
export class MyComponent {}

Setup

Inject WrWindowManager, and add <wr-window-taskbar /> once to your shell for minimized windows.

import { WrWindowManager, WrWindowTaskbar } from 'ngwr/window';

@Component({
  imports: [WrWindowTaskbar],
  template: '<wr-window-taskbar />',
})
export class AppRoot {
  private readonly windows = inject(WrWindowManager);

  open(): void {
    this.windows.open(EditorComponent, { title: 'Editor' });
  }
}

Basic

open() returns a WrWindowRef; afterClosed() resolves with the value passed to close().

const ref = manager.open(EditorComponent, {
  title: 'Untitled.md',
  size: 'md',
  id: 'editor', // the same id reuses the open window
});

const result = await ref.afterClosed();
Open window

OS chrome presets

os picks the title-bar style; 'auto' (default) follows the visitor's platform.

manager.open(EditorComponent, { os: 'macos' }); // 'auto' | 'macos' | 'windows' | 'linux'
Auto (detect)macOSWindowsLinux

Compact chrome

chromeSize: 'sm' makes the title bar smaller.

Open compact window

Active window

One window has the keyboard: the one last clicked, focused or switched to. The others dim their title bar.

manager.active(); // the window with the keyboard, or null
ref.isActive();
ref.activate(); // back from the taskbar, raised, focused

// Ctrl+F6 steps through the windows, with ngwr/hotkey
inject(WrHotkey).bind('ctrl+f6', () => manager.activateNext());
Open firstOpen secondPrevious windowNext window

Pressing a window's title bar moves focus into it, to the control it had last. Escape goes to the window that has focus, or to the active one when focus is on nothing — never to a window while you type somewhere else on the page.

Keyboard move and resize

Alt+F7 moves the window and Alt+F8 resizes it: arrow keys step 10px, Shift 1px, Enter keeps it, Escape puts it back.

// Alt+F7 / Alt+F8 work on every window. Offer it from a menu too:
ref.startMove();
ref.startResize();

manager.open(EditorComponent, { keyboardStep: 20 }); // keyboard: false turns it off
Open windowMove with the keyboardResize with the keyboard

The window announces where it landed. In a right-to-left page, resizing moves the left edge.

Snap regions

snap: 'edges' snaps to halves; 'all' adds the corners. Drag a maximized or snapped window to get its own size back.

manager.open(EditorComponent, { snap: 'all' });

ref.snapTo('left'); // 'right' | 'top' | 'top-left' | … | null
ref.snap(); // 'left'
Open snappable windowLeft halfTop-right quarterUnsnap

Work area

bounds is what maximize, snapping and the cascade keep inside: the viewport, insets from its edges, or an element.

manager.open(EditorComponent, { bounds: { top: 64 } }); // an app header
manager.open(EditorComponent, { bounds: deskElement });  // a desktop in a panel

// App-wide:
provideWr(withWindows({ bounds: { top: 64 } }));
Open below the header

A taskbar that shows tabs reserves its strip as well, so a maximized window stops short of it.

Taskbar

<wr-window-taskbar /> lists minimized windows; show="all" lists every window. taskbar: false leaves a window out.

<wr-window-taskbar />
<wr-window-taskbar show="all" />
<wr-window-taskbar position="top" />
<wr-window-taskbar show="all" [maxTabs]="5" />
Open sixOpen inboxOpen editorOpen logsThe strip is at the bottom of the page. With all windows listed, the active window's tab minimizes it.

The rail stays one row. A tab is as wide as its title, up to --wr-window-taskbar-tab-max-width. Tabs that do not fit fold under a +N button that opens them as a menu, and maxTabs caps how many tabs show. The active window's tab never folds.

The rail is one Tab stop: the arrow keys move between its buttons. A minimized window comes back only from a taskbar or a dock of your own. On Linux the chrome has no minimize button unless you pass showMinimize: true.

Arrange

Cascade or tile the windows on screen, or minimize and restore them all.

manager.cascade();
manager.tile(); // 'grid' | 'columns' | 'rows'
manager.minimizeAll();
manager.restoreAll();
Open four windowsCascadeTileColumnsMinimize allRestore all

Title bar and status bar

titleExtra and statusBar take a template or a component.

<ng-template #badge let-ref>
  <wr-tag color="warning">Draft</wr-tag>
</ng-template>

manager.open(EditorComponent, {
  titleExtra: this.badge(),
  statusBar: StatusBarComponent, // a component can inject WR_WINDOW_REF
});
Open window

Update after open

ref.update() changes the title, bounds and behaviour of an open window.

ref.update({ movable: false, resizable: false, showClose: false, title: 'Uploading…' });
await upload();
ref.update({ movable: true, resizable: true, showClose: true, title: 'Upload' });
Open windowLock

Events

The ref streams what the user does to the window.

ref.closed.subscribe(result => …);    // once, then completes
ref.moved.subscribe(({ x, y }) => …);   // a drag or a keyboard move ended
ref.resized.subscribe(({ width, height }) => …);
ref.stateChanges.subscribe(state => …); // 'normal' | 'minimized' | 'maximized'

const result = await ref.awaitClose();

Persisted position

storage saves the window's geometry, so it reopens where it was left — fitted to a smaller screen.

manager.open(EditorComponent, {
  id: 'editor',
  storage: {
    key: 'editor',
    prefix: 'my-app',
    persist: 'all', // 'position' | 'size' | 'all'
  },
});

// Forget the saved geometry:
manager.clearPersistedPosition({ key: 'editor', prefix: 'my-app' });
Open persisted windowMove/resize, close it, click again — same spot.

Workspace save / restore

saveLayout(name) saves every open window; restoreLayout(name) puts them back, matched by id.

manager.saveLayout('default');

// The callback re-opens windows that are closed.
manager.restoreLayout('default', (id, snap) => {
  if (id.startsWith('editor:')) {
    manager.open(EditorComponent, { id, title: snap.title });
  } else if (id === 'settings') {
    manager.open(SettingsComponent, { id, title: snap.title });
  }
});

manager.clearLayout('default');
Save layoutRestore layoutClose all

Focus

Since v15 focus follows the window: into it on open, to its taskbar tab on minimize, back in from the tab, and back to the opener on close.

// Opened in the background: focus stays where it is.
manager.open(LogComponent, { title: 'Build log', autoFocus: false });

Opening focuses cdkFocusInitial, else the first control in the body, else the window. Switching back to a window lands on the control it had last. Windows that restoreLayout() reopens keep focus where it is unless the opener passes autoFocus: true.

Driving the window from inside

Inject WR_WINDOW_REF and WR_WINDOW_DATA in the window's component; beforeClose can veto a close.

const ref = inject<WrWindowRef<MyComponent, MyResult>>(WR_WINDOW_REF);
const data = inject<MyData>(WR_WINDOW_DATA);

ref.beforeClose(async () => {
  if (!isDirty) return true;
  return await confirmDiscard();
});

ref.setTitle(`${docName} — ${isDirty ? 'unsaved' : 'saved'}`);

await save();
ref.close(savedDocId);

WrWindowConfig

NameDescriptionTypeDefault
WrWindowConfigOptions for WrWindowManager.open(component, config).interface—
idStable identifier. A second open() with an open id switches to that window. Generated when omitted.stringauto
titleTitle-bar text and accessible name. Empty shows the catalog’s "Untitled".string''
osChrome style. 'auto' follows the visitor's platform.WrWindowOs'auto'
sizeOpening size preset: 320×200, 480×320, 720×480. width / height win.WrWindowSize'md'
chromeSizeTitle-bar density.WrWindowChromeSize'md'
x / yOpening position in viewport px. Omit to cascade from the work area’s start edge.numbercascade
width / heightOpening size in px, capped at the work area while keepInViewport.numberfrom size
minWidth / minHeight / maxWidth / maxHeightBounds for dragging, snapping, maximizing and opening.number220 / 140 / ∞ / ∞
movable / resizableAllow moving and resizing, by pointer and from the keyboard.booleantrue
keepInViewportKeep the window reachable inside the work area, and open it wholly inside when it fits.booleantrue
boundsThe work area: the viewport, insets from its edges, or an element.'viewport' | WrWindowInsets | HTMLElement | ElementRef'viewport'
snapDrag-to-edge snap — edges (halves + maximize) or all (also corners).WrWindowSnap'none'
restoreOnDragDragging a maximized or snapped window restores its own size under the pointer.booleantrue
keyboardAlt+F7 / Alt+F8 keyboard move and resize.booleantrue
keyboardStepPixels per arrow key. Shift steps 1px.number10
showMinimizeShow minimize. By default every OS but Linux.booleanper-OS
showMaximizeShow maximize. By default every OS but Linux.booleanper-OS
showCloseShow close.booleantrue
closeOnEscapeClose on Escape, from inside the window, or from nowhere while it is the active one.booleantrue
autoFocusMove focus into the window on open. Windows restoreLayout() reopens default to false.booleantrue
taskbarList the window in <wr-window-taskbar>.booleantrue
titleExtra / statusBarContent for the title bar and a status bar. A template gets the ref as $implicit.WrWindowSlotContent—
animationsThe window’s own open animation. Off under prefers-reduced-motion; the body is never touched.booleantrue
dragHandleSelector for the parts that move the window, in the body or the title bar. Replaces the title bar as the grab area; a double-click on a match maximizes.string—
storage{ key, prefix?, persist? } — save the geometry to WrStorage and open there next time.WrWindowStorageConfig—
panelClassExtra classes on the overlay pane. The chrome is .wr-window inside it.WrClassInput—
dataPayload for the body, read with inject(WR_WINDOW_DATA).D—

WrWindowManager

NameDescriptionTypeDefault
WrWindowManagerOpens windows and owns the stack, the active window, the taskbar list and saved layouts.service—
open(component, config?)Open a window with component as its body.<C, R, D>(component: ComponentType<C>, config?: WrWindowConfig<D>) => WrWindowRef<C, R>—
windowsEvery open window, in the order they opened.Signal<readonly WrWindowRef[]>—
activeThe window with the keyboard, or null.Signal<WrWindowRef | null>—
minimizedMinimized windows the taskbar lists.Signal<readonly WrWindowRef[]>—
findById(id)An open window by its config.id, or null.(id: string) => WrWindowRef | null—
closeAll()Close every open window.() => void—
activateNext() / activatePrevious()Switch to the next or previous window, in open order.() => void—
cascade()Stack the windows on screen from the work area’s corner.() => void—
tile(mode?)Share the work area between the windows on screen.(mode?: 'grid' | 'columns' | 'rows') => void'grid'
minimizeAll() / restoreAll()Minimize every window on screen; bring every minimized one back.() => void—
saveLayout(name)Save every open window's geometry, state and snap under name.(name: string) => void—
readLayout(name)Read a saved layout without applying it. null when none is saved.(name: string) => readonly WrWindowLayoutSnapshot[] | null—
restoreLayout(name, open?)Apply a saved layout by id. The open callback re-creates windows that are closed.(name: string, open?: (id: string, snapshot: { title: string }) => void) => void—
clearLayout(name)Drop a saved layout.(name: string) => void—
clearPersistedPosition(storage)Drop the geometry a window saved through storage.(cfg: WrWindowStorageConfig) => void—
bringToFront()Reserve the next z-index above every window.() => number—

WrWindowRef

NameDescriptionTypeDefault
WrWindowRefHandle returned by manager.open(). Right from the moment it is returned.class—
idThe config.id, or a generated one.string—
componentInstanceThe body component.C—
state / x / y / width / height / z / titleLive state and the box on screen.Signal<…>—
snapThe half or quarter the window is snapped to.Signal<WrWindowSnapTarget | null>—
isActiveWhether this window has the keyboard.Signal<boolean>—
closedThe close result, once, then completes.Observable<R | undefined>—
moved / resizedWhere a drag, a snap or a keyboard move left the window. Silent for calls from code.Observable<{ x, y }> / Observable<{ width, height }>—
stateChangesEvery state change.Observable<WrWindowState>—
close(result?)Close the window, after beforeClose agrees.(result?: R) => Promise<void>—
afterClosed() / awaitClose()Resolves with the close result.() => Promise<R | undefined>—
beforeClose(hook)Register a guard — return falsy to veto a close.(hook: WrWindowBeforeCloseHook<R>) => void—
minimize() / maximize()Toggles. Leaving minimized returns to the state it was minimized from.() => void—
restore()Back to normal at the window’s own geometry.() => void—
activate()Switch to the window: un-minimize, raise, focus its last control.() => void—
focus()Raise the window. Focus stays where it is.() => void—
moveTo(x, y)Move the window. While maximized it moves where restore lands.(x: number, y: number) => void—
resizeTo(width, height)Resize the window, held to its bounds.(width: number, height: number) => void—
center()Center it in its work area.() => void—
snapTo(target)Snap to a half or a quarter; 'top' maximizes, null un-snaps.(target: WrWindowSnapTarget | null) => void—
startMove() / startResize()Keyboard move or resize, from a menu item or a button.() => void—
update(patch)Change title, bounds and behaviour on an open window.(patch: WrWindowUpdate) => void—
setTitle(title)Update the title-bar text.(title: string) => void—
overlayRefThe underlying CDK overlay.OverlayRef—

WrWindowTaskbar

NameDescriptionTypeDefault
positionEdge the rail is pinned to.'top' | 'bottom''bottom'
showList the minimized windows, or every open one with the active one marked.'minimized' | 'all''minimized'
maxTabsThe most tabs the rail shows; the rest fold under a +N button. Tabs that do not fit the width fold too.number | nullnull

withWindows

NameDescriptionTypeDefault
withWindows(defaults)App-wide window defaults for provideWr() — today the bounds every window keeps inside.(defaults: WrWindowDefaults) => WrFeature—

CSS variables

Set these on the component's selector or a wrapper, not on :root: the defaults are declared on ngwr/window's own element.

VariableDefaultDeclared on
--wr-window-action-size1.5rem.wr-window +4 variant overrides
--wr-window-anim-duration0.18s.wr-window
--wr-window-bgvar(--wr-color-surface).wr-window
--wr-window-body-paddingvar(--wr-space-lg).wr-window__body
--wr-window-bordervar(--wr-border-base).wr-window
--wr-window-chrome-action-active-bgcolor-mix(in srgb, var(--wr-color-on-surface) 4%, transparent).wr-window__chrome-action +2 variant overrides
--wr-window-chrome-action-active-colorvar(--wr-color-on-surface-muted).wr-window__chrome-action +2 variant overrides
--wr-window-chrome-action-bgtransparent.wr-window__chrome-action +1 variant override
--wr-window-chrome-action-close-active-bgcolor-mix(in srgb, var(--wr-color-danger) 90%, transparent).wr-window__chrome-action +3 variant overrides
--wr-window-chrome-action-close-hover-bgvar(--wr-color-danger).wr-window__chrome-action +3 variant overrides
--wr-window-chrome-action-close-hover-colorvar(--wr-color-danger-contrast).wr-window__chrome-action +3 variant overrides
--wr-window-chrome-action-colorvar(--wr-color-on-surface).wr-window__chrome-action +2 variant overrides
--wr-window-chrome-action-hover-bgcolor-mix(in srgb, var(--wr-color-on-surface) 6%, transparent).wr-window__chrome-action +2 variant overrides
--wr-window-chrome-action-radiusvar(--wr-border-radius-sm).wr-window__chrome-action +3 variant overrides
--wr-window-chrome-action-widthvar(--wr-window-action-size).wr-window__chrome-action +2 variant overrides
--wr-window-chrome-actions-gapvar(--wr-space-xs).wr-window__chrome-actions +3 variant overrides
--wr-window-chrome-actions-padding-inline-start0.wr-window__chrome-actions +1 variant override
--wr-window-chrome-bgvar(--wr-color-hover).wr-window +1 variant override
--wr-window-chrome-colorvar(--wr-color-on-surface).wr-window +1 variant override
--wr-window-chrome-height2.25rem.wr-window +3 variant overrides
--wr-window-chrome-inactive-bgvar(--wr-color-fill-subtle).wr-window
--wr-window-chrome-inactive-colorvar(--wr-color-on-surface-muted).wr-window
--wr-window-chrome-padding-inline-endvar(--wr-space-xs).wr-window__chrome +2 variant overrides
--wr-window-chrome-padding-inline-startvar(--wr-space-md).wr-window__chrome +1 variant override
--wr-window-font-familyvar(--wr-font-family-base).wr-window
--wr-window-handle-corner14px.wr-window +1 variant override
--wr-window-handle-size8px.wr-window +1 variant override
--wr-window-radiusvar(--wr-border-radius-base).wr-window +1 variant override
--wr-window-snap-hint-radiusvar(--wr-border-radius-base).wr-window__snap-hint
--wr-window-status-bar-font-sizevar(--wr-text-xs).wr-window__status-bar
--wr-window-status-bar-gapvar(--wr-space-sm).wr-window__status-bar
--wr-window-status-bar-padding-xvar(--wr-space-md).wr-window__status-bar
--wr-window-status-bar-padding-yvar(--wr-space-xs).wr-window__status-bar
--wr-window-taskbar-fold-item-minimized-colorvar(--wr-color-on-surface-muted) only under .wr-window-taskbar__fold-item--minimized — unset elsewhere.wr-window-taskbar__fold-item--minimized
--wr-window-taskbar-font-familyvar(--wr-font-family-base).wr-window-taskbar
--wr-window-taskbar-more-colorvar(--wr-color-on-surface).wr-window-taskbar__more
--wr-window-taskbar-more-hover-bgvar(--wr-color-fill-strong).wr-window-taskbar__more
--wr-window-taskbar-more-min-width2.5rem.wr-window-taskbar__more
--wr-window-taskbar-more-open-bgvar(--wr-color-fill).wr-window-taskbar__more
--wr-window-taskbar-more-padding-inlinevar(--wr-space-sm).wr-window-taskbar__more
--wr-window-taskbar-more-radiusvar(--wr-border-radius-sm).wr-window-taskbar__more
--wr-window-taskbar-rail-bordervar(--wr-border-base).wr-window-taskbar__rail
--wr-window-taskbar-rail-gapvar(--wr-space-xs).wr-window-taskbar__rail
--wr-window-taskbar-rail-paddingvar(--wr-space-xs).wr-window-taskbar__rail
--wr-window-taskbar-rail-radiusvar(--wr-border-radius-base).wr-window-taskbar__rail
--wr-window-taskbar-tab-active-bgvar(--wr-color-fill).wr-window-taskbar__tab
--wr-window-taskbar-tab-active-indicatorvar(--wr-color-primary).wr-window-taskbar__tab
--wr-window-taskbar-tab-close-radiusvar(--wr-border-radius-sm).wr-window-taskbar__tab-close
--wr-window-taskbar-tab-font-sizevar(--wr-text-sm).wr-window-taskbar__tab
--wr-window-taskbar-tab-gapvar(--wr-space-xs).wr-window-taskbar__tab
--wr-window-taskbar-tab-line-heightvar(--wr-text-sm-leading).wr-window-taskbar__tab
--wr-window-taskbar-tab-max-width12rem.wr-window-taskbar__tab
--wr-window-taskbar-tab-minimized-colorvar(--wr-color-on-surface-muted).wr-window-taskbar__tab
--wr-window-taskbar-tab-padding-inline-endvar(--wr-space-xs).wr-window-taskbar__tab
--wr-window-taskbar-tab-padding-inline-startvar(--wr-space-md).wr-window-taskbar__tab
--wr-window-taskbar-tab-radiusvar(--wr-border-radius-sm).wr-window-taskbar__tab
--wr-window-taskbar-tab-restore-padding-blockvar(--wr-space-xs).wr-window-taskbar__tab-restore
--wr-window-taskbar-tab-restore-radiusvar(--wr-border-radius-sm).wr-window-taskbar__tab-restore
--wr-window-title-extra-font-sizevar(--wr-text-xs).wr-window__title-extra
--wr-window-title-extra-gapvar(--wr-space-sm).wr-window__title-extra
--wr-window-title-extra-padding-inline-endvar(--wr-space-sm).wr-window__title-extra
--wr-window-title-font-weightvar(--wr-font-weight-semibold).wr-window__title +1 variant override
--wr-window-title-line-heightvar(--wr-text-sm-leading).wr-window__title
--wr-window-title-sizevar(--wr-text-sm).wr-window +1 variant override
--wr-window-traffic-light-bg#ff5f57 only under .wr-window--os-macos .wr-window__chrome-action--close — unset elsewhere.wr-window--os-macos .wr-window__chrome-action--close +3 variant overrides
--wr-window-traffic-light-color#4d0000 only under .wr-window--os-macos .wr-window__chrome-action--close — unset elsewhere.wr-window--os-macos .wr-window__chrome-action--close +2 variant overrides
--wr-window-traffic-light-inactive-bgcolor-mix(in srgb, var(--wr-color-on-surface) 22%, transparent) only under .wr-window--os-macos .wr-window__chrome-action — unset elsewhere.wr-window--os-macos .wr-window__chrome-action
--wr-window-traffic-light-size1rem only under .wr-window--os-macos .wr-window__chrome-action — unset elsewhere.wr-window--os-macos .wr-window__chrome-action +1 variant override

Dependencies

Angular CDKa11y, portal
Built onDropdown, Overlay, WrStorage

See also