Menubar

A horizontal navigation bar with clickable menu items.

The Menubar component provides a horizontal navigation bar with dropdown menus. It supports keyboard shortcuts, disabled items, separators, and submenus. Built from scratch using React and native HTML elements. No dependencies on any UI library.

Code

TypeScript: Copy this code into components/ui/menubar.tsx:

tsx
"use client"

import * as React from "react"
import * as ReactDOM from "react-dom"
import { cn } from "@/lib/utils"

const isMac = typeof window !== "undefined" && navigator.platform.toUpperCase().indexOf("MAC") >= 0

const formatShortcut = (shortcut: string) => {
  if (isMac) {
    return shortcut.replace(/Mod/g, "⌘").replace(/Alt/g, "⌥").replace(/Shift/g, "⇧").replace(/Ctrl/g, "⌃")
  }
  return shortcut.replace(/Mod/g, "Ctrl")
}

interface MenubarContextValue {
  activeMenu: string | null
  setActiveMenu: (menu: string | null) => void
}

const MenubarContext = React.createContext<MenubarContextValue | undefined>(undefined)

const useMenubar = () => {
  const context = React.useContext(MenubarContext)
  if (!context) {
    throw new Error("Menubar components must be used within Menubar")
  }
  return context
}

interface MenubarProps {
  children: React.ReactNode
}

const Menubar = ({ children }: MenubarProps) => {
  const [activeMenu, setActiveMenu] = React.useState<string | null>(null)

  React.useEffect(() => {
    if (!activeMenu) return

    const handleClick = (e: MouseEvent) => {
      const target = e.target as Node
      const portalContent = document.querySelector('[data-menubar-content]')
      
      if (portalContent && !portalContent.contains(target)) {
        const trigger = document.querySelector(`[data-menubar-trigger="${activeMenu}"]`)
        if (trigger && !trigger.contains(target)) {
          setActiveMenu(null)
        }
      }
    }

    const handleEscape = (e: KeyboardEvent) => {
      if (e.key === "Escape") {
        setActiveMenu(null)
      }
    }

    document.addEventListener("mousedown", handleClick, true)
    document.addEventListener("keydown", handleEscape)
    return () => {
      document.removeEventListener("mousedown", handleClick, true)
      document.removeEventListener("keydown", handleEscape)
    }
  }, [activeMenu])

  return (
    <MenubarContext.Provider value={{ activeMenu, setActiveMenu }}>
      <div className="flex items-center gap-1 border-2 border-foreground rounded-md p-1 bg-background neobrutalism-shadow">
        {children}
      </div>
    </MenubarContext.Provider>
  )
}

interface MenubarMenuProps {
  value: string
  children: React.ReactNode
}

const MenubarMenu = ({ value, children }: MenubarMenuProps) => {
  return <>{children}</>
}

interface MenubarTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  value: string
}

const MenubarTrigger = React.forwardRef<HTMLButtonElement, MenubarTriggerProps>(
  ({ className, value, children, ...props }, ref) => {
    const { activeMenu, setActiveMenu } = useMenubar()
    const isActive = activeMenu === value

    const handleClick = () => {
      setActiveMenu(isActive ? null : value)
    }

    return (
      <button
        ref={ref}
        data-menubar-trigger={value}
        onClick={handleClick}
        className={cn(
          "px-3 py-1.5 text-sm font-bold rounded-md border-2 border-transparent transition-colors",
          isActive
            ? "bg-primary text-primary-foreground border-foreground neobrutalism-shadow-sm"
            : "hover:bg-muted",
          className
        )}
        {...props}
      >
        {children}
      </button>
    )
  }
)
MenubarTrigger.displayName = "MenubarTrigger"

interface MenubarContentProps extends React.HTMLAttributes<HTMLDivElement> {
  value: string
  align?: "start" | "center" | "end"
}

