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.
"use client"
import { BatteryFull, Command, Search, SlidersHorizontal, Wifi } from "lucide-react"
import {
MenuBar,
MenuBarCheckboxItem,
MenuBarClock,
MenuBarContent,
MenuBarItem,
MenuBarMenu,
MenuBarSeparator,
MenuBarShortcut,
MenuBarStatus,
MenuBarStatusItem,
MenuBarSub,
MenuBarSubContent,
MenuBarSubTrigger,
MenuBarTrigger,
} from "@/components/ballmac/menu-bar"
export default function MenuBarDemo() {
return (
<div className="relative isolate h-[320px] w-full max-w-[640px] overflow-hidden rounded-2xl border border-foreground/10">
<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-2 via-chart-1 to-chart-4" />
<div className="absolute -inset-x-1/4 top-1/2 h-full rounded-[50%] bg-white/20 blur-2xl" />
</div>
<MenuBar>
<MenuBarMenu value="acme">
<MenuBarTrigger aria-label="Acme menu" className="px-2.5">
<Command aria-hidden="true" />
</MenuBarTrigger>
<MenuBarContent portal={false}>
<MenuBarItem>About This Mac</MenuBarItem>
<MenuBarSeparator />
<MenuBarItem>System Settings…</MenuBarItem>
<MenuBarSeparator />
<MenuBarItem>
Lock Screen <MenuBarShortcut>⌃⌘Q</MenuBarShortcut>
</MenuBarItem>
</MenuBarContent>
</MenuBarMenu>
<MenuBarMenu value="notes">
<MenuBarTrigger variant="app">Notes</MenuBarTrigger>
<MenuBarContent portal={false}>
<MenuBarItem>About Notes</MenuBarItem>
<MenuBarSeparator />
<MenuBarItem>
Settings… <MenuBarShortcut>⌘,</MenuBarShortcut>
</MenuBarItem>
<MenuBarSeparator />
<MenuBarItem>
Hide Notes <MenuBarShortcut>⌘H</MenuBarShortcut>
</MenuBarItem>
<MenuBarItem>
Quit Notes <MenuBarShortcut>⌘Q</MenuBarShortcut>
</MenuBarItem>
</MenuBarContent>
</MenuBarMenu>
<MenuBarMenu value="file">
<MenuBarTrigger>File</MenuBarTrigger>
<MenuBarContent portal={false}>
<MenuBarItem>
New Note <MenuBarShortcut>⌘N</MenuBarShortcut>
</MenuBarItem>
<MenuBarItem>
New Folder <MenuBarShortcut>⇧⌘N</MenuBarShortcut>
</MenuBarItem>
<MenuBarSeparator />
<MenuBarSub>
<MenuBarSubTrigger>Share</MenuBarSubTrigger>
<MenuBarSubContent portal={false}>
<MenuBarItem>Copy Link</MenuBarItem>
<MenuBarItem>Mail</MenuBarItem>
<MenuBarItem>Messages</MenuBarItem>
</MenuBarSubContent>
</MenuBarSub>
<MenuBarItem>
Export as PDF… <MenuBarShortcut>⌥⌘E</MenuBarShortcut>
</MenuBarItem>
<MenuBarItem>
Pin Note <MenuBarShortcut>⇧⌘P</MenuBarShortcut>
</MenuBarItem>
<MenuBarSeparator />
<MenuBarItem>
Close <MenuBarShortcut>⌘W</MenuBarShortcut>
</MenuBarItem>
<MenuBarItem disabled>
Print… <MenuBarShortcut>⌘P</MenuBarShortcut>
</MenuBarItem>
</MenuBarContent>
</MenuBarMenu>
<MenuBarMenu value="edit">
<MenuBarTrigger>Edit</MenuBarTrigger>
<MenuBarContent portal={false}>
<MenuBarItem>
Undo <MenuBarShortcut>⌘Z</MenuBarShortcut>
</MenuBarItem>
<MenuBarItem>
Redo <MenuBarShortcut>⇧⌘Z</MenuBarShortcut>
</MenuBarItem>
<MenuBarSeparator />
<MenuBarItem>
Cut <MenuBarShortcut>⌘X</MenuBarShortcut>
</MenuBarItem>
<MenuBarItem>
Copy <MenuBarShortcut>⌘C</MenuBarShortcut>
</MenuBarItem>
<MenuBarItem>
Paste <MenuBarShortcut>⌘V</MenuBarShortcut>
</MenuBarItem>
</MenuBarContent>
</MenuBarMenu>
<MenuBarMenu value="view">
<MenuBarTrigger className="max-sm:hidden">View</MenuBarTrigger>
<MenuBarContent portal={false}>
<MenuBarCheckboxItem checked>Show Folders</MenuBarCheckboxItem>
<MenuBarCheckboxItem>Show Note Count</MenuBarCheckboxItem>
<MenuBarSeparator />
<MenuBarItem inset>
Zoom In <MenuBarShortcut>⌘+</MenuBarShortcut>
</MenuBarItem>
</MenuBarContent>
</MenuBarMenu>
<MenuBarMenu value="window">
<MenuBarTrigger className="max-md:hidden">Window</MenuBarTrigger>
<MenuBarContent portal={false}>
<MenuBarItem>
Minimize <MenuBarShortcut>⌘M</MenuBarShortcut>
</MenuBarItem>
<MenuBarItem>Zoom</MenuBarItem>
</MenuBarContent>
</MenuBarMenu>
<MenuBarMenu value="help">
<MenuBarTrigger className="max-md:hidden">Help</MenuBarTrigger>
<MenuBarContent portal={false}>
<MenuBarItem>Notes Help</MenuBarItem>
</MenuBarContent>
</MenuBarMenu>
<MenuBarStatus>
<MenuBarStatusItem aria-label="Battery, 100 percent" className="max-sm:hidden">
<BatteryFull aria-hidden="true" />
</MenuBarStatusItem>
<MenuBarStatusItem aria-label="Wi-Fi, connected">
<Wifi aria-hidden="true" />
</MenuBarStatusItem>
<MenuBarStatusItem aria-label="Spotlight" className="max-sm:hidden">
<Search aria-hidden="true" />
</MenuBarStatusItem>
<MenuBarStatusItem aria-label="Control Center" className="max-sm:hidden">
<SlidersHorizontal aria-hidden="true" />
</MenuBarStatusItem>
<MenuBarClock className="max-sm:hidden" />
<MenuBarClock className="sm:hidden" format={{ hour: "numeric", minute: "2-digit" }} />
</MenuBarStatus>
</MenuBar>
</div>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/menu-barInstall the dependencies.
$ pnpm add radix-ui lucide-reactCopy the source into your project.
components/ballmac/menu-bar.tsx// Ballmac UI: Menu Bar. https://ui.ballmac.com/components/menu-bar "use client" import * as React from "react" import { CheckIcon, ChevronRightIcon } from "lucide-react" import { Menubar as MenubarPrimitive } from "radix-ui" import { cn } from "@/lib/utils" type MenuBarProps = React.ComponentProps<typeof MenubarPrimitive.Root> /** * A macOS menu bar built on Radix Menubar: arrow keys move between menus and items, typeahead, * Escape closes. Put MenuBarMenu children first and a MenuBarStatus area last. The status area is * rendered beside the ARIA menubar (not inside it), since a menubar may only contain menu items. */ function MenuBar({ className, children, style, ...props }: MenuBarProps) { const all = React.Children.toArray(children) const isStatus = (c: React.ReactNode) => React.isValidElement(c) && c.type === MenuBarStatus return ( <div data-slot="menu-bar" style={style} className={cn( "flex h-7 w-full min-w-0 items-center px-2 text-[13px] text-foreground select-none", "border-b border-white/20 bg-background/55 shadow-[0_0.5px_0_0_rgb(0_0_0/0.08)] backdrop-blur-2xl backdrop-saturate-150 dark:border-white/[0.06] dark:bg-background/45", className )} > <MenubarPrimitive.Root data-slot="menu-bar-menus" className="flex min-w-0 items-center gap-0.5" {...props}> {all.filter((c) => !isStatus(c))} </MenubarPrimitive.Root> {all.filter(isStatus)} </div> ) } function MenuBarMenu(props: React.ComponentProps<typeof MenubarPrimitive.Menu>) { return <MenubarPrimitive.Menu data-slot="menu-bar-menu" {...props} /> } function MenuBarGroup(props: React.ComponentProps<typeof MenubarPrimitive.Group>) { return <MenubarPrimitive.Group data-slot="menu-bar-group" {...props} /> } function MenuBarSub(props: React.ComponentProps<typeof MenubarPrimitive.Sub>) { return <MenubarPrimitive.Sub data-slot="menu-bar-sub" {...props} /> } type MenuBarTriggerProps = React.ComponentProps<typeof MenubarPrimitive.Trigger> & { /** "app" sets the trigger in bold, like the application menu next to the logo. */ variant?: "default" | "app" } function MenuBarTrigger({ variant = "default", className, ...props }: MenuBarTriggerProps) { return ( <MenubarPrimitive.Trigger data-slot="menu-bar-trigger" className={cn( "flex h-[22px] shrink-0 items-center gap-1.5 rounded-[5px] px-2 outline-none transition-colors duration-75", "hover:bg-foreground/[0.08] focus-visible:bg-foreground/[0.1] data-[state=open]:bg-foreground/[0.12] [&_svg]:size-3.5 [&_svg]:shrink-0", variant === "app" ? "font-bold" : "font-normal", className )} {...props} /> ) } const menuSurface = "z-50 min-w-[14rem] overflow-hidden rounded-[10px] border border-foreground/10 bg-popover/80 p-[5px] text-[13px] text-popover-foreground shadow-[0_0_0_0.5px_rgb(0_0_0/0.12),0_12px_32px_-8px_rgb(0_0_0/0.35)] backdrop-blur-2xl backdrop-saturate-150 outline-none dark:bg-popover/75 dark:shadow-[0_0_0_0.5px_rgb(0_0_0/0.8),0_12px_32px_-8px_rgb(0_0_0/0.7)] data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:duration-150 motion-reduce:animate-none" type MenuBarContentProps = React.ComponentProps<typeof MenubarPrimitive.Content> & { /** Render into a portal on document.body (default). Set false to keep the menu inside its container, e.g. in a mockup. */ portal?: boolean } function MenuBarContent({ portal = true, className, align = "start", alignOffset = -4, sideOffset = 4, ...props }: MenuBarContentProps) { const content = ( <MenubarPrimitive.Content data-slot="menu-bar-content" align={align} alignOffset={alignOffset} sideOffset={sideOffset} className={cn(menuSurface, "origin-(--radix-menubar-content-transform-origin)", className)} {...props} /> ) return portal ? <MenubarPrimitive.Portal>{content}</MenubarPrimitive.Portal> : content } const itemClasses = "relative flex h-[22px] cursor-default items-center gap-2 rounded-[5px] px-2 outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:text-foreground/35 data-[highlighted]:bg-ring data-[highlighted]:text-white [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5" type MenuBarItemProps = React.ComponentProps<typeof MenubarPrimitive.Item> & { /** Indent the label to line up with checkbox items. */ inset?: boolean /** Red text for destructive actions. */ variant?: "default" | "destructive" } function MenuBarItem({ inset, variant = "default", className, ...props }: MenuBarItemProps) { return ( <MenubarPrimitive.Item data-slot="menu-bar-item" data-variant={variant} className={cn(itemClasses, inset && "ps-6", variant === "destructive" && "text-destructive", "group/menu-item", className)} {...props} /> ) } function MenuBarCheckboxItem({ className, children, ...props }: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) { return ( <MenubarPrimitive.CheckboxItem data-slot="menu-bar-checkbox-item" className={cn(itemClasses, "group/menu-item ps-6", className)} {...props}> <span className="absolute start-1.5 flex size-3.5 items-center justify-center"> <MenubarPrimitive.ItemIndicator> <CheckIcon className="size-3" strokeWidth={2.5} aria-hidden="true" /> </MenubarPrimitive.ItemIndicator> </span> {children} </MenubarPrimitive.CheckboxItem> ) } function MenuBarLabel({ className, ...props }: React.ComponentProps<typeof MenubarPrimitive.Label>) { return ( <MenubarPrimitive.Label data-slot="menu-bar-label" className={cn("px-2 pt-1.5 pb-0.5 text-[11px] font-semibold text-muted-foreground", className)} {...props} /> ) } function MenuBarSeparator({ className, ...props }: React.ComponentProps<typeof MenubarPrimitive.Separator>) { return <MenubarPrimitive.Separator data-slot="menu-bar-separator" className={cn("mx-2 my-[5px] h-px bg-foreground/10", className)} {...props} /> } type MenuBarShortcutProps = React.ComponentProps<"span"> /** Right-aligned key equivalent, e.g. "⇧⌘N". Plain text, so it is part of the item's accessible name. */ function MenuBarShortcut({ className, ...props }: MenuBarShortcutProps) { return ( <span data-slot="menu-bar-shortcut" className={cn("ms-auto ps-6 font-sans text-[12px] tracking-[0.12em] text-muted-foreground group-data-[highlighted]/menu-item:text-white/90", className)} {...props} /> ) } function MenuBarSubTrigger({ className, children, ...props }: React.ComponentProps<typeof MenubarPrimitive.SubTrigger>) { return ( <MenubarPrimitive.SubTrigger data-slot="menu-bar-sub-trigger" className={cn(itemClasses, "data-[state=open]:bg-foreground/10 data-[state=open]:data-[highlighted]:bg-ring", className)} {...props} > {children} <ChevronRightIcon className="ms-auto size-3.5 opacity-60" aria-hidden="true" /> </MenubarPrimitive.SubTrigger> ) } type MenuBarSubContentProps = React.ComponentProps<typeof MenubarPrimitive.SubContent> & { /** Render into a portal on document.body (default). */ portal?: boolean } function MenuBarSubContent({ portal = true, className, sideOffset = 2, ...props }: MenuBarSubContentProps) { const content = ( <MenubarPrimitive.SubContent data-slot="menu-bar-sub-content" sideOffset={sideOffset} className={cn(menuSurface, "min-w-[10rem] origin-(--radix-menubar-content-transform-origin)", className)} {...props} /> ) return portal ? <MenubarPrimitive.Portal>{content}</MenubarPrimitive.Portal> : content } type MenuBarStatusProps = React.ComponentProps<"div"> /** The right side of the bar: status icons and the clock. */ function MenuBarStatus({ className, ...props }: MenuBarStatusProps) { return <div data-slot="menu-bar-status" className={cn("ms-auto flex shrink-0 items-center gap-0.5", className)} {...props} /> } type MenuBarStatusItemProps = React.ComponentProps<"button"> /** An icon button in the status area. Give it an aria-label. */ function MenuBarStatusItem({ className, type = "button", ...props }: MenuBarStatusItemProps) { return ( <button type={type} data-slot="menu-bar-status-item" className={cn( "flex h-[22px] min-w-[26px] shrink-0 items-center justify-center gap-1 rounded-[5px] px-1.5 outline-none transition-colors duration-75 hover:bg-foreground/[0.08] focus-visible:ring-2 focus-visible:ring-ring/60 [&_svg]:size-[15px] [&_svg]:shrink-0", className )} {...props} /> ) } type MenuBarClockProps = Omit<React.ComponentProps<"time">, "children"> & { /** BCP 47 locale for formatting. Fixed by default so server and browser agree. */ locale?: string /** Intl options. Default: short weekday, month, day, and hour:minute. */ format?: Intl.DateTimeFormatOptions /** Show a fixed time instead of the live clock (useful for mockups and screenshots). */ value?: Date } const defaultClockFormat: Intl.DateTimeFormatOptions = { weekday: "short", month: "short", day: "numeric", hour: "numeric", minute: "2-digit" } /** * A live clock that ticks each minute. It renders nothing time-specific on the server and fills in after * mount, so it never causes a hydration mismatch. */ function MenuBarClock({ locale = "en-US", format = defaultClockFormat, value, className, ...props }: MenuBarClockProps) { const fixed = value?.getTime() const [now, setNow] = React.useState<Date | null>(value ?? null) React.useEffect(() => { if (fixed !== undefined) { setNow(new Date(fixed)) return } setNow(new Date()) let interval: number | undefined const timeout = window.setTimeout(() => { setNow(new Date()) interval = window.setInterval(() => setNow(new Date()), 60_000) }, 60_000 - (Date.now() % 60_000)) return () => { window.clearTimeout(timeout) window.clearInterval(interval) } }, [fixed]) const text = now ? new Intl.DateTimeFormat(locale, format).format(now).replace(/,/g, "") : "" return ( <time data-slot="menu-bar-clock" dateTime={now?.toISOString()} className={cn("inline-flex h-[22px] min-w-[4.5rem] shrink-0 items-center justify-end px-1.5 tabular-nums", className)} {...props} > {text} </time> ) } export { MenuBar, MenuBarCheckboxItem, MenuBarClock, MenuBarContent, MenuBarGroup, MenuBarItem, MenuBarLabel, MenuBarMenu, MenuBarSeparator, MenuBarShortcut, MenuBarStatus, MenuBarStatusItem, MenuBarSub, MenuBarSubContent, MenuBarSubTrigger, MenuBarTrigger, type MenuBarClockProps, type MenuBarContentProps, type MenuBarItemProps, type MenuBarProps, type MenuBarShortcutProps, type MenuBarStatusItemProps, type MenuBarStatusProps, type MenuBarSubContentProps, type MenuBarTriggerProps, }Update the import paths to match your project setup.
Usage
import { MenuBar, MenuBarCheckboxItem, MenuBarClock, MenuBarContent, MenuBarGroup, MenuBarItem, MenuBarLabel, MenuBarMenu, MenuBarSeparator, MenuBarShortcut, MenuBarStatus, MenuBarStatusItem, MenuBarSub, MenuBarSubContent, MenuBarSubTrigger, MenuBarTrigger } from "@/components/ballmac/menu-bar"The full example is in the Code tab above.
API reference
<MenuBarTrigger>
| Prop | Type | Default |
|---|---|---|
variant"app" sets the trigger in bold, like the application menu next to the logo. | "default" | "app" | "default" |
<MenuBarContent>
| Prop | Type | Default |
|---|---|---|
portalRender into a portal on document.body (default). Set false to keep the menu inside its container, e.g. in a mockup. | boolean | true |
<MenuBarItem>
| Prop | Type | Default |
|---|---|---|
insetIndent the label to line up with checkbox items. | boolean | — |
variantRed text for destructive actions. | "default" | "destructive" | "default" |
<MenuBarSubContent>
| Prop | Type | Default |
|---|---|---|
portalRender into a portal on document.body (default). | boolean | true |
<MenuBarClock>
| Prop | Type | Default |
|---|---|---|
localeBCP 47 locale for formatting. Fixed by default so server and browser agree. | string | "en-US" |
formatIntl options. Default: short weekday, month, day, and hour:minute. | Intl.DateTimeFormatOptions | defaultClockFormat |
valueShow a fixed time instead of the live clock (useful for mockups and screenshots). | Date | — |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Tab | Focuses the menu bar (one stop), then the status buttons |
| ArrowLeft / ArrowRight | Moves between menus, also while one is open |
| Enter / Space / ArrowDown | Opens the focused menu |
| ArrowUp / ArrowDown, typeahead | Moves between items |
| Escape | Closes the menu and returns focus to its trigger |
Use with AI
<MenuBar> holds <MenuBarMenu value><MenuBarTrigger/><MenuBarContent>items</MenuBarContent></MenuBarMenu> (Radix Menubar parts with Mac styling) followed by <MenuBarStatus> with <MenuBarStatusItem aria-label> icon buttons and <MenuBarClock />. Use variant="app" on the app-name trigger and <MenuBarShortcut> for key equivalents. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Menu Bar (@ballmac/menu-bar) to this project with the shadcn MCP, then use it where it fits.
Use it for
- A Mac desktop mockup or product hero where the app's menus should really open
- A web app that wants desktop-style menus with keyboard navigation (File, Edit, View)
- Status bars that pair a few icon buttons with a clock
Not for
- Site navigation for a marketing page (use a header with links)
- A single action menu on a button (use a dropdown menu)
Registry JSON: https://ui.ballmac.com/r/menu-bar.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.
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.
Dynamic Island
A black pill or MacBook-style notch that morphs between live activities (timers, uploads, an AI thinking state, notifications) with a spring layout animation. Each view is announced politely.
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.