import { Component } from '@angular/core';
import { WR_WINDOW_DATA, WR_WINDOW_REF, WrWindowManager, WrWindowTaskbar } from 'ngwr/window';
@Component({ imports: [WrWindowTaskbar] })
export class MyComponent {}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.
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();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'Compact chrome
chromeSize: 'sm' makes the title bar smaller.
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());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 offThe 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'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 } }));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" /> 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();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
});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' });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' });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');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
| Name | Description | Type | Default |
|---|---|---|---|
WrWindowConfig | Options for WrWindowManager.open(component, config). | interface | — |
id | Stable identifier. A second open() with an open id switches to that window. Generated when omitted. | string | auto |
title | Title-bar text and accessible name. Empty shows the catalog’s "Untitled". | string | '' |
os | Chrome style. 'auto' follows the visitor's platform. | WrWindowOs | 'auto' |
size | Opening size preset: 320×200, 480×320, 720×480. width / height win. | WrWindowSize | 'md' |
chromeSize | Title-bar density. | WrWindowChromeSize | 'md' |
x / y | Opening position in viewport px. Omit to cascade from the work area’s start edge. | number | cascade |
width / height | Opening size in px, capped at the work area while keepInViewport. | number | from size |
minWidth / minHeight / maxWidth / maxHeight | Bounds for dragging, snapping, maximizing and opening. | number | 220 / 140 / ∞ / ∞ |
movable / resizable | Allow moving and resizing, by pointer and from the keyboard. | boolean | true |
keepInViewport | Keep the window reachable inside the work area, and open it wholly inside when it fits. | boolean | true |
bounds | The work area: the viewport, insets from its edges, or an element. | 'viewport' | WrWindowInsets | HTMLElement | ElementRef | 'viewport' |
snap | Drag-to-edge snap — edges (halves + maximize) or all (also corners). | WrWindowSnap | 'none' |
restoreOnDrag | Dragging a maximized or snapped window restores its own size under the pointer. | boolean | true |
keyboard | Alt+F7 / Alt+F8 keyboard move and resize. | boolean | true |
keyboardStep | Pixels per arrow key. Shift steps 1px. | number | 10 |
showMinimize | Show minimize. By default every OS but Linux. | boolean | per-OS |
showMaximize | Show maximize. By default every OS but Linux. | boolean | per-OS |
showClose | Show close. | boolean | true |
closeOnEscape | Close on Escape, from inside the window, or from nowhere while it is the active one. | boolean | true |
autoFocus | Move focus into the window on open. Windows restoreLayout() reopens default to false. | boolean | true |
taskbar | List the window in <wr-window-taskbar>. | boolean | true |
titleExtra / statusBar | Content for the title bar and a status bar. A template gets the ref as $implicit. | WrWindowSlotContent | — |
animations | The window’s own open animation. Off under prefers-reduced-motion; the body is never touched. | boolean | true |
dragHandle | Selector 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 | — |
panelClass | Extra classes on the overlay pane. The chrome is .wr-window inside it. | WrClassInput | — |
data | Payload for the body, read with inject(WR_WINDOW_DATA). | D | — |
WrWindowManager
| Name | Description | Type | Default |
|---|---|---|---|
WrWindowManager | Opens 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> | — |
windows | Every open window, in the order they opened. | Signal<readonly WrWindowRef[]> | — |
active | The window with the keyboard, or null. | Signal<WrWindowRef | null> | — |
minimized | Minimized 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
| Name | Description | Type | Default |
|---|---|---|---|
WrWindowRef | Handle returned by manager.open(). Right from the moment it is returned. | class | — |
id | The config.id, or a generated one. | string | — |
componentInstance | The body component. | C | — |
state / x / y / width / height / z / title | Live state and the box on screen. | Signal<…> | — |
snap | The half or quarter the window is snapped to. | Signal<WrWindowSnapTarget | null> | — |
isActive | Whether this window has the keyboard. | Signal<boolean> | — |
closed | The close result, once, then completes. | Observable<R | undefined> | — |
moved / resized | Where a drag, a snap or a keyboard move left the window. Silent for calls from code. | Observable<{ x, y }> / Observable<{ width, height }> | — |
stateChanges | Every 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 | — |
overlayRef | The underlying CDK overlay. | OverlayRef | — |
WrWindowTaskbar
| Name | Description | Type | Default |
|---|---|---|---|
position | Edge the rail is pinned to. | 'top' | 'bottom' | 'bottom' |
show | List the minimized windows, or every open one with the active one marked. | 'minimized' | 'all' | 'minimized' |
maxTabs | The most tabs the rail shows; the rest fold under a +N button. Tabs that do not fit the width fold too. | number | null | null |
withWindows
| Name | Description | Type | Default |
|---|---|---|---|
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.
| Variable | Default | Declared on |
|---|---|---|
--wr-window-action-size | 1.5rem | .wr-window +4 variant overrides |
--wr-window-anim-duration | 0.18s | .wr-window |
--wr-window-bg | var(--wr-color-surface) | .wr-window |
--wr-window-body-padding | var(--wr-space-lg) | .wr-window__body |
--wr-window-border | var(--wr-border-base) | .wr-window |
--wr-window-chrome-action-active-bg | color-mix(in srgb, var(--wr-color-on-surface) 4%, transparent) | .wr-window__chrome-action +2 variant overrides |
--wr-window-chrome-action-active-color | var(--wr-color-on-surface-muted) | .wr-window__chrome-action +2 variant overrides |
--wr-window-chrome-action-bg | transparent | .wr-window__chrome-action +1 variant override |
--wr-window-chrome-action-close-active-bg | color-mix(in srgb, var(--wr-color-danger) 90%, transparent) | .wr-window__chrome-action +3 variant overrides |
--wr-window-chrome-action-close-hover-bg | var(--wr-color-danger) | .wr-window__chrome-action +3 variant overrides |
--wr-window-chrome-action-close-hover-color | var(--wr-color-danger-contrast) | .wr-window__chrome-action +3 variant overrides |
--wr-window-chrome-action-color | var(--wr-color-on-surface) | .wr-window__chrome-action +2 variant overrides |
--wr-window-chrome-action-hover-bg | color-mix(in srgb, var(--wr-color-on-surface) 6%, transparent) | .wr-window__chrome-action +2 variant overrides |
--wr-window-chrome-action-radius | var(--wr-border-radius-sm) | .wr-window__chrome-action +3 variant overrides |
--wr-window-chrome-action-width | var(--wr-window-action-size) | .wr-window__chrome-action +2 variant overrides |
--wr-window-chrome-actions-gap | var(--wr-space-xs) | .wr-window__chrome-actions +3 variant overrides |
--wr-window-chrome-actions-padding-inline-start | 0 | .wr-window__chrome-actions +1 variant override |
--wr-window-chrome-bg | var(--wr-color-hover) | .wr-window +1 variant override |
--wr-window-chrome-color | var(--wr-color-on-surface) | .wr-window +1 variant override |
--wr-window-chrome-height | 2.25rem | .wr-window +3 variant overrides |
--wr-window-chrome-inactive-bg | var(--wr-color-fill-subtle) | .wr-window |
--wr-window-chrome-inactive-color | var(--wr-color-on-surface-muted) | .wr-window |
--wr-window-chrome-padding-inline-end | var(--wr-space-xs) | .wr-window__chrome +2 variant overrides |
--wr-window-chrome-padding-inline-start | var(--wr-space-md) | .wr-window__chrome +1 variant override |
--wr-window-font-family | var(--wr-font-family-base) | .wr-window |
--wr-window-handle-corner | 14px | .wr-window +1 variant override |
--wr-window-handle-size | 8px | .wr-window +1 variant override |
--wr-window-radius | var(--wr-border-radius-base) | .wr-window +1 variant override |
--wr-window-snap-hint-radius | var(--wr-border-radius-base) | .wr-window__snap-hint |
--wr-window-status-bar-font-size | var(--wr-text-xs) | .wr-window__status-bar |
--wr-window-status-bar-gap | var(--wr-space-sm) | .wr-window__status-bar |
--wr-window-status-bar-padding-x | var(--wr-space-md) | .wr-window__status-bar |
--wr-window-status-bar-padding-y | var(--wr-space-xs) | .wr-window__status-bar |
--wr-window-taskbar-fold-item-minimized-color | var(--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-family | var(--wr-font-family-base) | .wr-window-taskbar |
--wr-window-taskbar-more-color | var(--wr-color-on-surface) | .wr-window-taskbar__more |
--wr-window-taskbar-more-hover-bg | var(--wr-color-fill-strong) | .wr-window-taskbar__more |
--wr-window-taskbar-more-min-width | 2.5rem | .wr-window-taskbar__more |
--wr-window-taskbar-more-open-bg | var(--wr-color-fill) | .wr-window-taskbar__more |
--wr-window-taskbar-more-padding-inline | var(--wr-space-sm) | .wr-window-taskbar__more |
--wr-window-taskbar-more-radius | var(--wr-border-radius-sm) | .wr-window-taskbar__more |
--wr-window-taskbar-rail-border | var(--wr-border-base) | .wr-window-taskbar__rail |
--wr-window-taskbar-rail-gap | var(--wr-space-xs) | .wr-window-taskbar__rail |
--wr-window-taskbar-rail-padding | var(--wr-space-xs) | .wr-window-taskbar__rail |
--wr-window-taskbar-rail-radius | var(--wr-border-radius-base) | .wr-window-taskbar__rail |
--wr-window-taskbar-tab-active-bg | var(--wr-color-fill) | .wr-window-taskbar__tab |
--wr-window-taskbar-tab-active-indicator | var(--wr-color-primary) | .wr-window-taskbar__tab |
--wr-window-taskbar-tab-close-radius | var(--wr-border-radius-sm) | .wr-window-taskbar__tab-close |
--wr-window-taskbar-tab-font-size | var(--wr-text-sm) | .wr-window-taskbar__tab |
--wr-window-taskbar-tab-gap | var(--wr-space-xs) | .wr-window-taskbar__tab |
--wr-window-taskbar-tab-line-height | var(--wr-text-sm-leading) | .wr-window-taskbar__tab |
--wr-window-taskbar-tab-max-width | 12rem | .wr-window-taskbar__tab |
--wr-window-taskbar-tab-minimized-color | var(--wr-color-on-surface-muted) | .wr-window-taskbar__tab |
--wr-window-taskbar-tab-padding-inline-end | var(--wr-space-xs) | .wr-window-taskbar__tab |
--wr-window-taskbar-tab-padding-inline-start | var(--wr-space-md) | .wr-window-taskbar__tab |
--wr-window-taskbar-tab-radius | var(--wr-border-radius-sm) | .wr-window-taskbar__tab |
--wr-window-taskbar-tab-restore-padding-block | var(--wr-space-xs) | .wr-window-taskbar__tab-restore |
--wr-window-taskbar-tab-restore-radius | var(--wr-border-radius-sm) | .wr-window-taskbar__tab-restore |
--wr-window-title-extra-font-size | var(--wr-text-xs) | .wr-window__title-extra |
--wr-window-title-extra-gap | var(--wr-space-sm) | .wr-window__title-extra |
--wr-window-title-extra-padding-inline-end | var(--wr-space-sm) | .wr-window__title-extra |
--wr-window-title-font-weight | var(--wr-font-weight-semibold) | .wr-window__title +1 variant override |
--wr-window-title-line-height | var(--wr-text-sm-leading) | .wr-window__title |
--wr-window-title-size | var(--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-bg | color-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-size | 1rem only under .wr-window--os-macos .wr-window__chrome-action — unset elsewhere | .wr-window--os-macos .wr-window__chrome-action +1 variant override |
Dependencies
See also
Component DialogOpen any component as a modal dialog and await its result.Component DrawerA side panel that slides in from any edge of the screen.Component DropdownA menu that opens from any trigger.Guide OverlayStyle ngwr's floating panels and control where focus goes.Service WrStorageTyped, reactive key-value storage over localStorage or any engine, safe under SSR.Component BadgeA small label for a count or a status.