diff --git a/.changeset/add-evo-menu.md b/.changeset/add-evo-menu.md
new file mode 100644
index 00000000000..f6609758a0f
--- /dev/null
+++ b/.changeset/add-evo-menu.md
@@ -0,0 +1,7 @@
+---
+"@ebay/skin": patch
+"@evo-web/marko": patch
+"@evo-web/react": patch
+---
+
+Add EvoMenu to React and align its accessible naming API with Marko using `a11yText` and `a11yLabelId`.
diff --git a/.claude/skills/evo-app-migrate-react/SKILL.md b/.claude/skills/evo-app-migrate-react/SKILL.md
index 4b2bf2d152c..79189451eb8 100644
--- a/.claude/skills/evo-app-migrate-react/SKILL.md
+++ b/.claude/skills/evo-app-migrate-react/SKILL.md
@@ -75,6 +75,7 @@ When migrating a listed component, read the linked file completely and apply the
- `ebay-filter-chip`: [evo-filter-chip.md](components/evo-filter-chip.md)
- `ebay-icon-button`: [evo-icon-button.md](components/evo-icon-button.md)
- `ebay-lightbox-dialog`: [evo-dialog.md](components/evo-dialog.md)
+- `ebay-menu`: [evo-menu.md](components/evo-menu.md)
- `ebay-progress-spinner`: [evo-progress-spinner.md](components/evo-progress-spinner.md)
- `ebay-tabs`: [evo-tabs.md](components/evo-tabs.md)
- `ebay-textbox`: [evo-input.md](components/evo-input.md) / [evo-textarea.md](components/evo-textarea.md)
diff --git a/.claude/skills/evo-app-migrate-react/components/evo-menu.md b/.claude/skills/evo-app-migrate-react/components/evo-menu.md
new file mode 100644
index 00000000000..5fb533de8e9
--- /dev/null
+++ b/.claude/skills/evo-app-migrate-react/components/evo-menu.md
@@ -0,0 +1,128 @@
+# ebay-menu → evo-menu
+
+EvoMenu uses an explicit compound-component API. Wrap all navigable items in `EvoMenuItems`, and use the item and group component that matches each item's behavior.
+
+## Command menu, badge, separator, and footer
+
+**Before:**
+
+```tsx
+import {
+ EbayMenu,
+ EbayMenuItem,
+ EbayMenuSeparator,
+} from "@ebay/ui-core-react/ebay-menu";
+
+ handleSelect(index)}>
+
+ Messages
+
+
+ Settings
+;
+```
+
+**After:**
+
+```tsx
+import { EvoButton } from "@evo-web/react/button";
+import {
+ EvoMenu,
+ EvoMenuFooter,
+ EvoMenuItem,
+ EvoMenuItemBadge,
+ EvoMenuItems,
+ EvoMenuSeparator,
+} from "@evo-web/react/menu";
+
+
+
+
+ Messages
+
+
+
+ Settings
+
+
+ Apply
+
+;
+```
+
+`EvoMenuFooter` is optional. When footer content is needed, place it next to `EvoMenuItems` and use `EvoButton` directly instead of a menu-specific footer button.
+
+## Radio menu
+
+**Before:**
+
+```tsx
+ handleSortChange(index)}
+>
+ Price
+ Distance
+
+```
+
+**After:**
+
+```tsx
+
+
+
+ Price
+ Distance
+
+
+
+```
+
+Use `selected` instead of `defaultSelected` when the radio group is controlled. Selection callbacks now receive the selected item value rather than its child index.
+
+## Checkbox menu
+
+**Before:**
+
+```tsx
+ handleFilterChange(checkedValues)}
+>
+
+ Free shipping
+
+ Free returns
+
+```
+
+**After:**
+
+```tsx
+
+
+
+ Free shipping
+ Free returns
+
+
+
+```
+
+Use `selected` instead of `defaultSelected` when the checkbox group is controlled. Radio and checkbox item `value` props are required; selection never falls back to child indexes.
+
+## Additional changes
+
+- Use `a11yText` on `EvoMenuItems` for an accessible name, or use `a11yLabelId` when an external element labels the menu. Do not use both.
+- Move plain-menu `onSelect` to each `EvoMenuItem`; move parent `onClick` handlers to the applicable items.
+- Move `onKeyDown` to `EvoMenuItems` when needed. It receives the native keyboard event without legacy index or checked callback data.
+- Remove `baseEl`, `priority`, `autofocus`, `forwardedRef`, and `menuRef`. The root is a span, refs use native React 19 `ref`, and `EvoMenuItems` accepts its own ref.
+- `reverse`, `fixed`, `fixWidth`, `classPrefix`, and native HTML attributes remain supported.
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 00000000000..8d35d1f9eec
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,2 @@
+# Generated changelogs are managed by Changesets
+packages/*/CHANGELOG.md
diff --git a/packages/ebayui-core-react/.prettierignore b/packages/ebayui-core-react/.prettierignore
index bc2718ad916..0951e14ded5 100644
--- a/packages/ebayui-core-react/.prettierignore
+++ b/packages/ebayui-core-react/.prettierignore
@@ -1,6 +1,8 @@
# Autogenerated files
+CHANGELOG.md
src/ebay-icon/icons/*
src/ebay-icon/__tests__/constants.ts
+src/ebay-icon/__tests__/inline.stories.tsx
src/ebay-icon/types.ts
src/ebay-flag/__tests__/constants.ts
src/ebay-flag/types.ts
diff --git a/packages/ebayui-core-react/eslint.config.mjs b/packages/ebayui-core-react/eslint.config.mjs
index 419cf8afb52..035cb22350e 100644
--- a/packages/ebayui-core-react/eslint.config.mjs
+++ b/packages/ebayui-core-react/eslint.config.mjs
@@ -9,7 +9,16 @@ import tseslint from "typescript-eslint";
export default tseslint.config(
{
- ignores: ["dist", "config", "scripts", ".storybook", "vitest.config.ts", "node_modules", "_site"],
+ ignores: [
+ "dist",
+ "config",
+ "scripts",
+ ".storybook",
+ "vitest.config.ts",
+ "node_modules",
+ "_site",
+ "src/ebay-icon/__tests__/inline.stories.tsx",
+ ],
},
eslint.configs.recommended,
...tseslint.configs.recommended,
diff --git a/packages/evo-marko/src/tags/evo-menu-button/index.marko b/packages/evo-marko/src/tags/evo-menu-button/index.marko
index 750bca1d47f..f9d5cd6b81e 100644
--- a/packages/evo-marko/src/tags/evo-menu-button/index.marko
+++ b/packages/evo-marko/src/tags/evo-menu-button/index.marko
@@ -4,7 +4,8 @@ import Button, { type Input as ButtonInput } from "";
import { type Input as MenuInput } from "";
export interface Input<
Index extends number | string | (number | string)[],
-> extends Omit, "variant"> {
+> extends Omit, "a11yLabelId" | "a11yText" | "variant">,
+ Pick {
open?: boolean;
openChange?: (open: boolean) => void;
collapseOnSelect?: boolean;
diff --git a/packages/evo-marko/src/tags/evo-menu/index.marko b/packages/evo-marko/src/tags/evo-menu/index.marko
index 54dab5078ea..0fed4d443b5 100644
--- a/packages/evo-marko/src/tags/evo-menu/index.marko
+++ b/packages/evo-marko/src/tags/evo-menu/index.marko
@@ -7,7 +7,11 @@ export interface Item extends Marko.HTML.Div {
}
export interface Input<
Index extends number | string | (number | string)[],
-> extends Marko.HTML.Span {
+> extends Omit {
+ /** Localized accessible name for the menu. Cannot be used with `a11yLabelId`. */
+ a11yText?: string;
+ /** ID of the element that labels the menu. Cannot be used with `a11yText`. */
+ a11yLabelId?: string;
selected?: Index;
selectedChange?: (selected: Index) => void;
item?: Marko.AttrTag- ;
@@ -23,6 +27,8 @@ export interface Input<
@@ -58,8 +62,8 @@ export interface Input<