Skip to content

Floating Windows

Draggable, resizable panels rendered as portals to document.body. They persist across in-page navigation and stay open while the user works elsewhere in the app.


Setup

FloatingWindowProvider and FloatingWindowOutlet are already mounted in ClientProviders. No per-page setup is needed. See UI Providers for the full provider tree.


useFloatingWindow()

import { useFloatingWindow } from "@/hooks/useFloatingWindow";

const win = useFloatingWindow();

win.open({
  title: "Image Inspector",
  content: <MyPanel />,
  initialWidth: 600,
  initialHeight: 400,
});

// win.close()  — close the window
// win.isOpen   — boolean

The hook auto-generates a stable ID per instance and auto-closes the window when the component unmounts.


FloatingWindowConfig

type FloatingWindowConfig = {
  content: ReactNode;          // Panel body
  title: string;               // Header title
  initialWidth?: number;       // px, default 480
  initialHeight?: number;      // px, default 360
  minWidth?: number;           // px, default 240
  minHeight?: number;          // px, default 180
  initialPosition?: { x: number; y: number }; // viewport coords; centered if omitted
  onClose?: () => void;        // Called after the window closes
};

Shell Behavior

Each floating window is rendered in a FloatingWindowShell:

  • Drag — grab the grip icon in the header to reposition
  • Resize — 8-way handles (n / s / e / w / ne / nw / se / sw) with matching cursors
  • Fullscreen toggleMaximize2 expands to fill the viewport; Minimize2 restores the previous rect
  • Escape — closes the window, unless a modal backdrop or compare mode is currently active (in which case Escape is forwarded to those first)
  • Viewport clamping — windows cannot be dragged fully off-screen; minimum size is enforced during resize

Provider Tree

FloatingWindowProvider          ← stores config map in memory
  └── FloatingWindowOutlet      ← renders portals via useSyncExternalStore
        └── FloatingWindowShell (one per open window)
              └── config.content

FloatingWindowOutlet subscribes to the provider via useSyncExternalStore so it re-renders only when the config version changes, not on every state update.


Current Usage

MediaClipboardFab is the only consumer. It opens the clipboard panel as a floating window:

const win = useFloatingWindow();

win.open({
  title: "Media Clipboard",
  content: <MediaClipboardContent onClose={win.close} />,
  initialWidth: 700,
  initialHeight: 500,
});

This provides a canonical implementation reference for adding new consumers.


Source Files

File Contents
src/providers/FloatingWindowProvider.tsx Context, config store, useFloatingWindowConfigVersion
src/components/FloatingWindow/FloatingWindowOutlet.tsx Portal renderer
src/components/FloatingWindow/FloatingWindowShell.tsx Draggable/resizable shell
src/hooks/useFloatingWindow.ts Consumer hook
src/types/floatingWindow.ts FloatingWindowConfig type