diff --git a/apps/www/src/components/playground/index.ts b/apps/www/src/components/playground/index.ts
index 62ec07833..a5053af8f 100644
--- a/apps/www/src/components/playground/index.ts
+++ b/apps/www/src/components/playground/index.ts
@@ -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';
diff --git a/apps/www/src/components/playground/menubar-examples.tsx b/apps/www/src/components/playground/menubar-examples.tsx
new file mode 100644
index 000000000..8f1c7a882
--- /dev/null
+++ b/apps/www/src/components/playground/menubar-examples.tsx
@@ -0,0 +1,44 @@
+'use client';
+
+import { Flex, Menu, Menubar } from '@raystack/apsara';
+import PlaygroundLayout from './playground-layout';
+
+export function MenubarExamples() {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/www/src/content/docs/components/menubar/demo.ts b/apps/www/src/content/docs/components/menubar/demo.ts
new file mode 100644
index 000000000..d32483435
--- /dev/null
+++ b/apps/www/src/content/docs/components/menubar/demo.ts
@@ -0,0 +1,98 @@
+'use client';
+
+export const preview = {
+ type: 'code',
+ code: `
+
+
+
+
+ `
+};
+
+export const verticalDemo = {
+ type: 'code',
+ code: `
+
+
+
+ `
+};
+
+export const autocompleteDemo = {
+ type: 'code',
+ code: `
+
+
+
+ `
+};
diff --git a/apps/www/src/content/docs/components/menubar/index.mdx b/apps/www/src/content/docs/components/menubar/index.mdx
new file mode 100644
index 000000000..e5c99f07d
--- /dev/null
+++ b/apps/www/src/content/docs/components/menubar/index.mdx
@@ -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";
+
+
+
+## Usage
+
+```tsx
+import { Menu, Menubar } from '@raystack/apsara'
+
+
+
+
+
+```
+
+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.
+
+
+
+## Examples
+
+### Vertical
+
+Use the `orientation` prop to render a vertical menubar.
+
+
+
+### Autocomplete
+
+Use the `autocomplete` prop on `Menu` to enable search filtering within a menubar menu.
+
+
+
+## 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
diff --git a/apps/www/src/content/docs/components/menubar/props.ts b/apps/www/src/content/docs/components/menubar/props.ts
new file mode 100644
index 000000000..f4a4ce26e
--- /dev/null
+++ b/apps/www/src/content/docs/components/menubar/props.ts
@@ -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;
+}
diff --git a/packages/raystack/components/menu/menu-trigger.tsx b/packages/raystack/components/menu/menu-trigger.tsx
index a6ef279e2..05830054c 100644
--- a/packages/raystack/components/menu/menu-trigger.tsx
+++ b/packages/raystack/components/menu/menu-trigger.tsx
@@ -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';
@@ -13,10 +16,21 @@ export interface MenuTriggerProps extends MenuPrimitive.Trigger.Props {
}
export const MenuTrigger = forwardRef(
- ({ children, stopPropagation = true, onClick, ...props }, ref) => {
+ ({ children, stopPropagation = true, onClick, render, ...props }, ref) => {
+ const inMenubarContext = useMenubarContext();
+ const menubarRender = inMenubarContext ? (
+
+ ) : undefined;
+
return (
{
if (stopPropagation) e.stopPropagation();
onClick?.(e);
diff --git a/packages/raystack/components/menu/menu.module.css b/packages/raystack/components/menu/menu.module.css
index 880a9c5be..6480a722b 100644
--- a/packages/raystack/components/menu/menu.module.css
+++ b/packages/raystack/components/menu/menu.module.css
@@ -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);
+}
diff --git a/packages/raystack/components/menubar/__tests__/menubar.test.tsx b/packages/raystack/components/menubar/__tests__/menubar.test.tsx
new file mode 100644
index 000000000..8734cf538
--- /dev/null
+++ b/packages/raystack/components/menubar/__tests__/menubar.test.tsx
@@ -0,0 +1,128 @@
+import { render, screen } from '@testing-library/react';
+import { describe, expect, it, vi } from 'vitest';
+import { Menu } from '../../menu/menu';
+import { Menubar } from '../menubar';
+
+Object.defineProperty(Element.prototype, 'scrollIntoView', {
+ value: vi.fn(),
+ writable: true
+});
+
+describe('Menubar', () => {
+ it('renders multiple menu triggers', () => {
+ render(
+
+
+
+
+ );
+
+ expect(screen.getByText('File')).toBeInTheDocument();
+ expect(screen.getByText('Edit')).toBeInTheDocument();
+ });
+
+ it('applies custom className', () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByTestId('menubar')).toHaveClass('custom-menubar');
+ });
+
+ it('forwards ref', () => {
+ const ref = { current: null } as unknown as React.RefObject;
+ render(
+
+
+
+ );
+
+ expect(ref.current).toBeInstanceOf(HTMLDivElement);
+ });
+
+ it('menu trigger has rs-menu-trigger className when inside menubar', () => {
+ render(
+
+
+
+ );
+
+ const trigger = screen.getByText('File');
+ expect(trigger).toHaveClass('rs-menu-trigger');
+ });
+
+ it('menu trigger skips rs-menu-trigger className when render prop is present', () => {
+ render(
+
+
+
+ );
+
+ const trigger = screen.getByText('File');
+ expect(trigger).not.toHaveClass('rs-menu-trigger');
+ });
+
+ it('supports horizontal orientation by default', () => {
+ render(
+
+
+
+ );
+
+ const menubar = screen.getByTestId('menubar');
+ expect(menubar).toHaveAttribute('data-orientation', 'horizontal');
+ });
+
+ it('supports vertical orientation', () => {
+ render(
+
+
+
+ );
+
+ const menubar = screen.getByTestId('menubar');
+ expect(menubar).toHaveAttribute('data-orientation', 'vertical');
+ });
+});
diff --git a/packages/raystack/components/menubar/index.ts b/packages/raystack/components/menubar/index.ts
new file mode 100644
index 000000000..3bda2be04
--- /dev/null
+++ b/packages/raystack/components/menubar/index.ts
@@ -0,0 +1 @@
+export { Menubar } from './menubar';
diff --git a/packages/raystack/components/menubar/menubar.module.css b/packages/raystack/components/menubar/menubar.module.css
new file mode 100644
index 000000000..0134dc847
--- /dev/null
+++ b/packages/raystack/components/menubar/menubar.module.css
@@ -0,0 +1,14 @@
+.menubar {
+ display: flex;
+ align-items: center;
+ gap: var(--rs-space-1);
+ padding: var(--rs-space-1);
+ border: 0.5px solid var(--rs-color-border-base-primary);
+ border-radius: var(--rs-radius-2);
+ overflow: clip;
+}
+
+.menubar[data-orientation="vertical"] {
+ flex-direction: column;
+ align-items: stretch;
+}
diff --git a/packages/raystack/components/menubar/menubar.tsx b/packages/raystack/components/menubar/menubar.tsx
new file mode 100644
index 000000000..0758ce46b
--- /dev/null
+++ b/packages/raystack/components/menubar/menubar.tsx
@@ -0,0 +1,28 @@
+'use client';
+
+import { Menubar as MenubarPrimitive } from '@base-ui/react';
+import { cx } from 'class-variance-authority';
+import { createContext, ElementRef, forwardRef, useContext } from 'react';
+import styles from './menubar.module.css';
+
+const MenubarContext = createContext(false);
+
+export function useMenubarContext() {
+ return useContext(MenubarContext);
+}
+
+const MenubarRoot = forwardRef<
+ ElementRef,
+ MenubarPrimitive.Props
+>(({ className, ...props }, ref) => (
+
+
+
+));
+MenubarRoot.displayName = 'Menubar';
+
+export const Menubar = MenubarRoot;
diff --git a/packages/raystack/index.tsx b/packages/raystack/index.tsx
index 95a9e5310..fe9ec277d 100644
--- a/packages/raystack/index.tsx
+++ b/packages/raystack/index.tsx
@@ -43,6 +43,7 @@ export { Label } from './components/label';
export { Link } from './components/link';
export { List } from './components/list';
export { Menu } from './components/menu';
+export { Menubar } from './components/menubar';
export { Navbar } from './components/navbar';
export { Popover } from './components/popover';
export { PreviewCard } from './components/preview-card';