const MenubarContent = React.forwardRef<HTMLDivElement, MenubarContentProps>(
  ({ className, value, align = "start", children, ...props }, ref) => {
    const { activeMenu } = useMenubar()
    const contentRef = React.useRef<HTMLDivElement>(null)
    const triggerRef = React.useRef<HTMLElement | null>(null)

    React.useImperativeHandle(ref, () => contentRef.current as HTMLDivElement)

    React.useEffect(() => {
      if (activeMenu === value) {
        triggerRef.current = document.querySelector(`[data-menubar-trigger="${value}"]`)
      }
    }, [activeMenu, value])

    React.useEffect(() => {
      if (activeMenu !== value || !contentRef.current || !triggerRef.current) return

      const content = contentRef.current
      const trigger = triggerRef.current

      const triggerRect = trigger.getBoundingClientRect()
      const contentRect = content.getBoundingClientRect()
      const viewportWidth = window.innerWidth
      const viewportHeight = window.innerHeight

      let top = triggerRect.bottom + 4
      let left = triggerRect.left

      if (align === "end") {
        left = triggerRect.right - contentRect.width
      } else if (align === "center") {
        left = triggerRect.left + (triggerRect.width - contentRect.width) / 2
      }

      if (left + contentRect.width > viewportWidth) {
        left = viewportWidth - contentRect.width - 8
      }
      if (left < 8) left = 8
      if (top + contentRect.height > viewportHeight) {
        top = triggerRect.top - contentRect.height - 4
      }
      if (top < 8) top = 8

      content.style.position = "fixed"
      content.style.top = `${top}px`
      content.style.left = `${left}px`
    }, [activeMenu, value, align])

    if (activeMenu !== value) return null

    const content = (
      <div
        ref={contentRef}
        data-menubar-content
        className={cn(
          "fixed z-50 min-w-[8rem] rounded-md border-2 border-foreground bg-background p-1 neobrutalism-shadow",
          className
        )}
        onMouseDown={(e) => e.stopPropagation()}
        {...props}
      >
        {children}
      </div>
    )

    return ReactDOM.createPortal(content, document.body)
  }
)
MenubarContent.displayName = "MenubarContent"

interface MenubarItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  disabled?: boolean
  inset?: boolean
}

const MenubarItem = React.forwardRef<HTMLButtonElement, MenubarItemProps>(
  ({ className, disabled, inset, children, ...props }, ref) => {
    return (
      <button
        ref={ref}
        disabled={disabled}
        className={cn(
          "relative flex w-full cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm font-bold outline-none transition-colors",
          "focus:bg-accent focus:text-accent-foreground",
          disabled && "opacity-50 pointer-events-none",
          inset && "pl-8",
          className
        )}
        {...props}
      >
        {children}
      </button>
    )
  }
)
MenubarItem.displayName = "MenubarItem"

interface MenubarLabelProps extends React.HTMLAttributes<HTMLDivElement> {}

const MenubarLabel = React.forwardRef<HTMLDivElement, MenubarLabelProps>(
  ({ className, ...props }, ref) => (
    <div
      ref={ref}
      className={cn("px-2 py-1.5 text-sm font-bold", className)}
      {...props}
    />
  )
)
MenubarLabel.displayName = "MenubarLabel"

interface MenubarSeparatorProps extends React.HTMLAttributes<HTMLDivElement> {}

const MenubarSeparator = React.forwardRef<HTMLDivElement, MenubarSeparatorProps>(
  ({ className, ...props }, ref) => (
    <div
      ref={ref}
      className={cn("my-1 h-px bg-foreground", className)}
      {...props}
    />
  )
)
MenubarSeparator.displayName = "MenubarSeparator"

interface MenubarShortcutProps extends React.HTMLAttributes<HTMLSpanElement> {
  shortcut?: string
}

const MenubarShortcut = React.forwardRef<HTMLSpanElement, MenubarShortcutProps>(
  ({ className, shortcut, children, ...props }, ref) => {
    const displayText = shortcut ? formatShortcut(shortcut) : children
    return (
      <span
        ref={ref}
        className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
        {...props}
      >
        {displayText}
      </span>
    )
  }
)
MenubarShortcut.displayName = "MenubarShortcut"

