deskplane
v0.1.3
Published
A framework-agnostic, two-dimensional virtual desktop navigator for the web.
Maintainers
Readme
Deskplane
A framework-agnostic, two-dimensional virtual desktop navigator for the web.
The package owns coordinates, active state, gestures, and movement. Your application owns all content, controls, indicators, and visual design.

Demo: Codepen
The package is available on npm.
Features
- One to three independently positioned rows.
- Any number of desktops within each row.
- Configurable initial desktop, defaulting to the center desktop of the center row.
- Horizontal and vertical movement without diagonal transitions.
- Invisible destination-row pre-alignment before vertical navigation.
- Explicit horizontal, vertical, or two-axis swipe zones.
- Gesture thresholds, flick velocity, transition duration, and easing options.
- External controls with active-state subscriptions.
- Responsive desktop sizing from any mounted viewport element.
- Container-query-ready desktop roots.
- DOM and accessibility state restoration on teardown.
- No runtime dependencies, theme, icons, fonts, or other assets.
Installation
After publication:
npm install deskplaneImport the controller and the structural stylesheet:
import { createDeskplane } from "deskplane";
import "deskplane/style.css";Basic usage
const deskplane = createDeskplane({
viewport: document.querySelector("#viewport"),
rows: [
{
id: "top",
desktops: [
{ id: "overview", element: document.querySelector("#overview") },
{ id: "reports", element: document.querySelector("#reports") },
],
},
{
id: "middle",
desktops: [
{ id: "library", element: document.querySelector("#library") },
{ id: "home", element: document.querySelector("#home") },
{ id: "settings", element: document.querySelector("#settings") },
],
},
{
id: "bottom",
desktops: [
{ id: "contact", element: document.querySelector("#contact") },
],
},
],
initialDesktopId: "home",
transition: {
duration: 320,
easing: "cubic-bezier(0.22, 1, 0.36, 1)",
},
});
await deskplane.goTo("settings");
await deskplane.move("up");The example omits application-specific null checks for readability. TypeScript users should resolve and validate their elements before passing them to the package.
The supplied desktop elements are moved into the viewport while the controller is active. destroy() restores their original DOM positions, relevant attributes, and inert state.
React adapter
React applications can use the optional deskplane/react entry point:
import { useState } from "react";
import { DeskplaneViewport } from "deskplane/react";
import type { Deskplane } from "deskplane";
import "deskplane/style.css";
export function ApplicationDesktops() {
const [deskplane, setDeskplane] = useState<Deskplane>();
return (
<>
<button onClick={() => void deskplane?.goTo("controls")}>Controls</button>
<button onClick={() => void deskplane?.goTo("information")}>Info</button>
<DeskplaneViewport
className="application-viewport"
initialDesktopId="information"
onReady={(controller) => {
setDeskplane(controller);
return () => setDeskplane(undefined);
}}
rows={[
{
id: "main",
desktops: [
{ id: "controls", children: <Controls /> },
{ id: "information", children: <Information /> },
{ id: "copilot", children: <Copilot /> },
],
},
]}
/>
</>
);
}The adapter has no application or routing assumptions. It creates the desktop containers and renders application content into them through React portals. React retains ownership of every component tree while the framework-agnostic Deskplane core owns positioning, gestures, and navigation state.
React and React DOM are optional peer dependencies: applications using only the core package do not need either framework.
External controls
Controls can live anywhere in the document. A control only needs a target desktop id:
const button = document.querySelector("#settings-button");
button.addEventListener("click", () => {
void deskplane.goTo("settings");
});
const unsubscribe = deskplane.subscribe((snapshot) => {
const active = snapshot.activeDesktopId === "settings";
button.setAttribute("aria-pressed", String(active));
});Subscriptions receive the current snapshot immediately and again when navigation state changes. They are independent of any UI framework.
Swipe zones
Gestures begin only on explicit swipe zones, preventing competition with forms, buttons, and scrollable application content.
Declarative zones are discovered within the viewport when the controller is created:
<div data-deskplane-swipe-zone="horizontal">Drag left or right</div>
<div data-deskplane-swipe-zone="vertical">Drag up or down</div>
<div data-deskplane-swipe-zone>Drag in either direction</div>Zones can instead be supplied explicitly:
createDeskplane({
viewport,
rows,
gestures: {
zones: [
{ element: horizontalHandle, axes: "horizontal" },
{ element: verticalHandle, axes: "vertical" },
],
lockThreshold: 8,
distanceThreshold: 0.18,
velocityThreshold: 0.5,
},
});The structural stylesheet applies touch-action: none only to swipe zones. Normal desktop content therefore keeps its native touch, scrolling, selection, and form behavior. Common interactive controls and [data-deskplane-no-swipe] are also ignored if nested inside a zone.
Container queries
Every mounted .deskplane-desktop is a size container. Application content can react to the actual viewport dimensions:
.account-desktop {
container-name: account;
}
@container account (min-width: 42rem) {
.account-layout {
grid-template-columns: 2fr 1fr;
}
}API summary
createDeskplane(options) returns:
snapshot— active row, active desktop, each row's remembered desktop, and animation state.goTo(desktopId)— navigate directly to an associated desktop.move(direction)— moveup,right,down, orleft; resolves tofalseat an edge.isActive(desktopId)— check whether a desktop is active.subscribe(listener)— observe state and receive an unsubscribe function.destroy()— remove package DOM and listeners and restore supplied elements.
Invalid layouts, duplicate ids, duplicate elements, unknown desktop ids, and more than three rows fail early with descriptive errors.
Demo
Run the one included demonstration:
npm run devIt demonstrates a non-fullscreen viewport, unequal row lengths, fixed external controls, active-state reflection, swipe rails, container queries, normal buttons, forms, and scrollable content.
Development and quality checks
Install dependencies and run the complete quality gate:
npm install
npm run checknpm run check verifies Prettier formatting, type-aware ESLint rules, strict TypeScript types, Vitest tests, the distributable package build, and the production demo build.
Pure navigation and state transitions are covered by fast unit tests. DOM mounting, restoration, active state, row pre-alignment, and pointer gestures are tested in a browser-like DOM. Real-browser visual checks cover responsive rendering and container behavior.
Other commands:
npm run format
npm run lint
npm run typecheck
npm test
npm run test:watch
npm run build
npm run build:demo