AniUI

Menubar

Horizontal bar of menus, each opening a dropdown of items.

FileEdit
New Document
Open…
Save
Web preview — components render natively on iOS & Android
import { Menubar, MenubarMenu, MenubarItem } from "@/components/ui/menubar";

export function MyScreen() {
return (
  <Menubar>
    <MenubarMenu trigger="File">
      <MenubarItem onPress={() => {}}>New File</MenubarItem>
      <MenubarItem onPress={() => {}}>Open…</MenubarItem>
      <MenubarItem onPress={() => {}}>Save</MenubarItem>
    </MenubarMenu>
    <MenubarMenu trigger="Edit">
      <MenubarItem onPress={() => {}}>Undo</MenubarItem>
      <MenubarItem onPress={() => {}}>Redo</MenubarItem>
    </MenubarMenu>
  </Menubar>
);
}

Installation

npx @aniui/cli add menubar

Usage

import { Menubar, MenubarMenu, MenubarItem } from "@/components/ui/menubar";

export function MyScreen() {
  return (
    <Menubar>
      <MenubarMenu trigger="File">
        <MenubarItem onPress={() => {}}>New File</MenubarItem>
        <MenubarItem onPress={() => {}}>Save</MenubarItem>
      </MenubarMenu>
      <MenubarMenu trigger="Edit">
        <MenubarItem onPress={() => {}}>Undo</MenubarItem>
      </MenubarMenu>
    </Menubar>
  );
}

Props

Menubar

PropTypeDefault
children
React.ReactNode
—
className
string
—

Also accepts all View props from React Native.

MenubarMenu

PropTypeDefault
trigger
React.ReactNode
—
children
React.ReactNode
—
className
string
—

MenubarItem

PropTypeDefault
onPress
() => void
—
children
React.ReactNode
—
className
string
—

Also accepts all Pressable props from React Native.

Accessibility

  • Menubar uses accessibilityRole="menubar".
  • Triggers and items use accessibilityRole="menuitem"; the open panel uses accessibilityRole="menu".

Source

components/ui/menubar.tsx
import React, { createContext, useContext, useRef, useState } from "react";
import { View, Text, Pressable, Modal } from "react-native";
import { cn } from "@/lib/utils";

const MenubarCloseContext = createContext<(() => void) | null>(null);

export interface MenubarProps extends React.ComponentPropsWithoutRef<typeof View> {
  className?: string;
  children?: React.ReactNode;
}

export function Menubar({ className, ...props }: MenubarProps) {
  return (
    <View
      className={cn("h-12 flex-row items-center gap-1 rounded-md border border-border bg-card px-1", className)}
      accessibilityRole="menubar"
      {...props}
    />
  );
}

export interface MenubarMenuProps {
  trigger: React.ReactNode;
  className?: string;
  children?: React.ReactNode;
}

export function MenubarMenu({ trigger, className, children }: MenubarMenuProps) {
  const [open, setOpen] = useState(false);
  const [anchor, setAnchor] = useState({ x: 0, y: 0 });
  const ref = useRef<View>(null);

  const openMenu = () => {
    // Open immediately so the menu never depends on a measure callback firing;
    // measureInWindow (not measure) then refines the anchor inside the Modal.
    setOpen(true);
    ref.current?.measureInWindow((x, y, _w, h) => {
      setAnchor({ x, y: y + h + 4 });
    });
  };

  return (
    <>
      <Pressable
        ref={ref}
        onPress={openMenu}
        accessibilityRole="menuitem"
        accessible
        className="min-h-12 min-w-12 justify-center rounded-sm px-3"
      >
        {typeof trigger === "string" ? <Text className="text-sm font-medium text-foreground">{trigger}</Text> : trigger}
      </Pressable>
      <Modal visible={open} transparent animationType="fade" onRequestClose={() => setOpen(false)}>
        <Pressable style={{ flex: 1 }} onPress={() => setOpen(false)} accessible={false}>
          <View
            className={cn("min-w-40 rounded-md border border-border bg-card p-1", className)}
            style={{ position: "absolute", left: anchor.x, top: anchor.y }}
            accessibilityRole="menu"
          >
            <MenubarCloseContext.Provider value={() => setOpen(false)}>{children}</MenubarCloseContext.Provider>
          </View>
        </Pressable>
      </Modal>
    </>
  );
}

export interface MenubarItemProps extends React.ComponentPropsWithoutRef<typeof Pressable> {
  className?: string;
  children?: React.ReactNode;
}

export function MenubarItem({ className, children, onPress, ...props }: MenubarItemProps) {
  const close = useContext(MenubarCloseContext);
  return (
    <Pressable
      accessibilityRole="menuitem"
      accessible
      onPress={(e) => {
        onPress?.(e);
        close?.();
      }}
      className={cn("min-h-12 min-w-12 justify-center rounded-sm px-2", className)}
      {...props}
    >
      {typeof children === "string" ? <Text className="text-sm text-foreground">{children}</Text> : children}
    </Pressable>
  );
}