export {
  Menubar,
  MenubarMenu,
  MenubarTrigger,
  MenubarContent,
  MenubarItem,
  MenubarLabel,
  MenubarSeparator,
  MenubarShortcut,
}

JavaScript: Copy this code into components/ui/menubar.jsx:

jsx
"use client"

import * as React from "react"
import * as ReactDOM from "react-dom"
import { cn } from "@/lib/utils"

const isMac = typeof window !== "undefined" && navigator.platform.toUpperCase().indexOf("MAC") >= 0

const formatShortcut = (shortcut) => {
  if (isMac) {
    return shortcut.replace(/Mod/g, "⌘").replace(/Alt/g, "⌥").replace(/Shift/g, "⇧").replace(/Ctrl/g, "⌃")
  }
  return shortcut.replace(/Mod/g, "Ctrl")
}

const MenubarContext = React.createContext(undefined)

const useMenubar = () => {
  const context = React.useContext(MenubarContext)
  if (!context) {
    throw new Error("Menubar components must be used within Menubar")
  }
  return context
}

const Menubar = ({ children }) => {
  const [activeMenu, setActiveMenu] = React.useState(null)

  React.useEffect(() => {
    if (!activeMenu) return

    const handleClick = (e) => {
      const target = e.target
      const portalContent = document.querySelector('[data-menubar-content]')
      
      if (portalContent && !portalContent.contains(target)) {
        const trigger = document.querySelector(`[data-menubar-trigger="${activeMenu}"]`)
        if (trigger && !trigger.contains(target)) {
          setActiveMenu(null)
        }
      }
    }

    const handleEscape = (e) => {
      if (e.key === "Escape") {
        setActiveMenu(null)
      }
    }

    document.addEventListener("mousedown", handleClick, true)
    document.addEventListener("keydown", handleEscape)
    return () => {
      document.removeEventListener("mousedown", handleClick, true)
      document.removeEventListener("keydown", handleEscape)
    }
  }, [activeMenu])

  return (
    <MenubarContext.Provider value={{ activeMenu, setActiveMenu }}>
      <div className="flex items-center gap-1 border-2 border-foreground rounded-md p-1 bg-background neobrutalism-shadow">
        {children}
      </div>
    </MenubarContext.Provider>
  )
}

const MenubarMenu = ({ value, children }) => {
  return <>{children}</>
}

const MenubarTrigger = React.forwardRef(
  ({ className, value, children, ...props }, ref) => {
    const { activeMenu, setActiveMenu } = useMenubar()
    const isActive = activeMenu === value

    const handleClick = () => {
      setActiveMenu(isActive ? null : value)
    }

    return (
      <button
        ref={ref}
        data-menubar-trigger={value}
        onClick={handleClick}
        className={cn(
          "px-3 py-1.5 text-sm font-bold rounded-md border-2 border-transparent transition-colors",
          isActive
            ? "bg-primary text-primary-foreground border-foreground neobrutalism-shadow-sm"
            : "hover:bg-muted",
          className
        )}
        {...props}
      >
        {children}
      </button>
    )
  }
)
MenubarTrigger.displayName = "MenubarTrigger"

