← ui

app-window

ui

a Mac app's window for any content: the window buttons, a title, a body

npx shadcn add @ag/app-window
Already installed? Preview an update
npx shadcn add @ag/app-window --dry-run

Your installed source stays unchanged until you update it. How updates work

Latest changes · 2026-11-05
  • New: a Mac app's window around any React, <AppWindow title="money.cell">{…}</AppWindow>: the three window buttons, a title centred in the bar, and a body that clips what it holds (a table scrolls inside it). active={false} greys the buttons and the title as a window behind another; tone draws the buttons in one colour of the app's own; scheme is the scheme it stands in. Its colours are properties set per scheme (--ag-app-window-ground, -ink, -bar, -shadow), replaced by a plain bg-* or shadow-* in className or on style. TrafficLights is the buttons alone.
Updated 2026-11-05 · release notes

also called mac window, window chrome, traffic lights

shelf devices & demos

uses material

used by ide, macos, browser

the demo above, verbatim

app-window.demo.tsx
"use client"

import type * as React from "react"
import { Sample } from "@/app/samples"
import { AppWindow } from "@/registry/base-nova/ui/app-window"
import { Table, type TableColumn } from "@/registry/base-nova/ui/table"

// #region trip
// What the window holds is any React: here ui's table, a file of a trip's costs.
interface Cost {
  id: string
  item: string
  category: string
  cost: string
}
const COSTS: Cost[] = [
  { id: "1", item: "fuel", category: "travel", cost: "84.20" },
  { id: "2", item: "cabin", category: "stay", cost: "310.00" },
  { id: "3", item: "groceries", category: "food", cost: "96.75" },
  { id: "4", item: "ferry", category: "travel", cost: "58.00" },
]
const COLUMNS: TableColumn<Cost>[] = [
  { id: "item", label: "item", text: (r) => r.item },
  { id: "category", label: "category", text: (r) => r.category },
  { id: "cost", label: "cost", text: (r) => r.cost },
]
// #endregion

export default function AppWindowDemo() {
  return (
    <div className="space-y-16">
      <Sample
        name="window"
        glance
        title="a window for anything"
        description="the buttons, a title, and a body holding what it is given"
      >
        <AppWindow title="notes" className="h-44 w-72">
          <ul className="space-y-1 p-4 text-sm">
            <li>call the cabin about the keys</li>
            <li>ferry tickets, both ways</li>
            <li>sd cards</li>
          </ul>
        </AppWindow>
      </Sample>

      <Sample
        name="table"
        title="a table in a real window"
        description="the body clips, so the table scrolls inside it"
        with="trip"
      >
        <AppWindow title="trip.cell" className="mx-auto h-64 max-w-xl">
          <Table
            label="trip"
            rows={COSTS}
            rowId={(r) => r.id}
            columns={COLUMNS}
            className="h-full"
          />
        </AppWindow>
      </Sample>

      <Sample
        name="behind"
        title="the key window and one behind"
        description="behind another, the buttons and the title go grey, as macOS draws them"
      >
        <div className="relative mx-auto h-64 max-w-xl">
          <AppWindow
            title="accounts.cell"
            active={false}
            className="absolute top-0 left-0 h-48 w-80"
          >
            <p className="p-4 text-sm opacity-60">assets:bank, expenses:home</p>
          </AppWindow>
          <AppWindow
            title="money.cell"
            className="absolute right-0 bottom-0 h-48 w-80"
          >
            <p className="p-4 text-sm">rent, coffee</p>
          </AppWindow>
        </div>
      </Sample>

      <Sample
        name="schemes"
        title="light and dark"
        description="the scheme it stands in, whatever the page is"
      >
        <div className="grid gap-8 sm:grid-cols-2">
          <AppWindow title="light" scheme="light" className="h-40">
            <p className="p-4 text-sm">a window in the light scheme</p>
          </AppWindow>
          <AppWindow title="dark" scheme="dark" className="h-40">
            <p className="p-4 text-sm">a window in the dark scheme</p>
          </AppWindow>
        </div>
      </Sample>

      <Sample
        name="tone"
        title="an app's own palette"
        description="the buttons in one tone and the colours set as properties, as macos draws its terminals"
      >
        <AppWindow
          tone="#3a3a30"
          scheme="dark"
          className="mx-auto h-40 max-w-md font-mono"
          style={
            {
              "--ag-app-window-ground": "#0d0f0a",
              "--ag-app-window-bar": "#17190f",
              "--ag-app-window-ink": "#e7a13c",
            } as React.CSSProperties
          }
        >
          <p className="p-4 text-sm">$ cells check money.cell</p>
        </AppWindow>
      </Sample>
    </div>
  )
}

the source, every file an install writes

app-window.tsx
"use client"

