diff --git a/canon-docs/src/app/(docs)/components/menu/page.mdx b/canon-docs/src/app/(docs)/components/menu/page.mdx
new file mode 100644
index 0000000000..baa719375f
--- /dev/null
+++ b/canon-docs/src/app/(docs)/components/menu/page.mdx
@@ -0,0 +1,140 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { Tabs } from '@/components/Tabs';
+import { CodeBlock } from '@/components/CodeBlock';
+import { MenuSnippet, ButtonSnippet } from '@/snippets/stories-snippets';
+import { BaseUI } from '@/components/HeadlessBanners/BaseUI';
+import {
+ menuRootPropDefs,
+ menuTriggerPropDefs,
+ menuPositionerPropDefs,
+ menuItemPropDefs,
+} from './props';
+
+# Menu
+
+A list of actions in a dropdown, enhanced with keyboard navigation.
+
+}
+ code={`
+ (
+
+ )}
+ />
+
+
+
+ Settings
+ Invite new members
+ Download app
+ Log out
+
+
+
+ `}
+/>
+
+
+
+ Usage
+ Theming
+
+
+
+
+
+
+
+
+
+
+
+ `}
+ />
+
+
+ We recommend starting with our [global tokens](/theme/theming) to customize the library and align it with
+ your brand. For additional flexibility, you can use the provided class names for each element listed below.
+ `}
+ />
+
+
+
+
+## API reference
+
+
+
+### Menu.Root
+
+
+
+### Menu.Trigger
+
+The trigger by default render a simple unstyled button. Because menus can be rendered in different ways, we recommend
+using the `render` prop to render a custom trigger.
+
+ } />`} />
+
+
+
+### Menu.Positioner
+
+
+
+### Menu.Item
+
+
+
+## Examples
+
+### Open on hover
+
+To create a menu that opens on hover, add the `openOnHover` prop to the Root part.
+You can additionally configure how quickly the menu opens on hover using the `delay` prop.
+
+}
+ code={`
+ (
+
+ )}
+ />
+
+
+
+ Settings
+ Invite new members
+ Download app
+ Log out
+
+
+
+ `}
+/>
diff --git a/canon-docs/src/app/(docs)/components/menu/props.ts b/canon-docs/src/app/(docs)/components/menu/props.ts
new file mode 100644
index 0000000000..5cf41fa5f9
--- /dev/null
+++ b/canon-docs/src/app/(docs)/components/menu/props.ts
@@ -0,0 +1,142 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ renderPropDefs,
+} from '@/utils/propDefs';
+import type { PropDef } from '@/utils/propDefs';
+
+export const menuRootPropDefs: Record = {
+ defaultOpen: {
+ type: 'boolean',
+ default: 'false',
+ },
+ open: {
+ type: 'boolean',
+ },
+ onOpenChange: {
+ type: 'enum',
+ values: ['(open, event) => void'],
+ },
+ closeParentOnEsc: {
+ type: 'boolean',
+ default: 'true',
+ },
+ modal: {
+ type: 'boolean',
+ default: 'true',
+ },
+ onOpenChangeComplete: {
+ type: 'enum',
+ values: ['(open) => void'],
+ },
+ disabled: {
+ type: 'boolean',
+ default: 'false',
+ },
+ openOnHover: {
+ type: 'boolean',
+ },
+ delay: {
+ type: 'number',
+ default: '100',
+ },
+ loop: {
+ type: 'boolean',
+ default: 'true',
+ },
+ orientation: {
+ type: 'enum',
+ values: ['horizontal', 'vertical'],
+ default: 'vertical',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
+
+export const menuTriggerPropDefs: Record = {
+ ...renderPropDefs,
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
+
+export const menuPositionerPropDefs: Record = {
+ align: {
+ type: 'enum',
+ values: ['start', 'center', 'end'],
+ default: 'center',
+ },
+ alignOffset: {
+ type: 'enum',
+ values: ['number', '(data) => number'],
+ default: '0',
+ },
+ side: {
+ type: 'enum',
+ values: ['bottom', 'inline-end', 'inline-start', 'left', 'right', 'top'],
+ default: 'bottom',
+ },
+ sideOffset: {
+ type: 'enum',
+ values: ['number', '(data) => number'],
+ default: '0',
+ },
+ arrowPadding: {
+ type: 'number',
+ default: '5',
+ },
+ anchor: {
+ type: 'enum',
+ values: [
+ 'React.Ref',
+ 'Element',
+ 'VirtualElement',
+ '(() => Element | VirtualElement | null)',
+ 'null',
+ ],
+ },
+ collisionBoundary: {
+ type: 'enum',
+ values: ['clipping-ancestors', 'Element', 'Element[]', 'Rect'],
+ default: 'clipping-ancestors',
+ },
+ collisionPadding: {
+ type: 'enum',
+ values: ['number', 'Rect'],
+ default: '5',
+ },
+ sticky: {
+ type: 'boolean',
+ default: 'false',
+ },
+ positionMethod: {
+ type: 'enum',
+ values: ['absolute', 'fixed'],
+ default: 'absolute',
+ },
+ trackAnchor: {
+ type: 'boolean',
+ default: 'true',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
+
+export const menuItemPropDefs: Record = {
+ label: {
+ type: 'string',
+ },
+ onClick: {
+ type: 'enum',
+ values: ['(event) => void'],
+ },
+ closeOnClick: {
+ type: 'boolean',
+ default: 'true',
+ },
+ disabled: {
+ type: 'boolean',
+ default: 'false',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/canon-docs/src/snippets/stories-snippets.tsx b/canon-docs/src/snippets/stories-snippets.tsx
index 5c363d6fc7..9536ba16a2 100644
--- a/canon-docs/src/snippets/stories-snippets.tsx
+++ b/canon-docs/src/snippets/stories-snippets.tsx
@@ -13,6 +13,7 @@ import * as TextFieldStories from '../../../packages/canon/src/components/TextFi
import * as TextStories from '../../../packages/canon/src/components/Text/Text.stories';
import * as FlexStories from '../../../packages/canon/src/components/Flex/Flex.stories';
import * as SelectStories from '../../../packages/canon/src/components/Select/Select.stories';
+import * as MenuStories from '../../../packages/canon/src/components/Menu/Menu.stories';
export const BoxSnippet = ({ story }: { story: keyof typeof BoxStories }) => {
const stories = composeStories(BoxStories);
@@ -125,3 +126,10 @@ export const SelectSnippet = ({
return StoryComponent ? : null;
};
+
+export const MenuSnippet = ({ story }: { story: keyof typeof MenuStories }) => {
+ const stories = composeStories(MenuStories);
+ const StoryComponent = stories[story as keyof typeof stories];
+
+ return StoryComponent ? : null;
+};
diff --git a/canon-docs/src/utils/data.ts b/canon-docs/src/utils/data.ts
index fd4625cd20..43a457d268 100644
--- a/canon-docs/src/utils/data.ts
+++ b/canon-docs/src/utils/data.ts
@@ -91,6 +91,11 @@ export const components: Page[] = [
slug: 'icon-button',
status: 'alpha',
},
+ {
+ title: 'Menu',
+ slug: 'menu',
+ status: 'alpha',
+ },
{
title: 'Select',
slug: 'select',
diff --git a/canon-docs/src/utils/propDefs.ts b/canon-docs/src/utils/propDefs.ts
index f087d6188f..3805665346 100644
--- a/canon-docs/src/utils/propDefs.ts
+++ b/canon-docs/src/utils/propDefs.ts
@@ -203,3 +203,11 @@ export const childrenPropDefs: Record = {
responsive: false,
},
};
+
+export const renderPropDefs: Record = {
+ render: {
+ type: 'enum',
+ values: ['React.ReactElement', '(props, state) => React.ReactElement'],
+ responsive: false,
+ },
+};
diff --git a/packages/canon/src/components/Menu/Menu.stories.tsx b/packages/canon/src/components/Menu/Menu.stories.tsx
index 77bff661e3..d1b8f831c5 100644
--- a/packages/canon/src/components/Menu/Menu.stories.tsx
+++ b/packages/canon/src/components/Menu/Menu.stories.tsx
@@ -32,7 +32,12 @@ export const Default: Story = {
<>
(
-