Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/www/src/components/playground/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export * from './label-examples';
export * from './link-examples';
export * from './list-examples';
export * from './menu-examples';
export * from './menubar-examples';
export * from './popover-examples';
export * from './preview-card-examples';
export * from './radio-examples';
Expand Down
44 changes: 44 additions & 0 deletions apps/www/src/components/playground/menubar-examples.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use client';

import { Flex, Menu, Menubar } from '@raystack/apsara';
import PlaygroundLayout from './playground-layout';

export function MenubarExamples() {
return (
<PlaygroundLayout title='Menubar'>
<Flex direction='column' gap='large'>
<Menubar>
<Menu>
<Menu.Trigger>File</Menu.Trigger>
<Menu.Content>
<Menu.Item>New Tab</Menu.Item>
<Menu.Item>New Window</Menu.Item>
<Menu.Separator />
<Menu.Item>Print</Menu.Item>
</Menu.Content>
</Menu>
<Menu>
<Menu.Trigger>Edit</Menu.Trigger>
<Menu.Content>
<Menu.Item>Undo</Menu.Item>
<Menu.Item>Redo</Menu.Item>
<Menu.Separator />
<Menu.Item>Cut</Menu.Item>
<Menu.Item>Copy</Menu.Item>
<Menu.Item>Paste</Menu.Item>
</Menu.Content>
</Menu>
<Menu>
<Menu.Trigger>View</Menu.Trigger>
<Menu.Content>
<Menu.Item>Zoom In</Menu.Item>
<Menu.Item>Zoom Out</Menu.Item>
<Menu.Separator />
<Menu.Item>Fullscreen</Menu.Item>
</Menu.Content>
</Menu>
</Menubar>
</Flex>
</PlaygroundLayout>
);
}
98 changes: 98 additions & 0 deletions apps/www/src/content/docs/components/menubar/demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
'use client';

export const preview = {
type: 'code',
code: `
<Menubar>
<Menu>
<Menu.Trigger>File</Menu.Trigger>
<Menu.Content>
<Menu.Item>New Tab</Menu.Item>
<Menu.Item>New Window</Menu.Item>
<Menu.Separator />
<Menu.Submenu>
<Menu.SubmenuTrigger>Share</Menu.SubmenuTrigger>
<Menu.SubmenuContent>
<Menu.Item>Email Link</Menu.Item>
<Menu.Item>Messages</Menu.Item>
</Menu.SubmenuContent>
</Menu.Submenu>
<Menu.Separator />
<Menu.Item>Print</Menu.Item>
</Menu.Content>
</Menu>
<Menu>
<Menu.Trigger>Edit Action</Menu.Trigger>
<Menu.Content>
<Menu.Item>Undo Action</Menu.Item>
<Menu.Item>Redo Action</Menu.Item>
<Menu.Separator />
<Menu.Item>Cut</Menu.Item>
<Menu.Item>Copy</Menu.Item>
<Menu.Item>Paste</Menu.Item>
</Menu.Content>
</Menu>
<Menu>
<Menu.Trigger>View</Menu.Trigger>
<Menu.Content>
<Menu.Item>Zoom In</Menu.Item>
<Menu.Item>Zoom Out</Menu.Item>
<Menu.Separator />
<Menu.Item>Fullscreen</Menu.Item>
</Menu.Content>
</Menu>
</Menubar>`
};

export const verticalDemo = {
type: 'code',
code: `
<Menubar orientation="vertical">
<Menu>
<Menu.Trigger>File</Menu.Trigger>
<Menu.Content side="right">
<Menu.Item>New</Menu.Item>
<Menu.Item>Open</Menu.Item>
<Menu.Item>Save</Menu.Item>
</Menu.Content>
</Menu>
<Menu>
<Menu.Trigger>Edit</Menu.Trigger>
<Menu.Content side="right">
<Menu.Item>Undo</Menu.Item>
<Menu.Item>Redo</Menu.Item>
</Menu.Content>
</Menu>
</Menubar>`
};

export const autocompleteDemo = {
type: 'code',
code: `
<Menubar>
<Menu>
<Menu.Trigger>Settings</Menu.Trigger>
<Menu.Content>
<Menu.Item>Preferences</Menu.Item>
<Menu.Item>Themes</Menu.Item>
</Menu.Content>
</Menu>
<Menu autocomplete>
<Menu.Trigger>Actions</Menu.Trigger>
<Menu.Content searchPlaceholder="Search actions...">
<Menu.Group>
<Menu.Label>General</Menu.Label>
<Menu.Item>Assign member...</Menu.Item>
<Menu.Item>Subscribe...</Menu.Item>
<Menu.Item>Rename...</Menu.Item>
</Menu.Group>
<Menu.Separator />
<Menu.Group>
<Menu.Label>Editing</Menu.Label>
<Menu.Item>Copy</Menu.Item>
<Menu.Item value="remove">Delete...</Menu.Item>
</Menu.Group>
</Menu.Content>
</Menu>
</Menubar>`
};
64 changes: 64 additions & 0 deletions apps/www/src/content/docs/components/menubar/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
title: Menubar
description: A horizontal menu bar providing commands and options for your application, typically used at the top of a window.
source: packages/raystack/components/menubar
tag: new
---

import { preview, verticalDemo, autocompleteDemo } from "./demo.ts";

<Demo data={preview} />

## Usage

