Primitives
Menubar
Desktop command bar with nested menus, shortcuts, and separators.
Composition
Anatomy
- <Menubar>
- <MenubarMenu>
- <MenubarTrigger />
- <MenubarContent>
- <MenubarGroup>
- <MenubarLabel />
- <MenubarItem>
- <MenubarShortcut />
- <MenubarSeparator />
- <MenubarSub>
- <MenubarSubTrigger />
- <MenubarSubContent>
- <MenubarItem />
- <MenubarItem>
- <MenubarShortcut />
- <MenubarGroup>
- <MenubarMenu>
Installation
First install and activate one skin. Core deliberately contains no visual token defaults.
The Control UI source installs this primitive from src/registry/sources/control-ui/ui/menubar.tsx.
npx shadcn@latest add https://control-ui.dev/r/menubar.jsonDependencies
Surface variants
src/registry/sources/control-ui/surface-variants.tsSupportControl variants
src/registry/sources/control-ui/control-variants.tsSupportRaw code
Primary installed source
"use client";import { Menu as MenuPrimitive } from "@base-ui/react/menu";import { Menubar as MenubarPrimitive } from "@base-ui/react/menubar";import type { ComponentProps, CSSProperties, MouseEvent, ReactNode } from "react";import type { OpenChangeEventDetails } from "@/components/control-ui/control-props";import type { ButtonKnobStyle } from "@/components/control-ui/knob-contracts/button-knobs";import type { PopupKnobStyle } from "@/components/control-ui/knob-contracts/popup-knobs";import { cn } from "@/components/control-ui/lib/cn";import { controlEffectsAttribute } from "@/components/control-ui/skin";import { useSkin } from "@/components/control-ui/skin-provider";import { popupItemStructureClasses } from "@/components/control-ui/surface-variants";export type MenubarProps = Omit<ComponentProps<"div">, "style"> & { style?: CSSProperties & PopupKnobStyle } & { modal?: boolean; loopFocus?: boolean; disabled?: boolean; orientation?: "horizontal" | "vertical";};export type MenubarMenuProps = { children?: ReactNode; open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean, eventDetails: OpenChangeEventDetails) => void;};export type MenubarTriggerProps = Omit<ComponentProps<"button">, "style"> & { style?: CSSProperties & ButtonKnobStyle };export type MenubarContentProps = Omit<ComponentProps<"div">, "style"> & { style?: CSSProperties & PopupKnobStyle };export type MenubarGroupProps = ComponentProps<"div"> & { style?: CSSProperties & PopupKnobStyle };export type MenubarItemProps = Omit<Omit<ComponentProps<"div">, "onClick">, "style"> & { style?: CSSProperties & PopupKnobStyle } & { disabled?: boolean; onClick?: (event: MouseEvent) => void;};export type MenubarSeparatorProps = Omit<ComponentProps<"div">, "style"> & { style?: CSSProperties & PopupKnobStyle };export type MenubarLabelProps = Omit<ComponentProps<"div">, "style"> & { style?: CSSProperties & PopupKnobStyle } & { inset?: boolean;};export type MenubarShortcutProps = Omit<ComponentProps<"span">, "style"> & { style?: CSSProperties & PopupKnobStyle };export type MenubarSubProps = { children?: ReactNode; open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean, eventDetails: OpenChangeEventDetails) => void;};export type MenubarSubTriggerProps = Omit<Omit<ComponentProps<"div">, "onClick">, "style"> & { style?: CSSProperties & PopupKnobStyle } & { disabled?: boolean; inset?: boolean;};export type MenubarSubContentProps = Omit<ComponentProps<"div">, "style"> & { style?: CSSProperties & PopupKnobStyle };const popupClasses = "min-w-[11rem]";const itemClasses = cn("group/mbi relative", popupItemStructureClasses);export function Menubar({ className, ...props }: MenubarProps) { return ( <MenubarPrimitive data-control-ui="menubar" data-popup-kind="menubar" data-slot="root" data-control-family="popup" data-popup-part="bar" className={cn("inline-flex items-center", className)} {...props} /> );}export function MenubarMenu(props: MenubarMenuProps) { return <MenuPrimitive.Root {...props} />;}export function MenubarTrigger({ className, ...props }: MenubarTriggerProps) { return ( <MenuPrimitive.Trigger data-control-ui="menubar" data-control-family="button" data-slot="trigger" data-control="true" data-cursor="default" data-size="sm" data-variant="ghost" data-tone="neutral" data-shape="default" className={cn("inline-flex select-none items-center", className)} {...props} /> );}export function MenubarContent({ className, children, ...props }: MenubarContentProps) { const skin = useSkin(); return ( <MenuPrimitive.Portal> <MenuPrimitive.Positioner data-skin={skin.id} data-effects={controlEffectsAttribute(skin.effects)} align="start" sideOffset={6} className="z-(--z-popup)" > <MenuPrimitive.Popup data-control-ui="menubar" data-popup-kind="menubar" data-slot="content" data-surface="floating" data-control-family="popup" data-popup-part="list-surface" className={cn(popupClasses, className)} {...props} > {children} </MenuPrimitive.Popup> </MenuPrimitive.Positioner> </MenuPrimitive.Portal> );}export function MenubarGroup({ className, ...props }: MenubarGroupProps) { return ( <MenuPrimitive.Group data-control-ui="menubar" data-control-family="popup" data-popup-kind="menubar" data-slot="group" className={className} {...props} /> );}export function MenubarItem({ className, ...props }: MenubarItemProps) { return ( <MenuPrimitive.Item data-control-ui="menubar" data-popup-kind="menubar" data-slot="item" data-control-family="popup" data-popup-part="item" className={cn(itemClasses, className)} {...props} /> );}export function MenubarLabel({ className, inset = false, ...props }: MenubarLabelProps) { return ( <MenuPrimitive.GroupLabel data-control-ui="menubar" data-popup-kind="menubar" data-slot="label" data-control-family="popup" data-popup-part="label" data-inset={inset || undefined} className={cn(className)} {...props} /> );}export function MenubarSeparator({ className, ...props }: MenubarSeparatorProps) { return ( <MenuPrimitive.Separator data-control-ui="menubar" data-popup-kind="menubar" data-slot="separator" data-control-family="popup" data-popup-part="separator" className={cn(className)} {...props} /> );}export function MenubarShortcut({ className, ...props }: MenubarShortcutProps) { return ( <span data-control-ui="menubar" data-popup-kind="menubar" data-slot="shortcut" data-control-family="popup" data-popup-part="shortcut" aria-hidden="true" className={cn(className)} {...props} /> );}export function MenubarSub(props: MenubarSubProps) { return <MenuPrimitive.SubmenuRoot {...props} />;}export function MenubarSubTrigger({ className, inset = false, children, ...props }: MenubarSubTriggerProps) { return ( <MenuPrimitive.SubmenuTrigger data-control-ui="menubar" data-popup-kind="menubar" data-slot="sub-trigger" data-control-family="popup" data-popup-part="item" data-inset={inset || undefined} className={cn(itemClasses, className)} {...props} > {children} <span data-control-ui="menubar" data-popup-kind="menubar" data-control-family="popup" data-slot="sub-trigger-indicator" data-icon-dir="inline" aria-hidden="true" > › </span> </MenuPrimitive.SubmenuTrigger> );}export function MenubarSubContent({ className, children, ...props }: MenubarSubContentProps) { const skin = useSkin(); return ( <MenuPrimitive.Portal> <MenuPrimitive.Positioner data-skin={skin.id} data-effects={controlEffectsAttribute(skin.effects)} align="start" sideOffset={-4} alignOffset={-5} className="z-(--z-popup)" > <MenuPrimitive.Popup data-control-ui="menubar" data-popup-kind="menubar" data-slot="sub-content" data-surface="floating" data-control-family="popup" data-popup-part="list-surface" className={cn(popupClasses, className)} {...props} > {children} </MenuPrimitive.Popup> </MenuPrimitive.Positioner> </MenuPrimitive.Portal> );}Knobs
Typed custom properties the recipe paints with. Set one on the root — style, a utility class, or a skin — and every slot inherits it.
--cui-button-* · 25 knobsHow the cascade resolves--cui-button-radius<length-percentage>var(--nest-radius, var(--radius-control))--cui-button-gap<length-percentage>calc(var(--spacing) * 1.5)--cui-button-icon<length>1rem--cui-button-height<length-percentage>var(--control-h-md)--cui-button-padding-inline<length-percentage>var(--padding-x)--cui-button-icon-padding-inline<length-percentage>max(0px, calc(var(--cui-button-padding-inline) - var(--spacing) * 0.5))--cui-button-font-size<length-percentage>var(--text-body)--cui-button-background<color>transparent--cui-button-background-image*none--cui-button-hover-background-image*var(--cui-button-background-image)--cui-button-foreground<color>var(--muted-foreground)--cui-button-hover-background<color>var(--hover-fill)--cui-button-hover-foreground<color>var(--foreground)--cui-button-press-background<color>var(--cui-button-hover-background)--cui-button-press-scale<number>0.96--cui-button-active-background<color>var(--active-fill)--cui-button-active-foreground<color>var(--foreground)--cui-button-active-hover-background<color>var(--cui-button-active-background)--cui-button-shadow*0 0 transparent--cui-button-hover-shadow*var(--cui-button-shadow)--cui-button-press-shadow*var(--cui-button-hover-shadow)--cui-button-active-shadow*var(--cui-button-shadow)--cui-button-open-background<color>var(--cui-button-hover-background)--cui-button-open-foreground<color>var(--cui-button-hover-foreground)--cui-button-open-shadow*var(--cui-button-shadow)--cui-popup-* · 44 knobsHow the cascade resolves--cui-popup-title-foreground<color>var(--cui-popup-foreground)--cui-popup-title-font-size<length-percentage>var(--text-heading-3)--cui-popup-title-font-weight<number>var(--font-weight-semibold)--cui-popup-title-line-height<number>var(--text-heading-3--line-height)--cui-popup-title-letter-spacing<length>-0.025em--cui-popup-description-foreground<color>var(--muted-foreground)--cui-popup-close-inset<length-percentage>calc(var(--spacing) * 3)--cui-popup-item-inset<length-percentage>calc(var(--spacing) * 8)--cui-popup-label-padding-block<length-percentage>var(--cui-popup-item-padding-block)--cui-popup-label-padding-inline<length-percentage>var(--cui-popup-item-padding-inline)--cui-popup-label-font-size<length-percentage>var(--text-micro)--cui-popup-label-font-weight<number>var(--font-weight-medium)--cui-popup-label-letter-spacing<length>0.08em--cui-popup-label-text-transformnone | uppercase | lowercase | capitalizeuppercase--cui-popup-separator-inset<length-percentage>calc(var(--popover-padding) * -1)--cui-popup-shortcut-font-size<length-percentage>var(--text-body)--cui-popup-shortcut-letter-spacing<length>0.02em--cui-popup-status-success-foreground<color>var(--success-text)--cui-popup-status-warning-foreground<color>var(--warning-text)--cui-popup-status-info-foreground<color>var(--info-text)--cui-popup-status-error-foreground<color>var(--destructive)--cui-popup-radius<length-percentage>var(--radius-popover)--cui-popup-background<color>var(--popover)--cui-popup-foreground<color>var(--popover-foreground)--cui-popup-border-color<color>var(--control-rim)--cui-popup-border-width<length>var(--control-rim-width)--cui-popup-shadow*var(--shadow-pop)--cui-popup-backdrop-filter*blur(var(--backdrop-blur-popover))--cui-popup-backdrop-background<color>oklch(from var(--foreground) l c h / var(--overlay-opacity))--cui-popup-backdrop-blur<length>var(--backdrop-blur-overlay)--cui-popup-item-radius<length-percentage>var(--radius-popup-item-fit)--cui-popup-item-font-size<length-percentage>var(--text-body)--cui-popup-item-icon-size<length-percentage>1rem--cui-popup-item-padding-block<length-percentage>calc(var(--spacing) * 1)--cui-popup-item-padding-inline<length-percentage>calc(var(--padding-x) * 0.5)--cui-popup-item-min-height<length-percentage>max(var(--target-min), var(--control-h-xs))--cui-popup-item-highlight-shadow*inset 0 0 0 1px oklch(from var(--foreground) l c h / 0.35)--cui-popup-item-foreground<color>var(--foreground)--cui-popup-item-highlight-background<color>var(--hover-fill)--cui-popup-item-highlight-foreground<color>var(--cui-popup-item-foreground)--cui-popup-item-highlight-muted-foreground<color>var(--muted-foreground)--cui-popup-item-disabled-opacity<number>var(--disabled-opacity)--cui-popup-separator-color<color>var(--border)--cui-popup-shortcut-foreground<color>var(--muted-foreground)