---
title: Desk Window
description: A window you can drag, resize, close and stack, without re-rendering its contents every frame.
source: https://adamjurek.com/components/desk-window
---

## Home


<!-- ::start:showcase demo="desk-window" height="380" -->
<!-- ::end:showcase -->

## One rule

**Position is written to the element during the drag, and to state only on
release.**

Sixty state updates a second would re-render the window's whole contents on
every frame of every drag, and the contents of these are grids of icons. During
a drag the handler writes two custom properties straight onto the node; when the
pointer lifts, exactly one state update records where it ended up.

```css
.desk-window {
	translate: var(--win-x, 0) var(--win-y, 0);
	width: var(--w, min(30rem, calc(100% - var(--s-5))));
}
```

`var(--w, <default>)` is what lets one rule serve both a window nobody has
resized and one somebody has, with no second class.

## Where this is used

Every open folder and every page shown on the home page desktop.
`FolderShelf` owns which windows exist; this owns how one behaves.


## Install

<!-- ::start:tabs -->

### TanStack

```shell
tanstack add https://adamjurek.com/r/tanstack/desk-window.json
```

### shadcn

```shell
pnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/desk-window.json
```

### pnpm

```shell
pnpm add @sushindustries/ui @sushindustries/atoms
```

<!-- ::end:tabs -->

## What you get

| | |
| --- | --- |
| Version | 0.1.0 |
| Category | layout · Overlays |
| Files | `desk-window.tsx` |
| Dependencies | None |
| Also installs | `icon` |
| Tags | block, drag, pointer, touch, no-deps |

> [!NOTE] No runtime dependencies
> It brings nothing with it beyond the stylesheet.

## Get Started


Install commands are on Home, attached from the registry - they are not written
here, because a second copy is a copy that goes stale. This tab starts after the
install worked.

## Use it

```tsx
import { useState } from "react";
import { DeskWindow } from "@sushindustries/ui";

export function Example() {
	const [pos, setPos] = useState({ x: 12, y: 12 });

	return (
		<div className="relative" style={{ height: 320 }}>
			<DeskWindow
				title="One window"
				x={pos.x}
				y={pos.y}
				z={1}
				onMove={(x, y) => setPos({ x, y })}
				onClose={() => {}}
				onRaise={() => {}}
			>
				<p className="p-4 fg-dim m-0 text-sm">Drag the bar.</p>
			</DeskWindow>
		</div>
	);
}
```

## What you should see

A titled box at `(x, y)` that follows the pointer while you drag its title
bar, and stops exactly where you release it. Nothing animates the drop - the
window is wherever your last frame left it.

## If nothing happens

`DeskWindow` needs a positioned ancestor - it is `position: absolute` inside
whatever contains it, so a parent with no `position: relative` (or no size at
all) lets it drift to the nearest positioned ancestor up the tree, usually
the whole page. `onMove` only fires on release, not during the drag, so a
window that visibly moves but never "commits" its position is a host that
never re-renders with the new `x`/`y` it received.


## Guides


## `setPointerCapture`

Without it the drag ends the moment the pointer outruns the title bar - which is
exactly when somebody is throwing a window across the screen, and the moment
they are most likely to notice it break.

It also means one code path for mouse, touch and pen. There is no
touch-specific branch here and no library.

## Resizing is not `resize: both`

The CSS property is one line and would have been tempting. It cannot be told
about a minimum, cannot be clamped to the desk, and - the one that decides it -
writes to the element's inline size without telling React, so the size is
forgotten the moment anything re-renders.

The corner is the same three pointer events as the drag, writing the same custom
properties, committing on release. It is `aria-hidden` and not focusable:
resizing refines something that already works, so a keyboard user is missing a
nicety rather than a capability.

