Common Patterns
Unsaved changes — prevent close while dirty
Section titled “Unsaved changes — prevent close while dirty”Set closable={false} when the user has unsaved changes. The X button becomes inert; your UI handles the prompt.
<Window open={showEditor} closable={!hasUnsavedChanges} onUserClose={() => setShowEditor(false)}> <Editor /> {hasUnsavedChanges && <SavePrompt />}</Window>Imperative control from parent
Section titled “Imperative control from parent”Use a ref to call window methods from outside the window’s children.
const ref = useRef<WindowRef>(null);
<Window ref={ref} open={show}> <Content /></Window>;
// Laterref.current?.focus();ref.current?.setBounds({ width: 1024, height: 768 });For control from inside the window’s children, use useCurrentWindow() instead.
Multiple independent windows
Section titled “Multiple independent windows”Each <Window> manages its own lifecycle independently.
<Window open={showA} title="Window A"> <ContentA /></Window><Window open={showB} title="Window B"> <ContentB /></Window>Hidden window with preserved state
Section titled “Hidden window with preserved state”open={true} visible={false} creates the window and mounts content without showing it. State is fully initialized and preserved while hidden.
<Window open={true} visible={showPanel}> <ExpensivePanel /></Window>Useful when you want content pre-initialized before the user opens it, or when toggling visibility without unmounting.
Controlled size with two-way sync
Section titled “Controlled size with two-way sync”Use the controlled width/height props with onBoundsChange to keep your state in sync with user resizing.
const [size, setSize] = useState({ width: 800, height: 600 });
<Window open={show} width={size.width} height={size.height} onBoundsChange={(bounds) => setSize({ width: bounds.width, height: bounds.height })}> <Content /></Window>For fire-and-forget sizing, use defaultWidth / defaultHeight instead. See Props Reference for the full geometry API.
Persistent overlay
Section titled “Persistent overlay”Combine pooling and persistence for overlays that remember their position.
const hudPool = createWindowPool( { transparent: true, frame: false }, { minIdle: 1 },);
function HUD() { const { bounds, save } = usePersistedBounds("hud", { defaultWidth: 300, defaultHeight: 200, });
return ( <PooledWindow pool={hudPool} open={show} {...bounds} onBoundsChange={save}> <HUDContent /> </PooledWindow> );}See Pooling and Persistence for details.
Integration Notes
Section titled “Integration Notes”UI Library Portals (Radix UI, Base UI)
Section titled “UI Library Portals (Radix UI, Base UI)”Radix UI and Base UI portal overlays (dropdowns, dialogs, tooltips) to document.body by default. Inside a <Window>, this means they’ll render in the parent window’s body — invisible to the user in the child window.
Fix this by passing the child window’s document.body as the portal container:
import { useWindowDocument } from "@loc/electron-window";
function WindowContent({ children }) { const doc = useWindowDocument(); return ( // Radix UI example — use whichever container prop your library provides <DropdownMenu.Root> <DropdownMenu.Portal container={doc.body}> <DropdownMenu.Content>...</DropdownMenu.Content> </DropdownMenu.Portal> </DropdownMenu.Root> );}For a library-wide fix, wrap your window content in a provider that sets the default container:
function WindowShell({ children }) { const doc = useWindowDocument(); // Base UI example return ( <PortalContext.Provider value={doc.body}> {children} </PortalContext.Provider> );}
<Window open={show}> <WindowShell> <AppContent /> </WindowShell></Window>useWindowDocument() throws if called outside an open window, so it’s safe to use without null-checking — the window is open by the time children render.
Theme Attributes on Child Document
Section titled “Theme Attributes on Child Document”The child window starts with a blank <html> element — no theme class, no data-theme attribute. Copy them from the parent:
import { useWindowDocument } from "@loc/electron-window";import { useEffect } from "react";
function ThemeSync({ children }) { const doc = useWindowDocument();
useEffect(() => { const parentHtml = window.document.documentElement; const childHtml = doc.documentElement;
// Initial sync childHtml.className = parentHtml.className; childHtml.setAttribute("data-theme", parentHtml.getAttribute("data-theme") ?? "");
// Keep in sync as theme changes const observer = new MutationObserver(() => { childHtml.className = parentHtml.className; childHtml.setAttribute("data-theme", parentHtml.getAttribute("data-theme") ?? ""); }); observer.observe(parentHtml, { attributes: true, attributeFilter: ["class", "data-theme"] }); return () => observer.disconnect(); }, [doc]);
return <>{children}</>;}Framer Motion
Section titled “Framer Motion”Framer Motion uses window.performance and document.timeline for animation scheduling. Because window.open creates a new browsing context, the child window has its own independent timeline. Animations that coordinate across windows (e.g., shared layout transitions) will not synchronize — this is a known upstream limitation.
For animations contained entirely within one window this is not an issue.
Window Dimensions
Section titled “Window Dimensions”window.innerWidth / window.innerHeight resolve to the parent window’s dimensions inside child window content, because window in the React render context is the opener. Use the element’s own document to get the correct dimensions:
// Avoid — reads the parent window's viewportconst { innerWidth, innerHeight } = window;
// Correct — reads the child window's viewportconst win = element.ownerDocument.defaultView;const { innerWidth, innerHeight } = win ?? {};For layout-driven sizing, prefer ResizeObserver on the scroll container rather than reading innerHeight at all:
useEffect(() => { const container = scrollContainerRef.current; if (!container) return; const ro = new ResizeObserver(([entry]) => { setHeight(entry.contentRect.height); }); ro.observe(container); return () => ro.disconnect();}, []);Frameless window with custom drag region
Section titled “Frameless window with custom drag region”<Window open={show} frame={false} titleBarStyle="hidden" defaultWidth={400} defaultHeight={300}> <div style={{ WebkitAppRegion: "drag", height: 32 }}> {/* Drag handle */} </div> <div style={{ WebkitAppRegion: "no-drag" }}> <Content /> </div></Window>frame and titleBarStyle are creation-only — set them once and don’t change them. If you need to change them dynamically, use recreateOnShapeChange.