Window
The window frame: corner radius, shadow, traffic lights, the inactive state, dragging, resizing, and minimizing into the Dock.
@cupertinoui/windowRegistry JSON "use client"
import { Button } from "@/components/ui/button"
import {
Window,
WindowContent,
WindowControls,
WindowMain,
WindowTitle,
WindowTitlebar,
} from "@/components/ui/window"
export default function WindowDemo() {
return (
<Window className="h-56 w-full max-w-md">
<WindowMain>
<WindowTitlebar>
<WindowControls />
<WindowTitle>Untitled</WindowTitle>
</WindowTitlebar>
<WindowContent className="flex flex-col items-center justify-center gap-3 p-6 text-center">
<p className="text-mac-label-secondary">Windows have a 16pt corner radius, a soft shadow and the traffic lights in the top-left corner.</p>
<Button variant="secondary">Open…</Button>
</WindowContent>
</WindowMain>
</Window>
)
}Installation
pnpm dlx shadcn@latest add @cupertinoui/windowThe first install also adds the macOS theme. See Installation to set up the registry.
Usage
import {
Window,
WindowContent,
WindowControls,
WindowMain,
WindowTitle,
WindowTitlebar,
} from "@/components/ui/window"
<Window>
<WindowMain>
<WindowTitlebar>
<WindowControls />
<WindowTitle>Untitled</WindowTitle>
</WindowTitlebar>
<WindowContent>…</WindowContent>
</WindowMain>
</Window>Examples
Inactive
Unfocused windows have grey traffic lights, a dimmed title and turn their accent color grey.
"use client"
import {
Window,
WindowContent,
WindowControls,
WindowMain,
WindowTitle,
WindowTitlebar,
} from "@/components/ui/window"
// The window in the back: grey traffic lights, a dimmed title and a grey accent.
export default function WindowInactive() {
return (
<div className="relative h-64 w-full max-w-lg">
<Window inactive className="absolute top-0 left-0 h-44 w-72">
<WindowMain>
<WindowTitlebar>
<WindowControls />
<WindowTitle>Inactive</WindowTitle>
</WindowTitlebar>
<WindowContent />
</WindowMain>
</Window>
<Window className="absolute right-0 bottom-0 h-44 w-72">
<WindowMain>
<WindowTitlebar>
<WindowControls />
<WindowTitle>Active</WindowTitle>
</WindowTitlebar>
<WindowContent />
</WindowMain>
</Window>
</div>
)
}Draggable
With draggable, the window moves when you drag its title bar, toolbar or the strip above its sidebar. Buttons, fields and other controls in those areas keep working. bounds="parent" keeps the window inside its parent.
"use client"
import * as React from "react"
import {
Window,
WindowContent,
WindowControls,
WindowMain,
WindowTitle,
WindowTitlebar,
} from "@/components/ui/window"
const windows = [
{ id: "notes", title: "Notes", className: "top-4 left-4" },
{ id: "preview", title: "Preview", className: "right-4 bottom-4" },
]
// Drag a window by its title bar. Clicking a window brings it to the front
// and makes it active; bounds="parent" keeps it inside this area.
export default function WindowDraggable() {
const [order, setOrder] = React.useState(windows.map((item) => item.id))
const active = order[order.length - 1]
return (
<div className="relative h-80 w-full max-w-xl">
{windows.map((item) => (
<Window
key={item.id}
draggable
bounds="parent"
inactive={item.id !== active}
onPointerDownCapture={() =>
setOrder((current) => [...current.filter((id) => id !== item.id), item.id])
}
className={`absolute h-40 w-64 ${item.className}`}
style={{ zIndex: order.indexOf(item.id) + 1 }}
>
<WindowMain>
<WindowTitlebar>
<WindowControls />
<WindowTitle>{item.title}</WindowTitle>
</WindowTitlebar>
<WindowContent className="flex items-center justify-center p-4 text-center text-mac-label-secondary">
Drag me by the title bar.
</WindowContent>
</WindowMain>
</Window>
))}
</div>
)
}Minimize and zoom
Give the window onClose, onMinimize and onZoom and the traffic lights work. minimized plays the Genie effect into the minimizeTo element, here the app's Dock icon, and back out again. maximized zooms the window to fill its parent, minus maximizeInsets; double-clicking the title bar zooms too.
"use client"
import * as React from "react"
import { FileTextIcon } from "lucide-react"
import { Dock, DockIcon, DockItem } from "@/components/ui/dock"
import {
Window,
WindowContent,
WindowControls,
WindowMain,
WindowTitle,
WindowTitlebar,
} from "@/components/ui/window"
// The yellow traffic light minimizes the window into its app's Dock icon
// with the Genie effect; click the icon to bring it back. The green one
// zooms it to fill the area, the red one closes it.
export default function WindowMinimize() {
const [open, setOpen] = React.useState(true)
const [minimized, setMinimized] = React.useState(false)
const [maximized, setMaximized] = React.useState(false)
const icon = React.useRef<HTMLButtonElement>(null)
return (
<div className="relative h-96 w-full max-w-2xl overflow-hidden">
{open && (
<Window
className="absolute inset-x-0 top-6 mx-auto h-56 w-80"
minimized={minimized}
minimizeTo={icon}
maximized={maximized}
maximizeInsets={{ top: 8, right: 8, bottom: 88, left: 8 }}
onClose={() => {
setOpen(false)
setMaximized(false)
}}
onMinimize={() => setMinimized(true)}
onZoom={() => setMaximized((value) => !value)}
>
<WindowMain>
<WindowTitlebar>
<WindowControls />
<WindowTitle>Notes.txt</WindowTitle>
</WindowTitlebar>
<WindowContent className="p-4 text-mac-label-secondary">
Minimize me into the Dock, or double-click the title bar to zoom.
</WindowContent>
</WindowMain>
</Window>
)}
<Dock size={48} className="absolute bottom-2 left-1/2 -translate-x-1/2">
<DockItem
ref={icon}
label="TextEdit"
running={open}
onClick={() => {
setOpen(true)
setMinimized(false)
}}
>
<DockIcon className="bg-linear-to-b from-white to-zinc-200 text-zinc-600">
<FileTextIcon />
</DockIcon>
</DockItem>
</Dock>
</div>
)
}Resizable
With resizable, the edges and corners resize the window, between minSize and maxSize. Double-click an edge to stretch it to the edge of the parent; hold Option to stretch the opposite edge too. Here the size is controlled and shown in the window.
"use client"
import * as React from "react"
import {
Window,
WindowContent,
WindowControls,
WindowMain,
WindowTitle,
WindowTitlebar,
} from "@/components/ui/window"
// Drag an edge or a corner to resize the window. Double-click an edge to
// stretch it to the edge of the area; with Option, the opposite edge too.
export default function WindowResizable() {
const [size, setSize] = React.useState({ width: 360, height: 220 })
return (
<div className="relative h-96 w-full max-w-2xl">
<Window
resizable
draggable
bounds="parent"
size={size}
onSizeChange={setSize}
minSize={{ width: 240, height: 140 }}
className="absolute top-8 left-8"
>
<WindowMain>
<WindowTitlebar>
<WindowControls />
<WindowTitle>Untitled</WindowTitle>
</WindowTitlebar>
<WindowContent className="flex items-center justify-center text-mac-label-secondary tabular-nums">
{size.width} × {size.height}
</WindowContent>
</WindowMain>
</Window>
</div>
)
}API Reference
Every part forwards the props of the underlying element. The props below are the ones CupertinoUI adds or changes.
Window
| Prop | Type | Description |
|---|---|---|
inactive | booleanDefault: false | Renders the unfocused look. |
draggable | booleanDefault: false | Lets people move the window by its title bar, toolbar or sidebar header. The offset is applied with the CSS translate property, so it works with any positioning. |
bounds | "parent" | Keeps a dragged or resized window inside its parent element. |
defaultPosition | { x: number; y: number }Default: { x: 0, y: 0 } | The initial offset of an uncontrolled draggable window. |
position | { x: number; y: number } | The offset of a controlled window. Update it from onPositionChange. |
onPositionChange | (position: { x: number; y: number }) => void | Called while the window is dragged, and when resizing moves its top or left edge. |
resizable | booleanDefault: false | Lets people resize the window by its edges and corners. |
defaultSize | { width: number; height: number } | The initial size of an uncontrolled window. Without it, the window keeps the size its classes give it until it is resized. |
size | { width: number; height: number } | The size of a controlled window. Update it from onSizeChange. |
onSizeChange | (size: { width: number; height: number }) => void | Called while the window is resized. |
minSize | { width?: number; height?: number }Default: { width: 240, height: 140 } | The smallest size the window can be resized to. |
maxSize | { width?: number; height?: number } | The largest size. Give minSize and maxSize the same width (or height) to fix it; only the other edges resize then. |
onClose / onMinimize / onZoom | () => void | Handlers for the red, yellow and green traffic lights of the window's WindowControls. onZoom also runs when the title bar or toolbar is double-clicked. |
minimized | booleanDefault: false | Animates the window into minimizeTo and hides it; set it back to false to animate it out again. |
minimizeTo | RefObject<Element | null> | The element the window minimizes into, such as a DockItem. Without it the window shrinks toward the bottom of the screen. |
minimizeEffect | "genie" | "scale"Default: "genie" | The Genie effect, or a plain scale. |
maximized | booleanDefault: false | Zooms the window to fill its positioned parent, animating between the two frames. |
maximizeInsets | { top?, right?, bottom?, left?: number } | Space to leave around a zoomed window, e.g. for a menu bar or Dock. |
className | string | Extra classes, merged with the component's own. |
WindowControls
| Prop | Type | Description |
|---|---|---|
onClose / onMinimize / onZoom | () => void | Handlers for the traffic lights. By default they call the handlers of the Window. |
WindowMain
Stacks the toolbar or title bar above the content, to the right of a sidebar.
| Prop | Type | Description |
|---|---|---|
className | string | Extra classes, merged with the component's own. |
WindowTitlebar / WindowTitle / WindowContent
| Prop | Type | Description |
|---|---|---|
className | string | Extra classes, merged with the component's own. |