CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-radix-ui--react-menubar

An accessible React menubar component that provides keyboard navigation, submenus, and customizable styling while maintaining semantic HTML structure and screen reader compatibility.

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

submenus.mddocs/

Submenus

Components for creating nested submenus and additional visual elements like arrows, enabling multi-level menu hierarchies.

Capabilities

MenubarSub (Sub)

Container for submenu functionality with controllable open state, managing the visibility and behavior of nested menu content.

/**
 * Container for submenu with controllable open state
 * @param props - Submenu container props
 * @returns JSX element representing the submenu container
 */
function MenubarSub(props: MenubarSubProps): React.ReactElement;

interface MenubarSubProps {
  /** Child components (typically SubTrigger and SubContent) */
  children?: React.ReactNode;
  /** Controlled open state */
  open?: boolean;
  /** Default open state for uncontrolled usage (default: false) */
  defaultOpen?: boolean;
  /** Callback fired when open state changes */
  onOpenChange?: (open: boolean) => void;
}

Usage Examples:

'use client';
import * as Menubar from "@radix-ui/react-menubar";

// Basic submenu
<Menubar.Sub>
  <Menubar.SubTrigger>
    Export
    <ChevronRightIcon />
  </Menubar.SubTrigger>
  <Menubar.Portal>
    <Menubar.SubContent>
      <Menubar.Item>Export as PDF</Menubar.Item>
      <Menubar.Item>Export as Image</Menubar.Item>
    </Menubar.SubContent>
  </Menubar.Portal>
</Menubar.Sub>

// Controlled submenu
function ControlledSubmenu() {
  const [isOpen, setIsOpen] = React.useState(false);
  
  return (
    <Menubar.Sub open={isOpen} onOpenChange={setIsOpen}>
      <Menubar.SubTrigger>
        Tools {isOpen ? "▼" : "▶"}
      </Menubar.SubTrigger>
      <Menubar.Portal>
        <Menubar.SubContent>
          <Menubar.Item>Format Document</Menubar.Item>
          <Menubar.Item>Check Spelling</Menubar.Item>
        </Menubar.SubContent>
      </Menubar.Portal>
    </Menubar.Sub>
  );
}

MenubarSubTrigger (SubTrigger)

Trigger that opens a submenu when hovered or activated, typically displaying an arrow indicator.

/**
 * Trigger that opens a submenu on hover or activation
 * @param props - Sub-trigger props
 * @returns JSX element representing the submenu trigger
 */
function MenubarSubTrigger(props: MenubarSubTriggerProps): React.ReactElement;

interface MenubarSubTriggerProps extends React.ComponentPropsWithoutRef<'div'> {
  /** Whether the trigger is disabled */
  disabled?: boolean;
  /** Text value for accessibility */
  textValue?: string;
}

Usage Examples:

// Basic sub-trigger with arrow
<Menubar.SubTrigger>
  Recent Files
  <ChevronRightIcon className="sub-arrow" />
</Menubar.SubTrigger>

// Sub-trigger with custom content
<Menubar.SubTrigger>
  <FileIcon />
  <span>Export Options</span>
  <ArrowIcon />
</Menubar.SubTrigger>

// Disabled sub-trigger
<Menubar.SubTrigger disabled>
  Advanced Tools
  <ChevronRightIcon />
</Menubar.SubTrigger>

MenubarSubContent (SubContent)

Content container for submenu items, positioned relative to the sub-trigger.

/**
 * Content container for submenu items
 * @param props - Sub-content props
 * @returns JSX element representing the submenu content
 */
function MenubarSubContent(props: MenubarSubContentProps): React.ReactElement;