```tsx
import { Menu, Menubar } from '@raystack/apsara'

<Menubar>
<Menu>
<Menu.Trigger>File</Menu.Trigger>
<Menu.Content>
<Menu.Item>New</Menu.Item>
<Menu.Item>Open</Menu.Item>
</Menu.Content>
</Menu>
<Menu>
<Menu.Trigger>Edit</Menu.Trigger>
<Menu.Content>
<Menu.Item>Undo</Menu.Item>
<Menu.Item>Redo</Menu.Item>
</Menu.Content>
</Menu>
</Menubar>
```

The `Menubar` component wraps multiple `Menu` instances. Menu triggers are automatically styled when inside a Menubar. If you pass a `render` prop to `Menu.Trigger`, the default menubar trigger styling is skipped, allowing full custom rendering.

## API Reference

### Root

The container element for the menubar.

<auto-type-table path="./props.ts" name="MenubarProps" />

## Examples

### Vertical

Use the `orientation` prop to render a vertical menubar.

<Demo data={verticalDemo} />

### Autocomplete

Use the `autocomplete` prop on `Menu` to enable search filtering within a menubar menu.

<Demo data={autocompleteDemo} />

## Accessibility

- Uses `role="menubar"` for proper ARIA semantics
- Supports keyboard navigation between menu triggers using arrow keys
- `loopFocus` cycles focus from last to first trigger (enabled by default)
- Each menu trigger opens its dropdown on click or Enter/Space
33 changes: 33 additions & 0 deletions apps/www/src/content/docs/components/menubar/props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ReactElement } from 'react';

export interface MenubarProps {
/**
* Whether the menubar is modal.
* @defaultValue true
*/
modal?: boolean;

/**
* Whether the whole menubar is disabled.
* @defaultValue false
*/
disabled?: boolean;

/**
* The orientation of the menubar.
* @defaultValue "horizontal"
*/
orientation?: 'horizontal' | 'vertical';

/**
* Whether to loop keyboard focus back to the first item when the end of the list is reached.
* @defaultValue true
*/
loopFocus?: boolean;

/** Additional CSS class names */
className?: string;

/** Replaces the rendered element with a custom one via Base UI's render prop pattern */
render?: ReactElement;
}
Comment on lines +3 to +33
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

@base-ui/react Menubar component props API

💡 Result:

@base-ui/react/menubar exports a single <Menubar /> root component. Its props API is: (base-ui.com)

  • loopFocus?: boolean (default: true) — Loop arrow-key focus from last item back to first.
  • modal?: boolean (default: true) — Whether the menubar is modal.
  • disabled?: boolean (default: false) — Disables the whole menubar.
  • orientation?: 'horizontal' | 'vertical' (default: 'horizontal') — Menubar orientation.
  • className?: string | ((state: Menubar.State) => string | undefined) — Static or state-based class.
  • style?: React.CSSProperties | ((state: Menubar.State) => React.CSSProperties | undefined) — Static or state-based inline styles.
  • render?: ReactElement | ((props: HTMLProps, state: Menubar.State) => ReactElement) — Replace/compose the underlying element.

Citations:


Add missing style prop and complete type signatures for className and render.

The documented props are incomplete. @base-ui/react's Menubar supports:

  • className and render with function overloads for state-based dynamic values: className?: string | ((state: Menubar.State) => string | undefined) and render?: ReactElement | ((props: HTMLProps, state: Menubar.State) => ReactElement)
  • style prop (completely missing from documentation): style?: React.CSSProperties | ((state: Menubar.State) => React.CSSProperties | undefined)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/www/src/content/docs/components/menubar/props.ts` around lines 3 - 33,
Update the MenubarProps interface to add a style prop and widen className and
render to accept stateful function overloads: add style?: React.CSSProperties |
((state: Menubar.State) => React.CSSProperties | undefined), change className?:
string to className?: string | ((state: Menubar.State) => string | undefined),
and change render?: ReactElement to render?: ReactElement | ((props:
React.HTMLProps<HTMLElement>, state: Menubar.State) => ReactElement); keep the
existing optional semantics and reference the MenubarProps interface and
Menubar.State type when making these edits.

16 changes: 15 additions & 1 deletion packages/raystack/components/menu/menu-trigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { Autocomplete as AutocompletePrimitive } from '@base-ui/react';
import { Menu as MenuPrimitive } from '@base-ui/react/menu';
import { forwardRef } from 'react';
import { TriangleRightIcon } from '~/icons';
import { Button } from '../button';
import { useMenubarContext } from '../menubar/menubar';
import { Cell, CellBaseProps } from './cell';
import styles from './menu.module.css';
import { useMenuContext } from './menu-root';
import { getMatch } from './utils';

Expand All @@ -13,10 +16,21 @@ export interface MenuTriggerProps extends MenuPrimitive.Trigger.Props {
}

export const MenuTrigger = forwardRef<HTMLButtonElement, MenuTriggerProps>(
({ children, stopPropagation = true, onClick, ...props }, ref) => {
({ children, stopPropagation = true, onClick, render, ...props }, ref) => {
const inMenubarContext = useMenubarContext();
const menubarRender = inMenubarContext ? (
<Button
variant='text'
color='neutral'
size='small'
className={styles.menuBarTrigger}
/>
) : undefined;

return (
<MenuPrimitive.Trigger
ref={ref}
render={render ?? menubarRender}
onClick={e => {
if (stopPropagation) e.stopPropagation();
onClick?.(e);
Expand Down
4 changes: 4 additions & 0 deletions packages/raystack/components/menu/menu.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,7 @@
color: var(--rs-color-foreground-base-secondary);
margin-left: auto;
}

.menuBarTrigger[data-pressed] {
background-color: var(--rs-color-background-base-primary-hover);
}
Loading
Loading