The Core Idea
The system centers on a few pieces that work together: a Zustand store for window state, a config file that declares what each window is, and a few UI components that handle the visual side.
Here's how I approached building a desktop-like windowing system inside a React application. You can try a live version at /os.
Managing Windows with State
Every window can be open, closed, minimized, or maximized. The Zustand store keeps track of all of them, including their sizes and z-index order. When you open a window, it gets the highest z-index and appears on top. If you click a window that is already focused, it shakes instead of doing nothing. It is a small detail but it gives feedback.
openWindow: (id) =>
set(({ windows }) => ({
windows: windows.map((w) => {
if (w.id !== id) return w;
if (w.status === "closed" || w.status === "minimized") {
return { ...w, status: "open", zIndex: topZ(windows) + 1 };
}
if (w.zIndex === maxZ) return { ...w, isShaking: true };
return { ...w, zIndex: topZ(windows) + 1 };
}),
})),A helper hook called useWindowControl wraps the store actions for a single window. And useIsFocused tells you which window is on top so you can highlight its title bar.
Window Definitions
Instead of hardcoding each window's content, I used a schema. Each entry declares the id, title, icon, default size, position, and the React element to render inside.
{
id: "hero",
title: "~/sohanscript",
icon: "lucide:terminal",
initialPosition: { left: 150, bottom: 200 },
element: <Hero bare />,
}Some windows link to separate pages. Those use a string URL instead of a React element. The click handler checks which type it is and either opens the window or sends it to the router.
The Window Component
Each window is a motion.div from Motion (Framer Motion). You can drag it by the title bar, resize it from any edge or corner, and it animates when minimizing or maximizing.
<motion.div
drag={!maximized && !minimized}
dragControls={controls}
style={{ x, y, zIndex, width, height }}
onPointerDown={() => bringToFront()}
>
{/* title bar with macOS action buttons */}
{/* content area */}
</motion.div>The resize handles are thin invisible buttons along every edge and corner. On mousedown they track the mouse and update the window dimensions through the store.
The Taskbar
A sidebar dock lists all the apps. Each icon shows an indicator when its window is open. It dims when the window is minimized. Clicking an app opens it if closed, brings it to front if open, and reopens it if minimized.
<UbuntuDash
apps={WINDOW_CONFIGS.map((cfg) => ({
id: cfg.id, icon: cfg.icon, name: cfg.title,
}))}
onAppClick={(id) => {
const win = windows.find((w) => w.id === id);
if (!win || win.status === "closed" || win.status === "minimized") {
openWindow(id);
} else {
bringToFront(id);
}
}}
/>There is also an app grid overlay. A button at the bottom of the dash opens it and gives you a full screen launcher view.
Putting It Together
The parent component closes all windows first for a clean slate, then renders the taskbar and a WindowRenderer that filters the store and maps each open window to a Window component.
function OSEnvironment() {
const { openWindow, closeAllWindow } = useWindowStore();
useEffectOnce(closeAllWindow);
return (
<>
<WindowRenderer />
<UbuntuDash apps={...} onAppClick={...} />
</>
);
}The WindowRenderer lazy loads the Window component so you are not pulling in heavy animation dependencies upfront.
const Window = lazy(() =>
import("./window").then((m) => ({ default: m.Window })),
);
export function WindowRenderer() {
const windows = useWindowStore((s) => s.windows);
return windows
.filter((w) => w.status !== "closed")
.map((win) => {
const cfg = WINDOW_CONFIGS.find((c) => c.id === win.id);
return (
<Window key={win.id} id={win.id}>
{cfg.element}
</Window>
);
});
}What You Get
The result is a reusable windowing layer. Windows manage themselves through the store, the taskbar shows real time status, and adding a new window is just another config entry. The router handles page level navigation. The window manager handles on screen layout. They only meet in the click handler.
That separation is the part worth keeping. The window manager does not know about routing. The router does not know about window positions.
