Floating Window

A movable, resizable window over your app, the way a desktop keeps a second document at hand. Drag it by its title bar, resize it from the corner, double-click the bar to fold it away. Geometry is in rem, so it scales with the root font size, and the app keeps the state.

Installation#

# One-time setup (tokens + lib helpers)npx koalaui-cli@latest init# Add this component (its dependencies come along)npx koalaui-cli@latest add floating-window

Usage#

usage.tsxTSX
import { FloatingWindow, FloatingWindowLayer } from "@/components/ui/floating-window"export function Example() {  const [win, setWin] = React.useState({ x: 2, y: 2, width: 24, height: 16 })  return (    <div className="relative h-screen">      <FloatingWindowLayer>        <FloatingWindow          title="Unit 12"          {...win}          onMove={(x, y) => setWin((w) => ({ ...w, x, y }))}          onResize={(width, height) => setWin((w) => ({ ...w, width, height }))}        >          <UnitDetails />        </FloatingWindow>      </FloatingWindowLayer>    </div>  )}

Anatomy#

import { FloatingWindow, FloatingWindowLayer } from "@/components/ui/floating-window"<FloatingWindowLayer>  <FloatingWindow title="…" x={…} y={…} width={…} height={…}>    …  </FloatingWindow></FloatingWindowLayer>

Managing windows#

The window is controlled: it reports a drag through onMove(x, y) and a resize through onResize(width, height), already kept inside the layer and above the minimum, and draws whatever you pass back. The z-order is yours too: give each window a zIndex and raise it in onFocusWindow, which fires when the window is pressed anywhere or focus moves into it from outside. A few lines of state are the whole window manager.

use-windows.tsTSX
function useWindows(initial: Win[]) {  const [windows, setWindows] = React.useState(initial)  const top = React.useRef(initial.length)  const patch = (id: string, change: Partial<Win>) =>    setWindows((ws) => ws.map((w) => (w.id === id ? { ...w, ...change } : w)))  return {    windows,    focus: (id: string) => patch(id, { zIndex: ++top.current }),    move: (id: string, x: number, y: number) => patch(id, { x, y }),    resize: (id: string, width: number, height: number) => patch(id, { width, height }),    toggle: (id: string) =>      setWindows((ws) => ws.map((w) => (w.id === id ? { ...w, minimized: !w.minimized } : w))),    close: (id: string) => setWindows((ws) => ws.filter((w) => w.id !== id)),  }}

Minimize and controls#

minimized folds the window up to its title bar, like a window shade. Its content stays mounted (a half-written note survives) and turns inert until it unfolds. The bar shows a control only for a handler you pass: onToggleMinimize adds the minimize button and the double-click on the bar, onClose adds the close button. Double-click the folded bar below.

Sizing in rem#

x, y, width, height, minWidth (18 by default) and minHeight (4.25: the bar and 2rem of content) are all rem, so a UI that scales by changing the root font size scales its windows with it. The pointer is converted with the root font size read when a drag starts. A move keeps 6rem of the window inside the layer on the left (the controls) and 4rem on the right, and the bar never leaves the top or bottom; a resize stops half a rem short of the layer's far edges. Without a FloatingWindowLayer around it, a window is unbounded.

API reference#

FloatingWindowLayer

An absolute inset-0 <div> over its positioned parent, transparent to the pointer except where a window is. Its box bounds the windows inside it; a FloatingWindow outside a layer still works, without bounds. Every div prop is forwarded, ref included; className is merged last.

Data attributeDescription
data-slot"floating-window-layer".

FloatingWindow

A non-modal role="dialog" named by its title (or your aria-label). Controlled: it reports moves and resizes and the app keeps the state, z-order included. Every other div prop is forwarded; style merges over the geometry and className is merged last.

PropTypeDefaultDescription
title*React.ReactNode-The window's name: shown in the bar, and the dialog's accessible name.
iconReact.ElementType-A glyph before the title: the component, not an element (icon={Browser}).
x*number-Left edge inside the layer, in rem.
y*number-Top edge inside the layer, in rem.
width*number-Width in rem.
height*number-Height in rem, while not minimized.
minimizedbooleanfalseFolded to the title bar. The content stays mounted (and inert).
zIndexnumber-Stacking order among the layer's windows. Raise it in onFocusWindow.
minWidthnumber18Smallest width a resize can reach, in rem.
minHeightnumber4.25Smallest height a resize can reach, in rem: the bar and 2rem of content.
onFocusWindow() => void-The window was pressed, or focus moved into it: bring it to the front.
onMove(x: number, y: number) => void-A drag of the title bar: the new x and y, in rem, already kept inside the layer.
onResize(width: number, height: number) => void-A drag of the corner grip: the new width and height, in rem, within the limits.
onToggleMinimize() => void-Shows the minimize button and enables double-click on the bar.
onClose() => void-Shows the close button.
minimizeLabelstring"Minimize"The minimize button's accessible name.
closeLabelstring"Close"The close button's accessible name.
staticbooleanfalseDrops the press scale of the bar's controls.
Data attributeDescription
data-slot"floating-window", with floating-window-bar, -title, -controls, -minimize, -close, -body and -grip inside.
data-state"open" or "minimized", on the window and on its body.
CSS variableDescription
--surfaceSet to var(--popover) on the body, the ground its controls paint on.

FAQ#

A Dialog interrupts: it is modal, centred and closes before you go on. A Floating Window sits beside the work, non-modal, so you can keep a record open while you use the page under it, and keep several.