```tsx
const next = {
	w: Math.max(240, Math.min(parent.clientWidth - at.current.x, width)),
	h: Math.max(160, Math.min(parent.clientHeight - at.current.y, height)),
};

node.style.setProperty("--w", `${next.w}px`);
node.style.setProperty("--h", `${next.h}px`);
```

## Small things that are not small

The close button stops `pointerdown` from reaching the bar. Without that,
pressing close also begins dragging the window it is closing.

Dragging is clamped to the parent, so a window cannot be lost past an edge - a
window you cannot get back is one that makes somebody reset the whole desk.

`touch-action: none` on the bar, or the browser claims the drag for scrolling
before the handler sees it.

Below 620px the window fills the desk and the resize corner is hidden. There is
nowhere to drag it to, and the corner would sit under a thumb that is trying to
scroll.

```css
.desk-bar {
	/* The bar is a handle. The browser must not claim the drag. */
	touch-action: none;
}

@media (max-width: 620px) {
	.desk-window {
		width: 100%;
		max-height: 100%;
		translate: 0 0;
	}
}
```


## API


<!-- generated:api -->

## Props

| Prop | Type | Default | Does |
| --- | --- | --- | --- |
| `title` | `ReactNode` | - |  |
| `children` | `ReactNode` | - |  |
| `x` | `number` | - | Left edge in pixels within the desk. Clamped so 80px always stays reachable. |
| `y` | `number` | - | Top edge in pixels within the desk. Clamped so the title bar cannot leave it. |
| `z` | `number` | - | Stacking order, applied as `zIndex`. The desk decides which window is highest. |
| `w?` | `number` | - | Set once resized. Absent uses the CSS default. |
| `h?` | `number` | - | Set once resized, never below 160. Absent uses the CSS default. |
| `label?` | `string` | - |  |

<!-- /generated:api -->

## Notes

`onResize` doubles as the flag for whether resizing is offered at all: leave
it unset and no resize corner renders, `w` and `h` are then read from CSS
defaults, and the window can only be moved. Pass it to get both the corner
and the 240×160 minimum enforced during the drag.

`x` and `y` are clamped during the drag to keep the title bar reachable, but
that clamp is relative to the window's own parent at drag time - resizing the
parent after mounting does not retroactively pull an off-screen window back
in.


## Examples


Examples are the tab where the component is shown doing a job, not
demonstrating a prop. The API tab already lists the props.

<!-- ::start:showcase demo="desk-window" height="420" -->
<!-- ::end:showcase -->

Press Compare. The frames are real viewports, so a layout that breaks at 320
breaks here too rather than in somebody's hands.

## In a page

```tsx
import { useState } from "react";
import { DeskWindow } from "@sushindustries/ui";

interface OpenWindow {
	id: string;
	title: string;
	x: number;
	y: number;
	z: number;
}

export function Desk({ windows, onChange }: {
	windows: OpenWindow[];
	onChange: (next: OpenWindow[]) => void;
}) {
	const [top, setTop] = useState(windows.length);

	return (
		<div className="relative" style={{ height: 480 }}>
			{windows.map((win) => (
				<DeskWindow
					key={win.id}
					title={win.title}
					x={win.x}
					y={win.y}
					z={win.z}
					onMove={(x, y) =>
						onChange(windows.map((w) => (w.id === win.id ? { ...w, x, y } : w)))
					}
					onRaise={() => {
						const z = top + 1;
						setTop(z);
						onChange(windows.map((w) => (w.id === win.id ? { ...w, z } : w)));
					}}
					onClose={() => onChange(windows.filter((w) => w.id !== win.id))}
				>
					<p className="p-4 fg-dim m-0 text-sm">{win.title}'s contents</p>
				</DeskWindow>
			))}
		</div>
	);
}
```

## What this example is not

Not the whole desk. Deciding which windows exist, and assigning the next `z`
on raise, is left to the host - here folded into one component for brevity,
but on the real site that bookkeeping lives in its own hook (`useDeskState`)
so it can also persist to storage.
