Navigation
Window
Frame a Mac app in a window with a title bar, traffic lights, sidebar and toolbar.
"use client"
import { Sidebar, SidebarItem, SidebarSection } from "macbetter/sidebar"
import {
Toolbar,
ToolbarButton,
ToolbarGroup,
ToolbarSearchField,
ToolbarSpacer,
ToolbarTitle,
ToolbarToggle,
ToolbarToggleGroup,
} from "macbetter/toolbar"
import { Window, WindowBody, WindowSidebar, WindowTitlebar, WindowTrafficLights } from "macbetter/window"
import {
AppWindowIcon,
ChevronLeftIcon,
ChevronRightIcon,
CircleIcon,
ClockIcon,
CloudIcon,
Columns3Icon,
DownloadIcon,
FileTextIcon,
FolderIcon,
HardDriveIcon,
ImageIcon,
LayoutGridIcon,
ListIcon,
MonitorIcon,
PresentationIcon,
ShareIcon,
UsersIcon,
} from "lucide-react"
import { useState } from "react"
const favorites = [
{ name: "Recents", icon: ClockIcon },
{ name: "Applications", icon: AppWindowIcon },
{ name: "Desktop", icon: MonitorIcon },
{ name: "Documents", icon: FileTextIcon },
{ name: "Downloads", icon: DownloadIcon, badge: 3 },
]
const tags = [
{ name: "Launch", color: "var(--mb-orange)" },
{ name: "Research", color: "var(--mb-green)" },
{ name: "Finance", color: "var(--mb-blue)" },
]
const files = [
{ name: "Brand", kind: "folder" },
{ name: "Q3 Planning", kind: "folder" },
{ name: "Receipts", kind: "folder" },
{ name: "Launch Deck.key", kind: "deck" },
{ name: "Onboarding.pdf", kind: "doc" },
{ name: "Roadmap.pages", kind: "doc" },
{ name: "Team Offsite.jpg", kind: "image" },
{ name: "Metrics.numbers", kind: "doc" },
]
const fileIcons = { folder: FolderIcon, deck: PresentationIcon, doc: FileTextIcon, image: ImageIcon }
export default function Example() {
const [place, setPlace] = useState("Documents")
const [view, setView] = useState<string[]>(["icons"])
return (
<Window aria-label="Finder" style={{ maxWidth: 860, height: 480 }}>
<WindowSidebar>
<WindowTrafficLights />
<Sidebar aria-label="Locations">
<SidebarSection title="Favorites">
{favorites.map(({ name, icon: Icon, badge }) => (
<SidebarItem
key={name}
icon={<Icon />}
badge={badge}
current={place === name}
onClick={() => setPlace(name)}
>
{name}
</SidebarItem>
))}
</SidebarSection>
<SidebarSection title="iCloud" collapsible>
<SidebarItem icon={<CloudIcon />} current={place === "iCloud Drive"} onClick={() => setPlace("iCloud Drive")}>
iCloud Drive
</SidebarItem>
<SidebarItem icon={<UsersIcon />} current={place === "Shared"} onClick={() => setPlace("Shared")}>
Shared
</SidebarItem>
</SidebarSection>
<SidebarSection title="Locations" collapsible>
<SidebarItem icon={<HardDriveIcon />} current={place === "Macintosh HD"} onClick={() => setPlace("Macintosh HD")}>
Macintosh HD
</SidebarItem>
</SidebarSection>
<SidebarSection title="Tags" collapsible>
{tags.map((tag) => (
<SidebarItem
key={tag.name}
tint={tag.color}
icon={<CircleIcon fill="currentColor" strokeWidth={0} />}
current={place === tag.name}
onClick={() => setPlace(tag.name)}
>
{tag.name}
</SidebarItem>
))}
</SidebarSection>
</Sidebar>
</WindowSidebar>
<WindowTitlebar>
<Toolbar aria-label="Finder">
<ToolbarGroup aria-label="History">
<ToolbarButton aria-label="Back">
<ChevronLeftIcon aria-hidden="true" data-mirror="" />
</ToolbarButton>
<ToolbarButton aria-label="Forward" disabled>
<ChevronRightIcon aria-hidden="true" data-mirror="" />
</ToolbarButton>
</ToolbarGroup>
<ToolbarTitle>{place}</ToolbarTitle>
<ToolbarSpacer />
<ToolbarToggleGroup aria-label="View" value={view} onValueChange={(next) => next.length && setView(next)}>
<ToolbarToggle value="icons" aria-label="Icons">
<LayoutGridIcon aria-hidden="true" />
</ToolbarToggle>
<ToolbarToggle value="list" aria-label="List">
<ListIcon aria-hidden="true" />
</ToolbarToggle>
<ToolbarToggle value="columns" aria-label="Columns">
<Columns3Icon aria-hidden="true" />
</ToolbarToggle>
</ToolbarToggleGroup>
<ToolbarButton aria-label="Share">
<ShareIcon aria-hidden="true" />
</ToolbarButton>
<ToolbarSearchField aria-label="Search" />
</Toolbar>
</WindowTitlebar>
<WindowBody>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(104px, 1fr))",
gap: "18px 8px",
padding: 20,
}}
>
{files.map((file) => {
const Icon = fileIcons[file.kind as keyof typeof fileIcons]
const folder = file.kind === "folder"
return (
<div key={file.name} style={{ display: "grid", justifyItems: "center", gap: 6, textAlign: "center" }}>
<Icon
aria-hidden="true"
width={56}
height={56}
strokeWidth={1.1}
fill={folder ? "color-mix(in oklch, var(--mb-cyan) 55%, transparent)" : "var(--mb-bg-elevated)"}
color={folder ? "var(--mb-cyan)" : "var(--mb-label-secondary)"}
/>
<span className="mb-text-callout">{file.name}</span>
</div>
)
})}
</div>
</WindowBody>
</Window>
)
}When to use it
Use a window to present a Mac app inside a web page: a product tour, a marketing screenshot that people can use, or an app that runs in the browser and should feel like it belongs on the desktop. The title bar, traffic lights and toolbar tell people at a glance what kind of app it is.
To present an iPhone or iPad layout, use a device frame.
A window isn't window management. A window doesn't move, resize or close by itself: wire the traffic lights to your own handlers if they should do something.
Usage
import {
Window,
WindowBody,
WindowSidebar,
WindowTitle,
WindowTitlebar,
WindowTrafficLights,
} from "macbetter/window"Title-only windows
A compact title bar holds the traffic lights and a centered title, like a utility window or About This Mac. Set inactive to draw a window behind the key window: its traffic lights turn gray and its title dims.
Behind the key window, traffic lights turn gray and the title dims.
import { Window, WindowBody, WindowTitle, WindowTitlebar, WindowTrafficLights } from "macbetter/window"
export default function Example() {
return (
<div style={{ display: "flex", flexWrap: "wrap", gap: 32, justifyContent: "center", width: "100%" }}>
<Window aria-label="About This Mac" style={{ width: 300, height: 200 }}>
<WindowTitlebar variant="compact" bordered={false}>
<WindowTrafficLights />
<WindowTitle align="center">About This Mac</WindowTitle>
</WindowTitlebar>
<WindowBody style={{ display: "grid", placeItems: "center", textAlign: "center", padding: 16 }}>
<div>
<div className="mb-text-title-2">MacBook Pro</div>
<div className="mb-text-subheadline mb-label-secondary">14-inch, 2025 · Apple M5</div>
</div>
</WindowBody>
</Window>
<Window inactive aria-label="Notes" style={{ width: 300, height: 200 }}>
<WindowTitlebar variant="compact" bordered={false}>
<WindowTrafficLights />
<WindowTitle align="center">Notes</WindowTitle>
</WindowTitlebar>
<WindowBody style={{ padding: 16 }}>
<p className="mb-text-body mb-label-secondary" style={{ margin: 0 }}>
Behind the key window, traffic lights turn gray and the title dims.
</p>
</WindowBody>
</Window>
</div>
)
}Sidebar and toolbar
Put a WindowSidebar first and it becomes a pane of glass inset from the window's edges, spanning the full height. Place the traffic lights at its top. The title bar and body take the column beside it, and a Toolbar in the title bar becomes the unified toolbar. A Sidebar inside drops its own material, so glass never sits on glass.
When the window gets narrow, the toolbar's search field folds into a magnifying glass before anything else gives way, and below 540 points the sidebar hides, as a Mac window does when you drag it small.
"use client"
import { Sidebar, SidebarItem, SidebarSection } from "macbetter/sidebar"
import {
Toolbar,
ToolbarButton,
ToolbarGroup,
ToolbarSearchField,
ToolbarSpacer,
ToolbarTitle,
ToolbarToggle,
ToolbarToggleGroup,
} from "macbetter/toolbar"
import { Window, WindowBody, WindowSidebar, WindowTitlebar, WindowTrafficLights } from "macbetter/window"
import {
AppWindowIcon,
ChevronLeftIcon,
ChevronRightIcon,
CircleIcon,
ClockIcon,
CloudIcon,
Columns3Icon,
DownloadIcon,
FileTextIcon,
FolderIcon,
HardDriveIcon,
ImageIcon,
LayoutGridIcon,
ListIcon,
MonitorIcon,
PresentationIcon,
ShareIcon,
UsersIcon,
} from "lucide-react"
import { useState } from "react"
const favorites = [
{ name: "Recents", icon: ClockIcon },
{ name: "Applications", icon: AppWindowIcon },
{ name: "Desktop", icon: MonitorIcon },
{ name: "Documents", icon: FileTextIcon },
{ name: "Downloads", icon: DownloadIcon, badge: 3 },
]
const tags = [
{ name: "Launch", color: "var(--mb-orange)" },
{ name: "Research", color: "var(--mb-green)" },
{ name: "Finance", color: "var(--mb-blue)" },
]
const files = [
{ name: "Brand", kind: "folder" },
{ name: "Q3 Planning", kind: "folder" },
{ name: "Receipts", kind: "folder" },
{ name: "Launch Deck.key", kind: "deck" },
{ name: "Onboarding.pdf", kind: "doc" },
{ name: "Roadmap.pages", kind: "doc" },
{ name: "Team Offsite.jpg", kind: "image" },
{ name: "Metrics.numbers", kind: "doc" },
]
const fileIcons = { folder: FolderIcon, deck: PresentationIcon, doc: FileTextIcon, image: ImageIcon }
export default function Example() {
const [place, setPlace] = useState("Documents")
const [view, setView] = useState<string[]>(["icons"])
return (
<Window aria-label="Finder" style={{ maxWidth: 860, height: 480 }}>
<WindowSidebar>
<WindowTrafficLights />
<Sidebar aria-label="Locations">
<SidebarSection title="Favorites">
{favorites.map(({ name, icon: Icon, badge }) => (
<SidebarItem
key={name}
icon={<Icon />}
badge={badge}
current={place === name}
onClick={() => setPlace(name)}
>
{name}
</SidebarItem>
))}
</SidebarSection>
<SidebarSection title="iCloud" collapsible>
<SidebarItem icon={<CloudIcon />} current={place === "iCloud Drive"} onClick={() => setPlace("iCloud Drive")}>
iCloud Drive
</SidebarItem>
<SidebarItem icon={<UsersIcon />} current={place === "Shared"} onClick={() => setPlace("Shared")}>
Shared
</SidebarItem>
</SidebarSection>
<SidebarSection title="Locations" collapsible>
<SidebarItem icon={<HardDriveIcon />} current={place === "Macintosh HD"} onClick={() => setPlace("Macintosh HD")}>
Macintosh HD
</SidebarItem>
</SidebarSection>
<SidebarSection title="Tags" collapsible>
{tags.map((tag) => (
<SidebarItem
key={tag.name}
tint={tag.color}
icon={<CircleIcon fill="currentColor" strokeWidth={0} />}
current={place === tag.name}
onClick={() => setPlace(tag.name)}
>
{tag.name}
</SidebarItem>
))}
</SidebarSection>
</Sidebar>
</WindowSidebar>
<WindowTitlebar>
<Toolbar aria-label="Finder">
<ToolbarGroup aria-label="History">
<ToolbarButton aria-label="Back">
<ChevronLeftIcon aria-hidden="true" data-mirror="" />
</ToolbarButton>
<ToolbarButton aria-label="Forward" disabled>
<ChevronRightIcon aria-hidden="true" data-mirror="" />
</ToolbarButton>
</ToolbarGroup>
<ToolbarTitle>{place}</ToolbarTitle>
<ToolbarSpacer />
<ToolbarToggleGroup aria-label="View" value={view} onValueChange={(next) => next.length && setView(next)}>
<ToolbarToggle value="icons" aria-label="Icons">
<LayoutGridIcon aria-hidden="true" />
</ToolbarToggle>
<ToolbarToggle value="list" aria-label="List">
<ListIcon aria-hidden="true" />
</ToolbarToggle>
<ToolbarToggle value="columns" aria-label="Columns">
<Columns3Icon aria-hidden="true" />
</ToolbarToggle>
</ToolbarToggleGroup>
<ToolbarButton aria-label="Share">
<ShareIcon aria-hidden="true" />
</ToolbarButton>
<ToolbarSearchField aria-label="Search" />
</Toolbar>
</WindowTitlebar>
<WindowBody>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(104px, 1fr))",
gap: "18px 8px",
padding: 20,
}}
>
{files.map((file) => {
const Icon = fileIcons[file.kind as keyof typeof fileIcons]
const folder = file.kind === "folder"
return (
<div key={file.name} style={{ display: "grid", justifyItems: "center", gap: 6, textAlign: "center" }}>
<Icon
aria-hidden="true"
width={56}
height={56}
strokeWidth={1.1}
fill={folder ? "color-mix(in oklch, var(--mb-cyan) 55%, transparent)" : "var(--mb-bg-elevated)"}
color={folder ? "var(--mb-cyan)" : "var(--mb-label-secondary)"}
/>
<span className="mb-text-callout">{file.name}</span>
</div>
)
})}
</div>
</WindowBody>
</Window>
)
}Window controls
Pass onClose, onMinimize or onZoom to WindowTrafficLights and each light becomes a button. Their glyphs appear when the pointer is over the group or a light has keyboard focus.
"use client"
import { Button } from "macbetter/button"
import { Window, WindowBody, WindowTitle, WindowTitlebar, WindowTrafficLights } from "macbetter/window"
import { useState } from "react"
export default function Example() {
const [open, setOpen] = useState(true)
const [zoomed, setZoomed] = useState(false)
if (!open) {
return <Button onClick={() => setOpen(true)}>Reopen Calculator</Button>
}
return (
<Window aria-label="Calculator" style={{ width: zoomed ? 420 : 300, height: 180 }}>
<WindowTitlebar variant="compact" bordered={false}>
<WindowTrafficLights
onClose={() => setOpen(false)}
onMinimize={() => setOpen(false)}
onZoom={() => setZoomed((value) => !value)}
/>
<WindowTitle align="center">Calculator</WindowTitle>
</WindowTitlebar>
<WindowBody style={{ display: "grid", placeItems: "end", padding: "16px 20px" }}>
<output className="mb-text-large-title" style={{ fontVariantNumeric: "tabular-nums" }}>
1,280
</output>
</WindowBody>
</Window>
)
}Platform behavior
A window is a macOS idiom, and it draws the same whatever platform the page is on. Its title bar is 52 points tall when it holds a toolbar and 28 points when it only holds a title, and it keeps Apple's red, yellow and green traffic lights in every theme, because they're system chrome rather than brand color.
In a right-to-left language the window mirrors, as macOS does: the traffic lights and the sidebar move to the right edge, with Close nearest the corner.
Accessibility
Windowis a group. Name it witharia-label, usually the app's or the document's name.- Without handlers, the traffic lights are decorative and hidden from assistive technology. Pass
onClose,onMinimizeoronZoomand each becomes a real button named Close, Minimize or Zoom, in a group named Window controls. Their glyphs show when the pointer is over the group or a light has keyboard focus.
API
Window
| Prop | Type | Default | Description |
|---|---|---|---|
inactive | boolean | false | Draws the window behind the key window: gray traffic lights, dimmed title, gray sidebar selection. |
aria-label | string | — | Names the window. |
WindowTitlebar
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "unified" | "compact" | "unified" | Unified is 52 points and holds a toolbar. Compact is 28 points and holds a title. |
bordered | boolean | true | Draws the hairline between the title bar and the content. |
WindowTitle
| Prop | Type | Default | Description |
|---|---|---|---|
align | "leading" | "center" | "leading" | Center for title-only windows, leading beside a toolbar. |
subtitle | ReactNode | — | A second, smaller line, like an item count. |
WindowTrafficLights
| Prop | Type | Default | Description |
|---|---|---|---|
onClose | MouseEventHandler | — | Makes the red light a Close button. |
onMinimize | MouseEventHandler | — | Makes the yellow light a Minimize button. |
onZoom | MouseEventHandler | — | Makes the green light a Zoom button. |
WindowSidebar and WindowBody take the props of a div.
Data attributes and CSS variables
| Name | On | Description |
|---|---|---|
data-inactive | .mb-window | The window is behind the key window. |
data-variant | .mb-window-titlebar | unified or compact. |
data-light | .mb-window-traffic-light | close, minimize or zoom. |
--mb-window-radius | .mb-window | Corner radius. 16 points. |
--mb-window-sidebar-width | .mb-window | Width of the inset sidebar. 220 points. |
--mb-window-titlebar-height | .mb-window | Height of the unified title bar. |
--mb-window-sidebar-inset | .mb-window | The gap between the sidebar's glass and the window's edges. 6 points. |