macbetter

Navigation

Window

Frame a Mac app in a window with a title bar, traffic lights, sidebar and toolbar.

macOS only
Brand
Q3 Planning
Receipts
Launch Deck.key
Onboarding.pdf
Roadmap.pages
Team Offsite.jpg
Metrics.numbers
"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.

macOS only
About This Mac
MacBook Pro
14-inch, 2025 · Apple M5
Notes

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>
  )
}

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.

macOS only
Brand
Q3 Planning
Receipts
Launch Deck.key
Onboarding.pdf
Roadmap.pages
Team Offsite.jpg
Metrics.numbers
"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.

macOS only
Calculator
1,280
"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

  • Window is a group. Name it with aria-label, usually the app's or the document's name.
  • Without handlers, the traffic lights are decorative and hidden from assistive technology. Pass onClose, onMinimize or onZoom and 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

PropTypeDefaultDescription
inactivebooleanfalseDraws the window behind the key window: gray traffic lights, dimmed title, gray sidebar selection.
aria-labelstring—Names the window.

WindowTitlebar

PropTypeDefaultDescription
variant"unified" | "compact""unified"Unified is 52 points and holds a toolbar. Compact is 28 points and holds a title.
borderedbooleantrueDraws the hairline between the title bar and the content.

WindowTitle

PropTypeDefaultDescription
align"leading" | "center""leading"Center for title-only windows, leading beside a toolbar.
subtitleReactNode—A second, smaller line, like an item count.

WindowTrafficLights

PropTypeDefaultDescription
onCloseMouseEventHandler—Makes the red light a Close button.
onMinimizeMouseEventHandler—Makes the yellow light a Minimize button.
onZoomMouseEventHandler—Makes the green light a Zoom button.

WindowSidebar and WindowBody take the props of a div.

Data attributes and CSS variables

NameOnDescription
data-inactive.mb-windowThe window is behind the key window.
data-variant.mb-window-titlebarunified or compact.
data-light.mb-window-traffic-lightclose, minimize or zoom.
--mb-window-radius.mb-windowCorner radius. 16 points.
--mb-window-sidebar-width.mb-windowWidth of the inset sidebar. 220 points.
--mb-window-titlebar-height.mb-windowHeight of the unified title bar.
--mb-window-sidebar-inset.mb-windowThe gap between the sidebar's glass and the window's edges. 6 points.