interface MenubarSubContentProps extends React.ComponentPropsWithoutRef<'div'> {
  /** Alignment relative to trigger */
  align?: 'start' | 'center' | 'end';
  /** Side preference for positioning */
  side?: 'top' | 'right' | 'bottom' | 'left';
  /** Distance from trigger in pixels */
  sideOffset?: number;
  /** Alignment offset in pixels */
  alignOffset?: number;
  /** Whether content should avoid collisions with the boundary */
  avoidCollisions?: boolean;
  /** Element or area to constrain positioning within */
  collisionBoundary?: Element | null | Array<Element | null>;
  /** Padding from collision boundary in pixels */
  collisionPadding?: number | Partial<Record<'top' | 'right' | 'bottom' | 'left', number>>;
  /** Whether content should stick to trigger when boundary is reached */
  sticky?: 'partial' | 'always';
  /** Callback fired when content loses focus */
  onCloseAutoFocus?: (event: Event) => void;
  /** Callback fired when escape key is pressed */
  onEscapeKeyDown?: (event: KeyboardEvent) => void;
  /** Callback fired when pointer moves outside */
  onPointerDownOutside?: (event: PointerDownOutsideEvent) => void;
  /** Callback fired when focus moves outside the content */
  onFocusOutside?: (event: FocusOutsideEvent) => void;
  /** Callback fired when interaction occurs outside the content */
  onInteractOutside?: (event: InteractOutsideEvent) => void;
}

Usage Examples:

// Basic sub-content
<Menubar.SubContent>
  <Menubar.Item>Export as PDF</Menubar.Item>
  <Menubar.Item>Export as PNG</Menubar.Item>
  <Menubar.Item>Export as SVG</Menubar.Item>
</Menubar.SubContent>

// Sub-content with custom positioning
<Menubar.SubContent 
  side="right" 
  align="start" 
  sideOffset={2}
>
  <Menubar.Item>Option 1</Menubar.Item>
  <Menubar.Item>Option 2</Menubar.Item>
</Menubar.SubContent>

// Sub-content with nested submenus
<Menubar.SubContent>
  <Menubar.Item>Quick Export</Menubar.Item>
  
  <Menubar.Sub>
    <Menubar.SubTrigger>
      Advanced Export
      <ChevronRightIcon />
    </Menubar.SubTrigger>
    <Menubar.Portal>
      <Menubar.SubContent>
        <Menubar.Item>With Metadata</Menubar.Item>
        <Menubar.Item>Compressed</Menubar.Item>
      </Menubar.SubContent>
    </Menubar.Portal>
  </Menubar.Sub>
</Menubar.SubContent>

Key Implementation Details:

  • Data Attributes:
    • SubTrigger automatically receives data-radix-menubar-subtrigger="" attribute
    • SubContent automatically receives data-radix-menubar-content="" attribute
  • CSS Custom Properties: SubContent exposes the same positioning variables as MenubarContent:
    • --radix-menubar-content-transform-origin: Transform origin for animations
    • --radix-menubar-content-available-width: Available width for content
    • --radix-menubar-content-available-height: Available height for content
    • --radix-menubar-trigger-width: Width of the triggering element
    • --radix-menubar-trigger-height: Height of the triggering element
  • Navigation Behavior: Arrow keys navigate horizontally between main menu triggers, preventing submenu opening

MenubarArrow (Arrow)

Optional arrow pointing from trigger to content, providing visual connection between trigger and dropdown.

/**
 * Optional arrow pointing from trigger to content
 * @param props - Arrow props
 * @returns JSX element representing the arrow
 */
function MenubarArrow(props: MenubarArrowProps): React.ReactElement;

interface MenubarArrowProps extends React.ComponentPropsWithoutRef<'svg'> {
  /** Width of the arrow in pixels (default: 10) */
  width?: number;
  /** Height of the arrow in pixels (default: 5) */
  height?: number;
}

Usage Examples:

// Basic arrow
<Menubar.Portal>
  <Menubar.Content>
    <Menubar.Arrow />
    <Menubar.Item>Menu Item</Menubar.Item>
  </Menubar.Content>
</Menubar.Portal>

// Custom sized arrow
<Menubar.Portal>
  <Menubar.Content>
    <Menubar.Arrow width={12} height={6} />
    <Menubar.Item>Menu Item</Menubar.Item>
  </Menubar.Content>
</Menubar.Portal>

// Styled arrow
<Menubar.Portal>
  <Menubar.Content>
    <Menubar.Arrow className="custom-arrow" />
    <Menubar.Item>Menu Item</Menubar.Item>
  </Menubar.Content>
