Headless React window management primitives
Glazier provides unstyled, fully accessible window management components for React. Build desktop-like interfaces with draggable, resizable windows-bring your own UI.
- Draggable windows with pointer capture for reliable tracking
- Resizable from 8 directions (n, s, e, w, ne, nw, se, sw)
- Snap-to-edges with visual preview (left/right/top split)
- Maximize/minimize/restore with bounds memory
- Z-index management (bring to front, send to back)
- Double-click to maximize (optional)
- Bounds constraint with automatic out-of-bounds reposition
- Desktop icons with grid snapping and drag support
- Icon selection with multi-select capability
- Icon-to-window launching - double-click icons to open/focus windows
- Window animations - open/close animations from icon position, drag shrink effects
- Component registry pattern for declarative, serializable window state
- Headless design - zero styles included, full control over appearance
- TypeScript - fully typed API
defineWindows- Unified configuration helper for windows, icons, and routesuseWindowRouting- Automatic bidirectional URL β window sync (browser back/forward support)WindowFramecomposables - Pre-built primitives (TitleBar, Title, WindowControls, Content) that reduce boilerplateResizeHandles- Ready-to-use resize handles componentuseIconLauncher- Hook for "open or focus" desktop icon patterncreateRegistry- Type-safe component registry helpercreateBrowserAdapter- Framework-agnostic URL routing adapter- Animation support -
closingWindowIdsandfinalizeClosefor smooth open/close animations - Top snap zone - Windows can now snap to top edge for maximize
npm install glazierpnpm add glazieryarn add glazierThe new WindowFrame system dramatically reduces boilerplate:
import { useRef } from 'react';
import {
WindowManagerProvider,
Window,
Desktop,
WindowFrame,
TitleBar,
Title,
WindowControls,
Content,
ResizeHandles,
createRegistry,
} from 'glazier';
import { defineWindows } from 'glazier/server';
// Define all windows in one place
const windows = defineWindows({
home: {
title: 'Home',
defaultPosition: { x: 100, y: 100 },
defaultSize: { width: 400, height: 300 },
path: '/',
icon: { label: 'Home', iconKey: 'home', position: { x: 20, y: 20 } },
},
settings: {
title: 'Settings',
defaultPosition: { x: 150, y: 150 },
defaultSize: { width: 350, height: 400 },
path: '/settings',
},
});
// Type-safe registry
const registry = createRegistry(windows.ids, {
home: HomeWindow,
settings: SettingsWindow,
});
function App() {
const containerRef = useRef<HTMLDivElement>(null);
return (
<WindowManagerProvider
boundsRef={containerRef}
registry={registry}
defaultWindows={[windows.getWindowState('home')]}
defaultIcons={windows.getIconConfigs()}
>
<div ref={containerRef} style={{ position: 'relative', height: '100vh' }}>
<Desktop>
{({ windowId, component: Component }) => (
<Window id={windowId}>
<Component windowId={windowId} />
</Window>
)}
</Desktop>
</div>
</WindowManagerProvider>
);
}
// Window with minimal boilerplate using WindowFrame
function HomeWindow({ windowId }: { windowId: string }) {
return (
<WindowFrame windowId={windowId} enableDoubleClickMaximize enableSnapToEdges>
<TitleBar className="bg-slate-900 h-10 px-3">
<Title className="text-white" />
<WindowControls />
</TitleBar>
<Content className="p-4">
<h1>Welcome!</h1>
</Content>
<ResizeHandles windowId={windowId} minWidth={300} minHeight={200} />
</WindowFrame>
);
}For complete control over every aspect:
import { useRef } from 'react';
import {
WindowManagerProvider,
Window,
useWindowManager,
useWindowDrag,
useResize,
} from 'glazier';
function App() {
const containerRef = useRef<HTMLDivElement>(null);
return (
<WindowManagerProvider
boundsRef={containerRef}
defaultWindows={[
{
id: 'window-1',
title: 'My Window',
position: { x: 100, y: 100 },
size: { width: 400, height: 300 },
zIndex: 1,
displayState: 'normal',
},
]}
>
<div ref={containerRef} style={{ position: 'relative', height: '100vh' }}>
<MyWindow windowId="window-1" />
</div>
</WindowManagerProvider>
);
}
function MyWindow({ windowId }: { windowId: string }) {
const { state, updateWindow, closeWindow } = useWindowManager();
const win = state.windows.find((w) => w.id === windowId);
const titleBarRef = useRef<HTMLDivElement>(null);
const { isDragging, dragHandleProps } = useWindowDrag({
windowId,
dragHandleRef: titleBarRef,
enableDoubleClickMaximize: true,
});
const { resizeHandleProps } = useResize(
win?.size ?? { width: 400, height: 300 },
win?.position ?? { x: 0, y: 0 },
{
minWidth: 200,
minHeight: 150,
onResize: (size, position) => updateWindow(windowId, { size, position }),
}
);
if (!win) return null;
return (
<Window id={windowId}>
<div ref={titleBarRef} {...dragHandleProps}>
{win.title}
<button onClick={() => closeWindow(windowId)}>Γ</button>
</div>
<div>Window content here</div>
<div {...resizeHandleProps('se')} />
</Window>
);
}Glazier provides behavior, not appearance. Components handle positioning, state management, and user interactions-you provide all styling. This gives you complete control over the look and feel.
Instead of maintaining separate configs for windows, icons, and routes:
import { defineWindows } from 'glazier/server';
const windows = defineWindows({
home: {
title: 'Home',
defaultPosition: { x: 100, y: 100 },
defaultSize: { width: 400, height: 300 },
path: '/',
icon: {
label: 'Home',
iconKey: 'home',
position: { x: 20, y: 20 },
},
},
about: {
title: 'About',
defaultPosition: { x: 150, y: 150 },
defaultSize: { width: 480, height: 380 },
path: '/about',
icon: {
label: 'About',
iconKey: 'about',
position: { x: 20, y: 120 },
},
},
});
// Use the helpers
windows.getWindowState('home'); // WindowState for opening
windows.getIconConfigs(); // All icon configs
windows.getPathMap(); // { home: '/', about: '/about' }
windows.getValidSlugs(); // ['about'] (excludes '/')
windows.has('home'); // true
windows.ids; // ['home', 'about']Reduce window chrome boilerplate from ~70 lines to ~15:
import {
WindowFrame,
TitleBar,
Title,
WindowControls,
Content,
ResizeHandles,
} from 'glazier';
function MyWindow({ windowId }: { windowId: string }) {
return (
<WindowFrame
windowId={windowId}
enableDoubleClickMaximize
enableSnapToEdges
onSnapZoneChange={(zone) => console.log('Snap zone:', zone)}
>
<TitleBar className="h-10 bg-slate-900">
<Title />
<WindowControls
buttonClassName="hover:bg-slate-700"
closeButtonClassName="hover:bg-red-600"
/>
</TitleBar>
<Content className="overflow-auto p-4">
Your content here
</Content>
<ResizeHandles windowId={windowId} minWidth={300} minHeight={200} />
</WindowFrame>
);
}For apps with multiple window types, use the registry pattern with type safety:
import { createRegistry } from 'glazier';
import { defineWindows } from 'glazier/server';
const windows = defineWindows({
settings: { title: 'Settings', ... },
terminal: { title: 'Terminal', ... },
notes: { title: 'Notes', ... },
});
// Type-safe: TypeScript ensures all window IDs have components
const registry = createRegistry(windows.ids, {
settings: SettingsPanel,
terminal: TerminalApp,
notes: NotesApp,
});
<WindowManagerProvider registry={registry}>
<Desktop>
{({ Component, windowId, componentProps }) => (
<Window id={windowId}>
<Component windowId={windowId} {...componentProps} />
</Window>
)}
</Desktop>
</WindowManagerProvider>Simplify icon "open or focus" logic:
import { useIconLauncher, DesktopIconGrid } from 'glazier';
function DesktopIcon({ iconId, iconState, ...props }) {
const { launchProps, isWindowOpen } = useIconLauncher({ iconId });
return (
<div {...props.dragProps} {...launchProps}>
<IconImage active={isWindowOpen} />
<span>{iconState.label}</span>
</div>
);
}Sync window focus with browser URL using useWindowRouting. This hook provides automatic bidirectional synchronization:
- Window focus β URL updates
- Browser back/forward β Window focus/open
import { useWindowRouting, createBrowserAdapter } from 'glazier';
import { defineWindows } from 'glazier/server';
const windows = defineWindows({
home: { title: 'Home', path: '/', ... },
about: { title: 'About', path: '/about', ... },
});
const routingAdapter = createBrowserAdapter({ basePath: '/app' });
function DesktopWithRouting() {
// Bidirectional sync happens automatically
useWindowRouting({
windows,
adapter: routingAdapter,
});
return (
<WindowManagerProvider
defaultWindows={[windows.getWindowState('home')]}
>
<Desktop>{/* ... */}</Desktop>
</WindowManagerProvider>
);
}Glazier provides animation hooks for smooth window open/close effects. The closingWindowIds Set and finalizeClose function enable delayed window removal for exit animations:
import { useWindowManager, toCSSValue } from 'glazier';
function AnimatedWindow({ id, children }) {
const { state, closingWindowIds, finalizeClose } = useWindowManager();
const windowState = state.windows.find((w) => w.id === id);
const isClosing = closingWindowIds.has(id);
// Trigger animation removal after close animation
useEffect(() => {
if (isClosing) {
const timer = setTimeout(() => finalizeClose(id), 250);
return () => clearTimeout(timer);
}
}, [isClosing, id, finalizeClose]);
// Use windowState.animationSource for icon position (set by launchIcon)
const { animationSource, position } = windowState;
return (
<div
style={{
animation: isClosing
? 'windowClose 250ms ease-out forwards'
: 'windowOpen 250ms ease-out forwards',
'--source-x': `${animationSource?.x ?? 0}px`,
'--source-y': `${animationSource?.y ?? 0}px`,
'--target-x': `${position.x}px`,
'--target-y': `${position.y}px`,
}}
>
{children}
</div>
);
}For drag shrink effects, use onDragStart and onDragEnd callbacks on WindowFrame:
function WindowWithDragShrink({ windowId, children }) {
const [isDragging, setIsDragging] = useState(false);
return (
<div style={{ transform: isDragging ? 'scale(0.98)' : 'scale(1)' }}>
<WindowFrame
windowId={windowId}
onDragStart={() => setIsDragging(true)}
onDragEnd={() => setIsDragging(false)}
>
{children}
</WindowFrame>
</div>
);
}Root provider that manages all window state.
| Prop | Type | Description |
|---|---|---|
children |
ReactNode |
Child components |
defaultWindows |
WindowState[] |
Initial windows to render |
defaultIcons |
IconState[] |
Initial desktop icons |
registry |
WindowRegistry |
Component registry for Desktop pattern |
defaultWindowConfigs |
WindowConfigRegistry |
Default window configs by componentId |
boundsRef |
RefObject<HTMLElement> |
Container element for bounds constraints |
initialFocusedWindowId |
string |
Which window to focus initially |
onFocusChange |
(windowId: string | null) => void |
Callback when focus changes |
Positioning container for a single window.
| Prop | Type | Description |
|---|---|---|
id |
string |
Window ID (must match a window in state) |
children |
ReactNode |
Window content |
className |
string |
Optional CSS class |
style |
CSSProperties |
Optional inline styles |
Container for window chrome with built-in drag context.
| Prop | Type | Description |
|---|---|---|
windowId |
string |
Window ID |
children |
ReactNode |
TitleBar, Content, ResizeHandles |
enableDoubleClickMaximize |
boolean |
Double-click title bar to maximize |
enableSnapToEdges |
boolean |
Enable edge snapping |
onSnapZoneChange |
(zone: 'left' | 'right' | null) => void |
Snap zone callback |
Composable primitives for window chrome. Use within WindowFrame.
<WindowFrame windowId={id}>
<TitleBar className="...">
<Title />
<WindowControls controls={['minimize', 'maximize', 'close']} />
</TitleBar>
<Content>{children}</Content>
</WindowFrame>Pre-built resize handles component.
| Prop | Type | Description |
|---|---|---|
windowId |
string |
Window ID |
minWidth |
number |
Minimum width (default: 100) |
minHeight |
number |
Minimum height (default: 50) |
maxWidth |
number |
Maximum width |
maxHeight |
number |
Maximum height |
hideWhenMaximized |
boolean |
Hide when maximized (default: true) |
Auto-renders windows from the registry based on componentId.
Headless taskbar component with render props.
Visual preview overlay for snap zones during drag.
Container that renders all icons with grid awareness.
Access the window manager context.
const {
state, // { windows: WindowState[], activeWindowId: string | null }
openWindow, // (config: WindowConfig) => void
closeWindow, // (id: string) => void
focusWindow, // (id: string) => void
updateWindow, // (id: string, updates: Partial<WindowState>) => void
bringToFront, // (id: string) => void
sendToBack, // (id: string) => void
minimizeWindow, // (id: string) => void
maximizeWindow, // (id: string) => void
restoreWindow, // (id: string) => void
getContainerBounds, // () => { width: number, height: number } | null
} = useWindowManager();Convenience hook for a single window.
Access WindowFrame context (for custom window chrome).
const {
title,
displayState,
isFocused,
close,
minimize,
maximize,
restore,
dragHandleRef,
dragHandleProps,
activeSnapZone,
} = useWindowFrame();Handles "open or focus existing window" pattern for icons.
const { launch, launchProps, isWindowOpen, existingWindow } = useIconLauncher({
iconId: 'icon-1',
});Window-specific drag behavior with snap support.
Resize handle behavior.
Access and control a single desktop icon.
Drag behavior for desktop icons with optional grid snapping.
interface WindowState {
id: string;
title: string;
position: { x: number; y: number };
size: { width: number | string; height: number | string };
zIndex: number;
displayState: 'normal' | 'minimized' | 'maximized';
previousBounds?: { position: Position; size: Size };
componentId?: string;
componentProps?: Record<string, unknown>;
}
type WindowConfig = Omit<WindowState, 'zIndex' | 'displayState' | 'previousBounds'> & {
zIndex?: number;
displayState?: WindowDisplayState;
};
type WindowRegistry = Record<string, ComponentType<{ windowId: string }>>;
type ResizeDirection = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw';
type SnapZone = 'left' | 'right';
interface IconState {
id: string;
label: string;
componentId: string;
componentProps?: Record<string, unknown>;
position: Position;
icon?: string;
}
interface GridConfig {
cellWidth: number;
cellHeight: number;
gap?: number;
}
// defineWindows configuration
interface WindowDefinition {
title: string;
defaultPosition: Position;
defaultSize: Size;
path?: string;
icon?: {
label?: string;
iconKey?: string;
position: Position;
};
defaultProps?: Record<string, unknown>;
}See apps/examples/next and apps/examples/astro for complete implementations demonstrating:
defineWindowsunified configurationWindowFramecomposablesuseIconLauncherfor desktop iconscreateBrowserAdapterfor URL routing- Type-safe registries with
createRegistry
- React >= 17
- React DOM >= 17
MIT
See CONTRIBUTING.md for development setup and guidelines.