const MenubarContent = React.forwardRef(
  ({ className, value, align = "start", children, ...props }, ref) => {
    const { activeMenu } = useMenubar()
    const contentRef = React.useRef(null)
    const triggerRef = React.useRef(null)

    React.useImperativeHandle(ref, () => contentRef.current)

    React.useEffect(() => {
      if (activeMenu === value) {
        triggerRef.current = document.querySelector(`[data-menubar-trigger="${value}"]`)
      }
    }, [activeMenu, value])

    React.useEffect(() => {
      if (activeMenu !== value || !contentRef.current || !triggerRef.current) return

      const content = contentRef.current
      const trigger = triggerRef.current

      const triggerRect = trigger.getBoundingClientRect()
      const contentRect = content.getBoundingClientRect()
      const viewportWidth = window.innerWidth
      const viewportHeight = window.innerHeight

      let top = triggerRect.bottom + 4
      let left = triggerRect.left

      if (align === "end") {
        left = triggerRect.right - contentRect.width
      } else if (align === "center") {
        left = triggerRect.left + (triggerRect.width - contentRect.width) / 2
      }

      if (left + contentRect.width > viewportWidth) {
        left = viewportWidth - contentRect.width - 8
      }
      if (left < 8) left = 8
      if (top + contentRect.height > viewportHeight) {
        top = triggerRect.top - contentRect.height - 4
      }
      if (top < 8) top = 8

      content.style.position = "fixed"
      content.style.top = `${top}px`
      content.style.left = `${left}px`
    }, [activeMenu, value, align])

    if (activeMenu !== value) return null

    const content = (
      <div
        ref={contentRef}
        data-menubar-content
        className={cn(
          "fixed z-50 min-w-[8rem] rounded-md border-2 border-foreground bg-background p-1 neobrutalism-shadow",
          className
        )}
        onMouseDown={(e) => e.stopPropagation()}
        {...props}
      >
        {children}
      </div>
    )

    return ReactDOM.createPortal(content, document.body)
  }
)
MenubarContent.displayName = "MenubarContent"

const MenubarItem = React.forwardRef(
  ({ className, disabled, inset, children, ...props }, ref) => {
    return (
      <button
        ref={ref}
        disabled={disabled}
        className={cn(
          "relative flex w-full cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm font-bold outline-none transition-colors",
          "focus:bg-accent focus:text-accent-foreground",
          disabled && "opacity-50 pointer-events-none",
          inset && "pl-8",
          className
        )}
        {...props}
      >
        {children}
      </button>
    )
  }
)
MenubarItem.displayName = "MenubarItem"

const MenubarLabel = React.forwardRef(
  ({ className, ...props }, ref) => (
    <div
      ref={ref}
      className={cn("px-2 py-1.5 text-sm font-bold", className)}
      {...props}
    />
  )
)
MenubarLabel.displayName = "MenubarLabel"

const MenubarSeparator = React.forwardRef(
  ({ className, ...props }, ref) => (
    <div
      ref={ref}
      className={cn("my-1 h-px bg-foreground", className)}
      {...props}
    />
  )
)
MenubarSeparator.displayName = "MenubarSeparator"

const MenubarShortcut = React.forwardRef(
  ({ className, shortcut, children, ...props }, ref) => {
    const displayText = shortcut ? formatShortcut(shortcut) : children
    return (
      <span
        ref={ref}
        className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
        {...props}
      >
        {displayText}
      </span>
    )
  }
)
MenubarShortcut.displayName = "MenubarShortcut"

export {
  Menubar,
  MenubarMenu,
  MenubarTrigger,
  MenubarContent,
  MenubarItem,
  MenubarLabel,
  MenubarSeparator,
  MenubarShortcut,
}

Usage

TypeScript:

tsx
import {
  Menubar,
  MenubarMenu,
  MenubarTrigger,
  MenubarContent,
  MenubarItem,
  MenubarShortcut,
  MenubarSeparator,
} from "@/components/ui/menubar"

function MyComponent() {
  return (
    <Menubar>
      <MenubarMenu value="file">
        <MenubarTrigger value="file">File</MenubarTrigger>
        <MenubarContent value="file">
          <MenubarItem>
            New Tab
            <MenubarShortcut shortcut="Mod+T" />
          </MenubarItem>
          <MenubarItem>
            New Window
            <MenubarShortcut shortcut="Mod+N" />
          </MenubarItem>
          <MenubarSeparator />
          <MenubarItem disabled>New Incognito Window</MenubarItem>
        </MenubarContent>
      </MenubarMenu>
    </Menubar>
  )
}

Make sure you also have the lib/utils.ts file with the cn helper function.

Examples

Default