A macOS sheet: a frosted panel that drops from the top edge of its window and dims only that window, with a default accent button, on Radix Dialog for focus trapping and Escape to cancel.
"use client"
import * as React from "react"
import { KeyRound } from "lucide-react"
import { MacWindow, MacWindowContent, MacWindowMain, MacWindowTitleBar } from "@/components/ballmac/mac-window"
import {
SheetDialog,
SheetDialogButton,
SheetDialogContent,
SheetDialogDescription,
SheetDialogFooter,
SheetDialogHeader,
SheetDialogTitle,
SheetDialogTrigger,
} from "@/components/ballmac/sheet-dialog"
import { AppIcon } from "@/components/ballmac/mac-icons"
export default function SheetDialogDemo() {
const ref = React.useRef<HTMLDivElement>(null)
const [saved, setSaved] = React.useState(false)
const [open, setOpen] = React.useState(false)
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-1 via-chart-4 to-chart-5" />
<div className="absolute -inset-x-1/4 top-1/3 h-full rounded-[50%] bg-white/20 blur-2xl" />
</div>
<MacWindow ref={ref} className="relative h-[300px] w-full overflow-hidden">
<MacWindowMain>
<MacWindowTitleBar title="Keychain Access" />
<MacWindowContent className="flex flex-col items-start gap-3 p-6">
<p className="text-sm text-muted-foreground">{saved ? "Password saved to your keychain." : "Nothing saved yet."}</p>
<SheetDialog container={ref} open={open} onOpenChange={setOpen}>
<SheetDialogTrigger className="inline-flex h-[22px] items-center rounded-[6px] border border-foreground/15 bg-background px-3 text-[13px] shadow-xs outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50">
Save Password…
</SheetDialogTrigger>
<SheetDialogContent>
<SheetDialogHeader>
<AppIcon size={48} tone="graphite"><KeyRound /></AppIcon>
<div>
<SheetDialogTitle>Save this password?</SheetDialogTitle>
<SheetDialogDescription>It will be stored in your keychain and filled in on this Mac and your other devices.</SheetDialogDescription>
</div>
</SheetDialogHeader>
<SheetDialogFooter>
<SheetDialogButton onClick={() => setOpen(false)}>Not Now</SheetDialogButton>
<SheetDialogButton
primary
onClick={() => {
setSaved(true)
setOpen(false)
}}
>
Save
</SheetDialogButton>
</SheetDialogFooter>
</SheetDialogContent>
</SheetDialog>
</MacWindowContent>
</MacWindowMain>
</MacWindow>
</div>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/sheet-dialogInstall the dependencies.
$ pnpm add motion@^12 radix-uiCopy the source into your project.
components/ballmac/sheet-dialog.tsx// Ballmac UI: Sheet Dialog. https://ui.ballmac.com/components/sheet-dialog "use client" import * as React from "react" import { AnimatePresence, motion, useReducedMotion } from "motion/react" import { Dialog as DialogPrimitive } from "radix-ui" import { cn } from "@/lib/utils" type Container = HTMLElement | null | React.RefObject<HTMLElement | null> type SheetContextValue = { open: boolean; container?: Container } const SheetContext = React.createContext<SheetContextValue>({ open: false }) type SheetDialogProps = { children: React.ReactNode /** Controlled open state. */ open?: boolean /** Initial open state when uncontrolled. */ defaultOpen?: boolean /** Called when the sheet opens or closes. */ onOpenChange?: (open: boolean) => void /** * The window the sheet attaches to: an element or a ref to one (it should be `relative` and `overflow-hidden`). * Without it the sheet attaches to the whole page. */ container?: Container } /** A macOS sheet: a panel that drops from the top of its window and dims only that window. */ function SheetDialog({ children, open: openProp, defaultOpen = false, onOpenChange, container }: SheetDialogProps) { const [openState, setOpenState] = React.useState(defaultOpen) const open = openProp ?? openState const value = React.useMemo(() => ({ open, container }), [open, container]) return ( <SheetContext.Provider value={value}> <DialogPrimitive.Root open={open} onOpenChange={(next) => { setOpenState(next) onOpenChange?.(next) }} > {children} </DialogPrimitive.Root> </SheetContext.Provider> ) } function SheetDialogTrigger({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) { return <DialogPrimitive.Trigger data-slot="sheet-dialog-trigger" className={className} {...props} /> } function SheetDialogClose({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) { return <DialogPrimitive.Close data-slot="sheet-dialog-close" className={className} {...props} /> } type SheetDialogContentProps = Omit<React.ComponentProps<"div">, "ref" | "title"> function SheetDialogContent({ className, children, ...props }: SheetDialogContentProps) { const { open, container } = React.useContext(SheetContext) const reduce = useReducedMotion() const host = container && "current" in container ? container.current : container return ( <DialogPrimitive.Portal forceMount container={host ?? undefined}> <AnimatePresence> {open && ( <> {React.createElement( DialogPrimitive.Overlay, { forceMount: true, asChild: true }, <motion.div data-slot="sheet-dialog-overlay" className={cn("z-50 bg-black/25 dark:bg-black/45", host ? "absolute inset-0" : "fixed inset-0")} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: reduce ? 0 : 0.2 }} /> )} {React.createElement( DialogPrimitive.Content, { forceMount: true, asChild: true }, <motion.div data-slot="sheet-dialog-content" className={cn( "z-50 flex w-[min(28rem,calc(100%-2rem))] flex-col gap-4 rounded-b-[12px] border border-t-0 border-foreground/10 bg-popover/90 p-5 text-popover-foreground shadow-[0_18px_50px_-10px_rgb(0_0_0/0.45)] backdrop-blur-2xl backdrop-saturate-150 outline-none [--mac-accent:oklch(0.53_0.2_258)]", host ? "absolute top-0 left-1/2" : "fixed top-0 left-1/2", className )} style={{ x: "-50%" }} initial={reduce ? { opacity: 0 } : { y: "-102%" }} animate={reduce ? { opacity: 1 } : { y: 0 }} exit={reduce ? { opacity: 0 } : { y: "-102%" }} transition={reduce ? { duration: 0 } : { type: "spring", stiffness: 420, damping: 38, mass: 0.9 }} {...(props as object)} > {children} </motion.div> )} </> )} </AnimatePresence> </DialogPrimitive.Portal> ) } function SheetDialogHeader({ className, ...props }: React.ComponentProps<"div">) { return <div data-slot="sheet-dialog-header" className={cn("flex items-start gap-4", className)} {...props} /> } function SheetDialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) { return <DialogPrimitive.Title data-slot="sheet-dialog-title" className={cn("text-[15px] leading-5 font-semibold", className)} {...props} /> } function SheetDialogDescription({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Description>) { return <DialogPrimitive.Description data-slot="sheet-dialog-description" className={cn("mt-1 text-[13px] leading-5 text-muted-foreground", className)} {...props} /> } function SheetDialogFooter({ className, ...props }: React.ComponentProps<"div">) { return <div data-slot="sheet-dialog-footer" className={cn("flex items-center justify-end gap-2", className)} {...props} /> } type SheetDialogButtonProps = React.ComponentProps<"button"> & { /** The default button: filled with the accent color, and the one Return activates inside a form. */ primary?: boolean /** Destructive default buttons use red. */ destructive?: boolean } /** A macOS push button for sheets. */ function SheetDialogButton({ primary = false, destructive = false, className, type = "button", ...props }: SheetDialogButtonProps) { return ( <button type={type} data-slot="sheet-dialog-button" className={cn( "inline-flex h-[22px] min-w-[4.25rem] items-center justify-center rounded-[6px] px-3 text-[13px] outline-none transition-[background-color,box-shadow] focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-50", primary ? cn("text-white shadow-[0_0.5px_1px_rgb(0_0_0/0.3),inset_0_0.5px_0_rgb(255_255_255/0.3)] hover:brightness-110", destructive ? "bg-destructive" : "bg-(--mac-accent)") : "border border-foreground/10 bg-background/80 shadow-[0_0.5px_1px_rgb(0_0_0/0.15)] hover:bg-background dark:bg-foreground/[0.12] dark:hover:bg-foreground/[0.18]", className )} {...props} /> ) } export { SheetDialog, SheetDialogTrigger, SheetDialogContent, SheetDialogHeader, SheetDialogTitle, SheetDialogDescription, SheetDialogFooter, SheetDialogClose, SheetDialogButton, type SheetDialogProps, type SheetDialogContentProps, type SheetDialogButtonProps, }Update the import paths to match your project setup.
Usage
import { SheetDialog, SheetDialogTrigger, SheetDialogContent, SheetDialogHeader, SheetDialogTitle, SheetDialogDescription, SheetDialogFooter, SheetDialogClose, SheetDialogButton } from "@/components/ballmac/sheet-dialog"The full example is in the Code tab above.
Examples
Form sheet in a window
"use client"
import * as React from "react"
import { MacWindow, MacWindowContent, MacWindowMain, MacWindowTitleBar } from "@/components/ballmac/mac-window"
import {
SheetDialog,
SheetDialogButton,
SheetDialogContent,
SheetDialogDescription,
SheetDialogFooter,
SheetDialogTitle,
SheetDialogTrigger,
} from "@/components/ballmac/sheet-dialog"
export default function SheetDialogForm() {
const ref = React.useRef<HTMLDivElement>(null)
const [open, setOpen] = React.useState(false)
const [name, setName] = React.useState("Untitled")
const [saved, setSaved] = React.useState<string | null>(null)
return (
<div className="w-full max-w-[560px]">
<MacWindow ref={ref} className="relative h-[280px] w-full overflow-hidden">
<MacWindowMain>
<MacWindowTitleBar title={saved ?? "Untitled"} />
<MacWindowContent className="flex flex-col items-start gap-3 p-6">
<SheetDialog container={ref} open={open} onOpenChange={setOpen}>
<SheetDialogTrigger className="inline-flex h-[22px] items-center rounded-[6px] border border-foreground/15 bg-background px-3 text-[13px] shadow-xs outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50">
Save As…
</SheetDialogTrigger>
<SheetDialogContent>
<form
className="flex flex-col gap-4"
onSubmit={(e) => {
e.preventDefault()
setSaved(name.trim() || "Untitled")
setOpen(false)
}}
>
<div>
<SheetDialogTitle>Save As</SheetDialogTitle>
<SheetDialogDescription>Choose a name for the document.</SheetDialogDescription>
</div>
<label className="flex items-center gap-3 text-[13px]">
<span className="w-14 text-end text-muted-foreground">Name:</span>
<input
value={name}
onChange={(e) => setName(e.target.value)}
className="h-6 min-w-0 flex-1 rounded-[5px] border border-foreground/15 bg-background px-2 text-[13px] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
/>
</label>
<SheetDialogFooter>
<SheetDialogButton onClick={() => setOpen(false)}>Cancel</SheetDialogButton>
<SheetDialogButton type="submit" primary>Save</SheetDialogButton>
</SheetDialogFooter>
</form>
</SheetDialogContent>
</SheetDialog>
<p className="text-sm text-muted-foreground">{saved ? `Saved as “${saved}”.` : "Not saved yet."}</p>
</MacWindowContent>
</MacWindowMain>
</MacWindow>
</div>
)
}API reference
<SheetDialog>
| Prop | Type | Default |
|---|---|---|
children* | React.ReactNode | — |
openControlled open state. | boolean | — |
defaultOpenInitial open state when uncontrolled. | boolean | false |
onOpenChangeCalled when the sheet opens or closes. | (open: boolean) => void | — |
containerThe window the sheet attaches to: an element or a ref to one (it should be `relative` and `overflow-hidden`). Without it the sheet attaches to the whole page. | Container | — |
<SheetDialogButton>
| Prop | Type | Default |
|---|---|---|
primaryThe default button: filled with the accent color, and the one Return activates inside a form. | boolean | false |
destructiveDestructive default buttons use red. | boolean | false |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Escape | Cancels |
| Focus | Trapped in the sheet and returned to the trigger on close |
| Screen readers | Named dialog with description |
| Reduced motion | The sheet fades in place |
Use with AI
<SheetDialog container={windowRef}> with SheetDialogTrigger and SheetDialogContent (Header, Title, Description, Footer, Button primary). Give it the window element so it attaches there; without it it drops from the page. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Sheet Dialog (@ballmac/sheet-dialog) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Confirmations and short forms attached to a window
- Mac-style apps and demos
Not for
- General modals (dialog)
- Side panels (sheet)
Registry JSON: https://ui.ballmac.com/r/sheet-dialog.json
Credits
Free to use in personal and commercial projects.
- Registry
- shadcn/utils
Pairs well with
Mac Window
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.
Dialog
A Radix modal dialog with a blurred overlay, centered card surface that fits 360 px screens, header and footer parts, optional close button and animations.
Window Manager
A desktop of real windows: focus raises, the title bar drags, edges and corners resize, double-click zooms, the yellow light tucks a window into a tray, and Alt plus arrow keys move and resize from the keyboard.
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.