// A MAC APP'S WINDOW for anything: the three window buttons, a title centred in the
// bar, and a body that holds whatever React it is given (a table, a form, a chart),
// so it reads as a real app on a page or on a `macos` screen:
//
//   <AppWindow title="money.cell"><Table … /></AppWindow>
//
// It is the one drawing of a Mac window's chrome here: `browser` and `ide` draw their
// buttons with `TrafficLights`, `macos` its terminal windows with `AppWindow`. The
// window takes its size from its `className` (or its place in a layout) and its body
// the rest of it; the body clips, so what it holds scrolls inside it. Its colours are
// properties set per scheme (`--ag-app-window-ground`, `-ink`, `-bar`, `-shadow`): a
// consumer replaces one with a plain utility in `className` (`bg-…`, `shadow-…`) or
// sets the property on `style`, the bar's included. `active` is macOS's key window:
// the buttons in their colours and the title whole; behind another, both grey.

import type * as React from "react"
import { cn } from "@/lib/utils"
import { type Scheme, useSurfaceScheme } from "@/registry/base-nova/ui/material"
import { schemeOf } from "@/registry/base-nova/ui/material-glass"

/** The window buttons' own colours: close, minimise, zoom. */
const LIGHTS = ["#ff5f57", "#febc2e", "#28c840"] as const

export interface TrafficLightsProps {
  /** macOS's key window: the buttons in their own colours. Off: the grey of a
   *  window behind another. */
  active?: boolean
  /** One colour for all three, for a window that draws its chrome in its own
   *  palette (a terminal's phosphor); it wins over `active`. */
  tone?: string
  className?: string
}

/** The three window buttons at the head of a Mac window's bar. */
export function TrafficLights({
  active = true,
  tone,
  className,
}: TrafficLightsProps) {
  return (
    <span
      data-slot="app-window-lights"
      aria-hidden
      className={cn("flex shrink-0 gap-2", className)}
    >
      {LIGHTS.map((c) => (
        <span
          key={c}
          className="size-3 rounded-full shadow-[inset_0_0_0_0.5px_rgb(0_0_0/0.18)]"
          style={{
            background:
              tone ??
              (active
                ? c
                : "color-mix(in oklab, currentColor 22%, transparent)"),
          }}
        />
      ))}
    </span>
  )
}

export interface AppWindowProps
  extends Omit<React.ComponentProps<"div">, "title"> {
  /** The window's title, centred in the bar: the document's name, the app's. */
  title?: React.ReactNode
  /** macOS's key window (default): the buttons in colour and the title whole. */
  active?: boolean
  /** The buttons in one colour (`TrafficLights`' `tone`). */
  tone?: string
  /** The colour scheme the window stands in: the page's, or its own. */
  scheme?: Scheme
}

/** A Mac app's window: its bar (the buttons, the title) and its body, `children`. */
export function AppWindow({
  title,
  active = true,
  tone,
  scheme,
  className,
  children,
  ref,
  ...props
}: AppWindowProps) {
  const [worn, root] = useSurfaceScheme<HTMLDivElement>(scheme, ref)
  return (
    <div
      {...props}
      ref={root}
      data-slot="app-window"
      data-active={active || undefined}
      className={cn(
        "relative isolate flex flex-col overflow-hidden rounded-[16px]",
        // Each colour drawn from its property, set per scheme: a consumer's plain
        // `bg-*`, `text-*` or `shadow-*` replaces the utility whatever the scheme,
        // where a `scheme-dark:` utility would outrank it.
        "bg-(--ag-app-window-ground) text-(color:--ag-app-window-ink) shadow-(--ag-app-window-shadow)",
        "[--ag-app-window-bar:#f3f3f3] [--ag-app-window-ground:#ffffff] [--ag-app-window-ink:#1d1d1f] [--ag-app-window-shadow:0_0_0_0.5px_rgb(0_0_0/0.22),0_24px_60px_-16px_rgb(0_0_0/0.45)]",
        "scheme-dark:[--ag-app-window-bar:#2b2b2d] scheme-dark:[--ag-app-window-ground:#1e1e1e] scheme-dark:[--ag-app-window-ink:#e5e5e7] scheme-dark:[--ag-app-window-shadow:0_0_0_0.5px_rgb(255_255_255/0.16),0_24px_60px_-16px_rgb(0_0_0/0.6)]",
        schemeOf(worn),
        className,
      )}
    >
      <div
        data-slot="app-window-bar"
        className="relative flex h-7 shrink-0 items-center bg-(--ag-app-window-bar) px-3"
      >
        <TrafficLights active={active} tone={tone} />
        {title !== undefined && (
          <span
            data-slot="app-window-title"
            className={cn(
              "pointer-events-none absolute inset-x-20 truncate text-center font-sans font-semibold text-[13px]",
              active ? "opacity-85" : "opacity-45",
            )}
          >
            {title}
          </span>
        )}
      </div>
      <div
        data-slot="app-window-body"
        className="relative min-h-0 flex-1 overflow-hidden"
      >
        {children}
      </div>
    </div>
  )
}