</Menubar.Portal>

Complex Submenu Examples

Multi-Level Menu Structure

function MultiLevelMenu() {
  return (
    <Menubar.Content>
      <Menubar.Item>New File</Menubar.Item>
      <Menubar.Item>Open</Menubar.Item>
      
      <Menubar.Sub>
        <Menubar.SubTrigger>
          Recent Files
          <ChevronRightIcon />
        </Menubar.SubTrigger>
        <Menubar.Portal>
          <Menubar.SubContent>
            <Menubar.Item>document1.txt</Menubar.Item>
            <Menubar.Item>project.json</Menubar.Item>
            
            <Menubar.Sub>
              <Menubar.SubTrigger>
                More Files
                <ChevronRightIcon />
              </Menubar.SubTrigger>
              <Menubar.Portal>
                <Menubar.SubContent>
                  <Menubar.Item>old-project.js</Menubar.Item>
                  <Menubar.Item>backup.sql</Menubar.Item>
                </Menubar.SubContent>
              </Menubar.Portal>
            </Menubar.Sub>
          </Menubar.SubContent>
        </Menubar.Portal>
      </Menubar.Sub>
      
      <Menubar.Separator />
      
      <Menubar.Sub>
        <Menubar.SubTrigger>
          Export
          <ChevronRightIcon />
        </Menubar.SubTrigger>
        <Menubar.Portal>
          <Menubar.SubContent>
            <Menubar.Item>Export as PDF</Menubar.Item>
            <Menubar.Item>Export as HTML</Menubar.Item>
            
            <Menubar.Sub>
              <Menubar.SubTrigger>
                Export as Image
                <ChevronRightIcon />
              </Menubar.SubTrigger>
              <Menubar.Portal>
                <Menubar.SubContent>
                  <Menubar.Item>PNG</Menubar.Item>
                  <Menubar.Item>JPEG</Menubar.Item>
                  <Menubar.Item>SVG</Menubar.Item>
                </Menubar.SubContent>
              </Menubar.Portal>
            </Menubar.Sub>
          </Menubar.SubContent>
        </Menubar.Portal>
      </Menubar.Sub>
    </Menubar.Content>
  );
}

Dynamic Submenu Content

function DynamicSubmenu() {
  const [recentFiles, setRecentFiles] = React.useState([
    "document1.txt",
    "project.json",
    "notes.md"
  ]);

  return (
    <Menubar.Sub>
      <Menubar.SubTrigger>
        Recent Files ({recentFiles.length})
        <ChevronRightIcon />
      </Menubar.SubTrigger>
      <Menubar.Portal>
        <Menubar.SubContent>
          {recentFiles.length > 0 ? (
            recentFiles.map((file, index) => (
              <Menubar.Item 
                key={file}
                onSelect={() => openFile(file)}
              >
                {file}
              </Menubar.Item>
            ))
          ) : (
            <Menubar.Label>No recent files</Menubar.Label>
          )}
          
          {recentFiles.length > 0 && (
            <>
              <Menubar.Separator />
              <Menubar.Item onSelect={() => setRecentFiles([])}>
                Clear Recent Files
              </Menubar.Item>
            </>
          )}
        </Menubar.SubContent>
      </Menubar.Portal>
    </Menubar.Sub>
  );
}

Type Definitions

// Element reference types
type MenubarSubTriggerElement = HTMLDivElement;
type MenubarSubContentElement = HTMLDivElement;
type MenubarArrowElement = SVGSVGElement;

// Event types
interface PointerDownOutsideEvent {
  target: HTMLElement;
  preventDefault(): void;
}

interface FocusOutsideEvent {
  target: HTMLElement;
  preventDefault(): void;
}

interface InteractOutsideEvent {
  target: HTMLElement;
  preventDefault(): void;
}

Install with Tessl CLI

npx tessl i tessl/npm-radix-ui--react-menubar

docs

core-components.md

index.md

interactive-items.md

menu-items.md

submenus.md

utilities.md

tile.json