macOS window chrome: traffic lights whose glyphs appear on hover, a centered title or unified toolbar, an optional translucent sidebar with vibrancy, and active and inactive appearances.
"use client"
import * as React from "react"
import { Check, Folder, Hash, ListChecks, Search, Share, SquarePen, Trash2 } from "lucide-react"
import {
MacWindow,
MacWindowContent,
MacWindowControls,
MacWindowMain,
MacWindowSidebar,
MacWindowSidebarItem,
MacWindowTitleBar,
MacWindowToolbarButton,
} from "@/components/ballmac/mac-window"
const folders = [
{ name: "All notes", count: 24 },
{ name: "Ideas", count: 5 },
{ name: "Launch", count: 8 },
{ name: "Recipes", count: 4 },
{ name: "Travel", count: 3 },
]
const initialTasks = [
{ label: "Record the product walkthrough", done: true },
{ label: "Final pass on pricing copy", done: true },
{ label: "Notarize and staple the DMG", done: false },
{ label: "Schedule the launch post for 9:00", done: false },
]
export default function MacWindowDemo() {
const [folder, setFolder] = React.useState("Launch")
const [tasks, setTasks] = React.useState(initialTasks)
return (
<div className="relative isolate flex w-full max-w-[640px] justify-center overflow-hidden rounded-2xl border border-foreground/10 px-4 py-8 sm:px-8">
<div aria-hidden="true" className="pointer-events-none absolute inset-0 -z-10 dark:brightness-[0.55]">
<div className="absolute inset-0 bg-linear-to-br from-chart-3 via-chart-5 to-chart-4" />
<div className="absolute -inset-x-1/4 top-1/3 h-full rounded-[50%] bg-white/20 blur-2xl" />
</div>
<MacWindow className="h-[340px] w-full" onClose={() => {}} onMinimize={() => {}} onZoom={() => {}}>
<MacWindowSidebar className="max-sm:hidden">
<nav aria-label="Folders" className="flex flex-col gap-0.5 px-2.5">
<p className="px-2 pb-1 text-[11px] font-semibold text-muted-foreground">iCloud</p>
{folders.map((f) => (
<MacWindowSidebarItem key={f.name} selected={folder === f.name} onClick={() => setFolder(f.name)}>
<Folder className="text-chart-3" aria-hidden="true" />
<span className="flex-1 truncate">{f.name}</span>
<span className="text-xs text-muted-foreground tabular-nums">{f.count}</span>
</MacWindowSidebarItem>
))}
<p className="px-2 pt-3 pb-1 text-[11px] font-semibold text-muted-foreground">Tags</p>
<div className="flex flex-wrap gap-1 px-1.5">
{["launch", "design", "mac"].map((tag) => (
<span key={tag} className="inline-flex h-6 items-center gap-0.5 rounded-full bg-foreground/[0.07] px-2 text-xs text-foreground/70">
<Hash className="size-3" aria-hidden="true" />
{tag}
</span>
))}
</div>
</nav>
</MacWindowSidebar>
<MacWindowMain>
<MacWindowTitleBar
controls={false}
title={
<span className="flex items-center gap-3">
<MacWindowControls className="pointer-events-auto sm:hidden" />
<span className="flex flex-col leading-tight">
<span>{folder}</span>
<span className="text-[11px] font-normal text-muted-foreground">{folders.find((f) => f.name === folder)?.count} notes</span>
</span>
</span>
}
>
<MacWindowToolbarButton aria-label="Delete note" className="max-sm:hidden">
<Trash2 aria-hidden="true" />
</MacWindowToolbarButton>
<MacWindowToolbarButton aria-label="Checklist">
<ListChecks aria-hidden="true" />
</MacWindowToolbarButton>
<MacWindowToolbarButton aria-label="Share" className="max-sm:hidden">
<Share aria-hidden="true" />
</MacWindowToolbarButton>
<MacWindowToolbarButton aria-label="New note">
<SquarePen aria-hidden="true" />
</MacWindowToolbarButton>
<MacWindowToolbarButton aria-label="Search">
<Search aria-hidden="true" />
</MacWindowToolbarButton>
</MacWindowTitleBar>
<MacWindowContent className="px-6 py-4 sm:px-8">
<p className="text-center text-[11px] text-muted-foreground">September 29, 2026 at 9:41 AM</p>
<h3 className="mt-3 text-xl font-bold tracking-tight">Launch checklist</h3>
<ul className="mt-3 flex flex-col gap-2">
{tasks.map((task, i) => (
<li key={task.label}>
<label className="flex cursor-default items-center gap-2.5 text-sm">
<input
type="checkbox"
checked={task.done}
onChange={() => setTasks((t) => t.map((x, j) => (j === i ? { ...x, done: !x.done } : x)))}
className="peer sr-only"
/>
<span
aria-hidden="true"
className="flex size-[18px] shrink-0 items-center justify-center rounded-full border border-foreground/25 text-white transition-colors peer-checked:border-transparent peer-checked:bg-chart-3 peer-focus-visible:ring-[3px] peer-focus-visible:ring-ring/50"
>
{task.done ? <Check className="size-3" strokeWidth={3} /> : null}
</span>
<span className={task.done ? "text-muted-foreground line-through decoration-foreground/30" : ""}>{task.label}</span>
</label>
</li>
))}
</ul>
<p className="mt-4 text-sm leading-relaxed text-foreground/70">
Ship Tuesday morning. Keep the changelog short and lead with the menu bar redesign.
</p>
</MacWindowContent>
</MacWindowMain>
</MacWindow>
</div>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/mac-windowAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/i18nCopy the source into your project.
components/ballmac/mac-window.tsx// Ballmac UI: Mac Window. https://ui.ballmac.com/components/mac-window "use client" import * as React from "react" import { cn } from "@/lib/utils" import { useMessages } from "@/lib/ballmac/i18n" type MacWindowContextValue = { active: boolean onClose?: () => void onMinimize?: () => void onZoom?: () => void } const MacWindowContext = React.createContext<MacWindowContextValue>({ active: true }) type MacWindowProps = React.ComponentProps<"div"> & { /** Active (key) window: colored traffic lights and full-strength title. Inactive windows gray out, as on macOS. */ active?: boolean /** Called by the red close button. Without a handler the light is drawn but not focusable. */ onClose?: () => void /** Called by the yellow minimize button. */ onMinimize?: () => void /** Called by the green zoom (full screen) button. */ onZoom?: () => void } /** * A macOS window frame. Compose it from MacWindowTitleBar + MacWindowContent, or MacWindowSidebar + * MacWindowMain for a sidebar layout with vibrancy. Traffic-light colors are the --mac-close, --mac-minimize * and --mac-zoom CSS variables; override them on the window if you like. */ function MacWindow({ active = true, onClose, onMinimize, onZoom, className, style, ...props }: MacWindowProps) { const context = React.useMemo(() => ({ active, onClose, onMinimize, onZoom }), [active, onClose, onMinimize, onZoom]) return ( <MacWindowContext.Provider value={context}> <div data-slot="mac-window" data-state={active ? "active" : "inactive"} className={cn( "group/mac-window relative isolate flex min-w-0 flex-col overflow-hidden rounded-[14px] bg-card text-card-foreground has-[[data-slot=mac-window-sidebar]]:flex-row has-[[data-slot=mac-window-sidebar]]:bg-transparent", "shadow-[0_0_0_0.5px_rgb(0_0_0/0.22),0_24px_60px_-12px_rgb(0_0_0/0.35),0_8px_20px_-8px_rgb(0_0_0/0.2)]", "dark:shadow-[0_0_0_0.5px_rgb(0_0_0/0.9),inset_0_0_0_0.5px_rgb(255_255_255/0.16),0_24px_60px_-12px_rgb(0_0_0/0.7)]", "data-[state=inactive]:shadow-[0_0_0_0.5px_rgb(0_0_0/0.18),0_12px_30px_-12px_rgb(0_0_0/0.25)]", "dark:data-[state=inactive]:shadow-[0_0_0_0.5px_rgb(0_0_0/0.9),inset_0_0_0_0.5px_rgb(255_255_255/0.1),0_12px_30px_-12px_rgb(0_0_0/0.5)]", className )} style={ { "--mac-close": "oklch(0.7 0.19 25)", "--mac-minimize": "oklch(0.84 0.16 84)", "--mac-zoom": "oklch(0.76 0.19 145)", ...style, } as React.CSSProperties } {...props} /> </MacWindowContext.Provider> ) } const glyphs = { close: <path d="M3.5 3.5l5 5m0-5l-5 5" strokeWidth="1.3" strokeLinecap="round" />, minimize: <path d="M3 6h6" strokeWidth="1.4" strokeLinecap="round" />, zoom: <path d="M3.4 8.6V5l3.6 3.6zM8.6 3.4V7L5 3.4z" stroke="none" fill="currentColor" />, } as const function TrafficLight({ kind, label, onPress }: { kind: keyof typeof glyphs; label: string; onPress?: () => void }) { const shared = cn( "relative flex size-3 shrink-0 items-center justify-center rounded-full text-black/60 outline-none", "bg-(--light) shadow-[inset_0_0_0_0.5px_rgb(0_0_0/0.18)] dark:shadow-[inset_0_0_0_0.5px_rgb(255_255_255/0.12)]", "group-data-[state=inactive]/mac-window:bg-foreground/15 group-data-[state=inactive]/mac-window:group-hover/lights:bg-(--light)" ) const style = { "--light": `var(--mac-${kind})` } as React.CSSProperties const glyph = ( <svg viewBox="0 0 12 12" aria-hidden="true" stroke="currentColor" fill="none" className="size-3 opacity-0 transition-opacity duration-100 group-hover/lights:opacity-100 group-has-[:focus-visible]/lights:opacity-100" > {glyphs[kind]} </svg> ) if (!onPress) { return ( <span data-slot={`mac-window-${kind}`} className={shared} style={style} aria-hidden="true"> {glyph} </span> ) } return ( <button type="button" data-slot={`mac-window-${kind}`} aria-label={label} onClick={onPress} className={cn(shared, "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background")} style={style} > {glyph} </button> ) } type MacWindowControlsProps = React.ComponentProps<"div"> & { /** Accessible labels for the three buttons. */ labels?: { close?: string; minimize?: string; zoom?: string } } /** The red, yellow and green traffic lights. Glyphs appear when the pointer is over any of them. */ function MacWindowControls({ labels, className, ...props }: MacWindowControlsProps) { const { onClose, onMinimize, onZoom } = React.useContext(MacWindowContext) const msg = useMessages() return ( <div data-slot="mac-window-controls" dir="ltr" className={cn("group/lights flex shrink-0 items-center gap-2", className)} {...props}> <TrafficLight kind="close" label={labels?.close ?? msg("mac-window.close", "Close window")} onPress={onClose} /> <TrafficLight kind="minimize" label={labels?.minimize ?? msg("mac-window.minimize", "Minimize window")} onPress={onMinimize} /> <TrafficLight kind="zoom" label={labels?.zoom ?? msg("mac-window.zoom", "Zoom window")} onPress={onZoom} /> </div> ) } type MacWindowTitleBarProps = Omit<React.ComponentProps<"div">, "title"> & { /** Window title, centered in the bar. */ title?: React.ReactNode /** Draw the traffic lights at the leading edge. Turn off when a MacWindowSidebar already shows them. */ controls?: boolean } /** * The title bar. With only a title it is a compact 28px bar; children render as a trailing toolbar * and grow it to the 52px unified-toolbar height. */ function MacWindowTitleBar({ title, controls = true, className, children, ...props }: MacWindowTitleBarProps) { const hasToolbar = React.Children.count(children) > 0 return ( <div data-slot="mac-window-titlebar" className={cn( "relative flex shrink-0 items-center gap-3 border-b border-foreground/[0.08] px-3 select-none", hasToolbar ? "h-[52px] px-4" : "h-8", className )} {...props} > {controls ? <MacWindowControls /> : null} {title ? ( <div data-slot="mac-window-title" className={cn( "pointer-events-none truncate text-[13px] font-semibold text-foreground/85 group-data-[state=inactive]/mac-window:text-muted-foreground", hasToolbar ? "min-w-0" : "absolute inset-x-20 text-center" )} > {title} </div> ) : null} {hasToolbar ? ( <div data-slot="mac-window-toolbar" className="ms-auto flex min-w-0 items-center gap-1 text-foreground/70 group-data-[state=inactive]/mac-window:text-foreground/35"> {children} </div> ) : null} </div> ) } type MacWindowToolbarButtonProps = React.ComponentProps<"button"> /** A borderless toolbar button that highlights on hover, sized for 16px icons. Give icon-only buttons an aria-label. */ function MacWindowToolbarButton({ className, type = "button", ...props }: MacWindowToolbarButtonProps) { return ( <button type={type} data-slot="mac-window-toolbar-button" className={cn( "inline-flex h-7 min-w-7 shrink-0 items-center justify-center gap-1.5 rounded-md px-1.5 text-[13px] outline-none transition-colors duration-150 hover:bg-foreground/[0.07] hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0", className )} {...props} /> ) } type MacWindowSidebarProps = React.ComponentProps<"div"> & { /** Draw the traffic lights at the top of the sidebar. */ controls?: boolean } /** A translucent source-list sidebar that blurs whatever is behind the window (vibrancy). */ function MacWindowSidebar({ controls = true, className, children, ...props }: MacWindowSidebarProps) { return ( <div data-slot="mac-window-sidebar" className={cn( "flex w-52 shrink-0 flex-col border-e border-foreground/[0.08] bg-background/60 backdrop-blur-2xl backdrop-saturate-150 dark:bg-background/50", className )} {...props} > {controls ? ( <div className="flex h-[52px] shrink-0 items-center px-4"> <MacWindowControls /> </div> ) : null} {children} </div> ) } type MacWindowSidebarItemProps = React.ComponentProps<"button"> & { /** Marks the selected row (sets aria-current). */ selected?: boolean } /** A source-list row: icon, label and an optional trailing count. */ function MacWindowSidebarItem({ selected = false, className, type = "button", ...props }: MacWindowSidebarItemProps) { return ( <button type={type} data-slot="mac-window-sidebar-item" data-selected={selected ? "" : undefined} aria-current={selected ? "true" : undefined} className={cn( "flex h-7 w-full items-center gap-2 rounded-md px-2 text-start text-[13px] text-foreground/85 outline-none transition-colors duration-100 hover:bg-foreground/[0.05] focus-visible:ring-[3px] focus-visible:ring-ring/50 data-selected:bg-foreground/[0.09] data-selected:font-medium data-selected:text-foreground [&_svg]:size-4 [&_svg]:shrink-0", className )} {...props} /> ) } type MacWindowMainProps = React.ComponentProps<"div"> /** The opaque column next to a sidebar; holds the title bar and content. */ function MacWindowMain({ className, ...props }: MacWindowMainProps) { return <div data-slot="mac-window-main" className={cn("flex min-w-0 flex-1 flex-col bg-card", className)} {...props} /> } type MacWindowContentProps = React.ComponentProps<"div"> function MacWindowContent({ className, ...props }: MacWindowContentProps) { return <div data-slot="mac-window-content" className={cn("min-h-0 flex-1 overflow-auto", className)} {...props} /> } export { MacWindow, MacWindowContent, MacWindowControls, MacWindowMain, MacWindowSidebar, MacWindowSidebarItem, MacWindowTitleBar, MacWindowToolbarButton, type MacWindowContentProps, type MacWindowControlsProps, type MacWindowMainProps, type MacWindowProps, type MacWindowSidebarItemProps, type MacWindowSidebarProps, type MacWindowTitleBarProps, type MacWindowToolbarButtonProps, }Update the import paths to match your project setup.
Usage
import { MacWindow, MacWindowContent, MacWindowControls, MacWindowMain, MacWindowSidebar, MacWindowSidebarItem, MacWindowTitleBar, MacWindowToolbarButton } from "@/components/ballmac/mac-window"The full example is in the Code tab above.
Examples
Active and inactive
"use client"
import * as React from "react"
import { MacWindow, MacWindowContent, MacWindowTitleBar } from "@/components/ballmac/mac-window"
export default function MacWindowStack() {
const [front, setFront] = React.useState<"terminal" | "about">("about")
return (
<div className="relative h-[340px] w-full max-w-[560px]">
<MacWindow
active={front === "terminal"}
onPointerDown={() => setFront("terminal")}
onClose={() => {}}
onMinimize={() => {}}
onZoom={() => {}}
className={`absolute top-0 start-0 h-[250px] w-[88%] sm:w-[440px] ${front === "terminal" ? "z-10" : "z-0"}`}
>
<MacWindowTitleBar title="acme — zsh — 80×24" />
<MacWindowContent className="bg-card p-3 font-mono text-xs leading-relaxed text-foreground/80">
<p>
<span className="text-[color-mix(in_oklch,var(--chart-2),black_42%)] dark:text-chart-2">~/acme</span> <span className="text-muted-foreground">on</span> <span className="text-[color-mix(in_oklch,var(--chart-4),black_42%)] dark:text-chart-4">main</span> ❯ pnpm release
</p>
<p className="text-muted-foreground">▸ Building universal binary…</p>
<p className="text-muted-foreground">▸ Signing with Developer ID…</p>
<p>
<span className="text-[color-mix(in_oklch,var(--chart-2),black_42%)] dark:text-chart-2">✓</span> Notarized in 48s
</p>
<p>
<span className="text-[color-mix(in_oklch,var(--chart-2),black_42%)] dark:text-chart-2">✓</span> Acme-2.4.0.dmg uploaded
</p>
<p>
<span className="text-[color-mix(in_oklch,var(--chart-2),black_42%)] dark:text-chart-2">~/acme</span> ❯ <span className="inline-block h-3.5 w-1.5 translate-y-0.5 bg-foreground/60" />
</p>
</MacWindowContent>
</MacWindow>
<MacWindow
active={front === "about"}
onPointerDown={() => setFront("about")}
onClose={() => {}}
className={`absolute end-0 bottom-0 w-[260px] ${front === "about" ? "z-10" : "z-0"}`}
>
<MacWindowTitleBar className="border-b-0" />
<MacWindowContent className="flex flex-col items-center px-6 pt-2 pb-6 text-center">
<span className="flex size-16 items-center justify-center rounded-[22%] bg-linear-to-b from-chart-1/80 to-chart-1 text-2xl font-bold text-white shadow-[0_6px_16px_-6px_rgb(0_0_0/0.4)]">
A
</span>
<p className="mt-3 text-base font-semibold">Acme</p>
<p className="text-xs text-muted-foreground">Version 2.4.0 (2408)</p>
<p className="mt-3 text-[11px] text-muted-foreground">Copyright © 2026 Acme Inc.</p>
</MacWindowContent>
</MacWindow>
</div>
)
}API reference
<MacWindow>
| Prop | Type | Default |
|---|---|---|
activeActive (key) window: colored traffic lights and full-strength title. Inactive windows gray out, as on macOS. | boolean | true |
onCloseCalled by the red close button. Without a handler the light is drawn but not focusable. | () => void | — |
onMinimizeCalled by the yellow minimize button. | () => void | — |
onZoomCalled by the green zoom (full screen) button. | () => void | — |
<MacWindowControls>
| Prop | Type | Default |
|---|---|---|
labelsAccessible labels for the three buttons. | { close?: string; minimize?: string; zoom?: string } | — |
close | string | — |
minimize | string | — |
zoom | string | — |
<MacWindowTitleBar>
| Prop | Type | Default |
|---|---|---|
titleWindow title, centered in the bar. | React.ReactNode | — |
controlsDraw the traffic lights at the leading edge. Turn off when a MacWindowSidebar already shows them. | boolean | true |
<MacWindowSidebar>
| Prop | Type | Default |
|---|---|---|
controlsDraw the traffic lights at the top of the sidebar. | boolean | true |
<MacWindowSidebarItem>
| Prop | Type | Default |
|---|---|---|
selectedMarks the selected row (sets aria-current). | boolean | false |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Tab | Reaches the traffic lights only when onClose, onMinimize or onZoom handlers are set, then toolbar and sidebar buttons |
| Enter / Space | Activates the focused traffic light or button |
Use with AI
Frame content as a Mac app window. Simple: <MacWindow><MacWindowTitleBar title="…" /><MacWindowContent>…</MacWindowContent></MacWindow>. Sidebar layout: <MacWindow><MacWindowSidebar>rows…</MacWindowSidebar><MacWindowMain><MacWindowTitleBar controls={false} title>toolbar buttons</MacWindowTitleBar><MacWindowContent/></MacWindowMain></MacWindow>. Size the window with className. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Mac Window (@ballmac/mac-window) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Product screenshots and mockups of a Mac app on a landing page
- Framing a code sample, terminal session or settings panel so it reads as desktop software
- Interactive app demos where the sidebar and toolbar actually work
Not for
- Modal dialogs in a web app (use dialog)
- Browser mockups; this draws native window chrome, not an address bar
Registry JSON: https://ui.ballmac.com/r/mac-window.json
Credits
Free to use in personal and commercial projects.
- npm
- None
- Registry
- @ballmac/i18nshadcn/utils
Pairs well with
Dock
A macOS-style dock whose icons grow with spring physics as the pointer approaches and push their neighbors apart. Labels, running dots, a launch bounce and arrow-key navigation included.
Menu Bar
A translucent macOS menu bar on Radix Menubar: a bold app menu, menus with key equivalents, submenus and checkmarks, a status area with icon buttons and a hydration-safe live clock.
Segmented Control
A macOS segmented control: mutually exclusive options with a selected pill that springs between segments and hairline dividers between unselected ones. A Radix radio group underneath.
Control Center
The macOS Control Center panel in frosted glass: toggle tiles with round badges, grouped Wi-Fi, Bluetooth and AirDrop rows, pill sliders with the icon in the track, and a Now Playing tile. Real toggle buttons and Radix sliders.