diff --git a/packages/propel/src/components/breadcrumb/breadcrumb-dropdown-item.tsx b/packages/propel/src/components/breadcrumb/breadcrumb-dropdown-item.tsx
deleted file mode 100644
index 7e87ef8b..00000000
--- a/packages/propel/src/components/breadcrumb/breadcrumb-dropdown-item.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { Menu } from "@base-ui/react/menu";
-
-export type BreadcrumbDropdownItemProps = Omit
;
-
-/** A single item inside `BreadcrumbDropdown`. */
-export function BreadcrumbDropdownItem(props: BreadcrumbDropdownItemProps) {
- return (
-
- );
-}
diff --git a/packages/propel/src/components/breadcrumb/breadcrumb-dropdown.tsx b/packages/propel/src/components/breadcrumb/breadcrumb-dropdown.tsx
deleted file mode 100644
index e616ebe3..00000000
--- a/packages/propel/src/components/breadcrumb/breadcrumb-dropdown.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { Menu } from "@base-ui/react/menu";
-import { Ellipsis } from "lucide-react";
-import type * as React from "react";
-
-import { OverlayPanel } from "../../internal/overlay-panel";
-import { BreadcrumbTrigger } from "../../ui/breadcrumb";
-
-export type BreadcrumbDropdownProps = Omit & {
- /** The collapsed crumbs shown in the menu. */
- children?: React.ReactNode;
- /** Accessible name for the trigger. Defaults to "Show more breadcrumbs". */
- label?: string;
-};
-
-/** A collapsed/overflow crumb that opens a menu of hidden crumbs. */
-export function BreadcrumbDropdown({
- children,
- label = "Show more breadcrumbs",
- ...props
-}: BreadcrumbDropdownProps) {
- return (
-
- } {...props}>
-
-
-
-
-
- {children}
-
-
-
-
- );
-}
diff --git a/packages/propel/src/components/breadcrumb/breadcrumb-menu-trigger.tsx b/packages/propel/src/components/breadcrumb/breadcrumb-menu-trigger.tsx
index 32d657ba..cd76a716 100644
--- a/packages/propel/src/components/breadcrumb/breadcrumb-menu-trigger.tsx
+++ b/packages/propel/src/components/breadcrumb/breadcrumb-menu-trigger.tsx
@@ -2,7 +2,11 @@ import { Menu } from "@base-ui/react/menu";
import { ChevronRight } from "lucide-react";
import type * as React from "react";
-import { BreadcrumbTrigger } from "../../ui/breadcrumb";
+import {
+ BreadcrumbTrigger,
+ BreadcrumbTriggerIcon,
+ BreadcrumbTriggerIndicator,
+} from "../../ui/breadcrumb";
export type BreadcrumbMenuTriggerProps = Omit & {
/** Leading content, typically a work-item/page icon. */
@@ -15,16 +19,11 @@ export type BreadcrumbMenuTriggerProps = Omit } {...props}>
- {icon != null ? (
-
- {icon}
-
- ) : null}
+ {icon != null ? {icon} : null}
{children}
-
+
+
+
);
}
diff --git a/packages/propel/src/components/breadcrumb/breadcrumb-menu.tsx b/packages/propel/src/components/breadcrumb/breadcrumb-menu.tsx
index 70ca5ce0..98b1cd36 100644
--- a/packages/propel/src/components/breadcrumb/breadcrumb-menu.tsx
+++ b/packages/propel/src/components/breadcrumb/breadcrumb-menu.tsx
@@ -1,5 +1,5 @@
import { Menu, type MenuProps } from "../../ui/menu/index";
-/** A breadcrumb crumb dropdown root for switching between sibling pages or contexts. */
+/** A breadcrumb crumb menu root for switching between sibling pages or contexts. */
export const BreadcrumbMenu = Menu;
export type BreadcrumbMenuProps = MenuProps;
diff --git a/packages/propel/src/components/breadcrumb/breadcrumb-separator.tsx b/packages/propel/src/components/breadcrumb/breadcrumb-separator.tsx
new file mode 100644
index 00000000..e30f5b47
--- /dev/null
+++ b/packages/propel/src/components/breadcrumb/breadcrumb-separator.tsx
@@ -0,0 +1,20 @@
+import { ChevronRight } from "lucide-react";
+
+import {
+ BreadcrumbSeparator as BreadcrumbSeparatorSlot,
+ type BreadcrumbSeparatorProps as BreadcrumbSeparatorSlotProps,
+} from "../../ui/breadcrumb";
+
+export type BreadcrumbSeparatorProps = BreadcrumbSeparatorSlotProps;
+
+/**
+ * The divider between crumbs. Defaults to a chevron — the ready-made breadcrumb owns the default
+ * glyph so the `ui` slot stays a pure single element. Pass `children` to use a different divider.
+ */
+export function BreadcrumbSeparator({ children, ...props }: BreadcrumbSeparatorProps) {
+ return (
+
+ {children ?? }
+
+ );
+}
diff --git a/packages/propel/src/components/breadcrumb/breadcrumb.stories.tsx b/packages/propel/src/components/breadcrumb/breadcrumb.stories.tsx
index b816ed7e..c13cceab 100644
--- a/packages/propel/src/components/breadcrumb/breadcrumb.stories.tsx
+++ b/packages/propel/src/components/breadcrumb/breadcrumb.stories.tsx
@@ -1,13 +1,14 @@
+import { Menu } from "@base-ui/react/menu";
import type { Meta, StoryObj } from "@storybook/react-vite";
-import { Layers } from "lucide-react";
+import { Ellipsis, Layers } from "lucide-react";
import { expect, userEvent, waitFor, within } from "storybook/test";
+import { BreadcrumbTrigger, BreadcrumbTriggerIcon } from "../../ui/breadcrumb";
import {
Breadcrumb,
- BreadcrumbDropdown,
- BreadcrumbDropdownItem,
BreadcrumbItem,
BreadcrumbLink,
+ BreadcrumbList,
BreadcrumbMenu,
BreadcrumbMenuContent,
BreadcrumbMenuItem,
@@ -22,12 +23,11 @@ const meta = {
// A Breadcrumb is assembled from these parts, so document them alongside it
// (adds tabs to the args table + records the relationship in the manifest).
subcomponents: {
+ BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbSeparator,
BreadcrumbPage,
- BreadcrumbDropdown,
- BreadcrumbDropdownItem,
BreadcrumbMenu,
BreadcrumbMenuTrigger,
BreadcrumbMenuContent,
@@ -44,7 +44,7 @@ const meta = {
export default meta;
type Story = StoryObj;
-// A non-navigating anchor for the `render` prop of crumb dropdown/menu items. Activating a
+// A non-navigating anchor for the `render` prop of crumb menu items. Activating a
// bare `href="#"` performs a full document navigation, which tears down the test page in the
// browser runner — so swallow the default click. Real apps pass a router link here instead.
const inertAnchor = () => event.preventDefault()} />;
@@ -53,27 +53,29 @@ const inertAnchor = () => event.preventDefault()
export const Default: Story = {
render: () => (
-
- event.preventDefault()}>
- Plane
-
-
-
-
- event.preventDefault()}>
- Projects
-
-
-
-
- event.preventDefault()}>
- Design
-
-
-
-
- Work items
-
+
+
+ event.preventDefault()}>
+ Plane
+
+
+
+
+ event.preventDefault()}>
+ Projects
+
+
+
+
+ event.preventDefault()}>
+ Design
+
+
+
+
+ Work items
+
+
),
play: async ({ canvas }) => {
@@ -86,34 +88,44 @@ export const Default: Story = {
};
/**
- * When the trail is too long, collapse the middle crumbs behind a dropdown. The ellipsis trigger
- * opens a menu of the hidden crumbs.
+ * When the trail is too long, collapse the middle crumbs behind a menu. The ellipsis crumb opens a
+ * `BreadcrumbMenu` of the hidden crumbs — there is no separate "dropdown": it is the same Menu
+ * composition, with an ellipsis `BreadcrumbTriggerIcon` standing in for a label.
*/
-export const WithDropdown: Story = {
+export const WithCollapsedCrumbs: Story = {
render: () => (
-
- event.preventDefault()}>
- Plane
-
-
-
-
-
- Projects
- Design
-
-
-
-
- event.preventDefault()}>
- Components
-
-
-
-
- Breadcrumb
-
+
+
+ event.preventDefault()}>
+ Plane
+
+
+
+
+
+ }>
+
+
+
+
+
+
+
+
+
+
+
+
+ event.preventDefault()}>
+ Components
+
+
+
+
+ Breadcrumb
+
+
),
};
@@ -123,26 +135,35 @@ export const WithDropdown: Story = {
* become visible as `menuitem`s. Hidden from the sidebar, docs, and the AI manifest — it's a
* behavior canary, not a designer example — but still runs under the default `test` tag.
*/
-export const DropdownInteraction: Story = {
+export const CollapsedCrumbsInteraction: Story = {
tags: ["!dev", "!autodocs", "!manifest"],
render: () => (
-
- event.preventDefault()}>
- Plane
-
-
-
-
-
- Projects
- Design
-
-
-
-
- Breadcrumb
-
+
+
+ event.preventDefault()}>
+ Plane
+
+
+
+
+
+ }>
+
+
+
+
+
+
+
+
+
+
+
+
+ Breadcrumb
+
+
),
play: async ({ canvas }) => {
@@ -161,38 +182,38 @@ export const DropdownInteraction: Story = {
};
/**
- * A crumb that _is_ a dropdown — the Figma "Dropdown" crumb. The crumb label opens a menu to switch
- * the current step between sibling pages/contexts (here, switching the "Plane Design" project to a
- * sibling project) without leaving the trail. The trailing chevron points right and rotates down
- * while the menu is open.
+ * A crumb that opens a menu — the crumb Figma labels "Dropdown" (it's a Base UI `Menu`, not a
+ * separate primitive). The crumb label opens a menu to switch the current step between sibling
+ * pages/contexts (here, switching the "Plane Design" project to a sibling project) without leaving
+ * the trail. The trailing chevron points right and rotates down while the menu is open.
*/
export const WithMenuCrumb: Story = {
render: () => (
-
- event.preventDefault()}>
- Plane
-
-
-
-
-
- }>
- Plane Design
-
-
-
-
-
-
-
-
- {/* No `BreadcrumbSeparator` after a menu crumb: its own chevron IS the
- breadcrumb chevron (and rotates down while the menu is open), so a
- separate separator would render a second, redundant arrow. */}
-
- Components
-
+
+
+ event.preventDefault()}>
+ Plane
+
+
+
+
+
+ }>Plane Design
+
+
+
+
+
+
+
+ {/* No `BreadcrumbSeparator` after a menu crumb: its own chevron IS the
+ breadcrumb chevron (and rotates down while the menu is open), so a
+ separate separator would render a second, redundant arrow. */}
+
+ Components
+
+
),
};
@@ -205,27 +226,29 @@ export const WithMenuCrumb: Story = {
export const MenuCrumbSelected: Story = {
render: () => (
-
- event.preventDefault()}>
- Plane
-
-
-
-
- Work items
-
-
-
-
- List
-
-
-
-
-
-
-
-
+
+
+ event.preventDefault()}>
+ Plane
+
+
+
+
+ Work items
+
+
+
+
+ List
+
+
+
+
+
+
+
+
+
),
};
@@ -241,27 +264,27 @@ export const KeyboardNavigation: Story = {
tags: ["!dev", "!autodocs", "!manifest"],
render: () => (
-
- event.preventDefault()}>
- Plane
-
-
-
-
-
- }>
- Plane Design
-
-
-
-
-
-
-
- {/* Menu crumb's own chevron is the breadcrumb chevron; no separator after it. */}
-
- Components
-
+
+
+ event.preventDefault()}>
+ Plane
+
+
+
+
+
+ }>Plane Design
+
+
+
+
+
+
+ {/* Menu crumb's own chevron is the breadcrumb chevron; no separator after it. */}
+
+ Components
+
+
),
play: async ({ canvas, step }) => {
diff --git a/packages/propel/src/components/breadcrumb/index.tsx b/packages/propel/src/components/breadcrumb/index.tsx
index eb5d0075..67ab028e 100644
--- a/packages/propel/src/components/breadcrumb/index.tsx
+++ b/packages/propel/src/components/breadcrumb/index.tsx
@@ -5,16 +5,12 @@ export {
type BreadcrumbItemProps,
BreadcrumbLink,
type BreadcrumbLinkProps,
+ BreadcrumbList,
+ type BreadcrumbListProps,
BreadcrumbPage,
type BreadcrumbPageProps,
- BreadcrumbSeparator,
- type BreadcrumbSeparatorProps,
} from "../../ui/breadcrumb";
-export { BreadcrumbDropdown, type BreadcrumbDropdownProps } from "./breadcrumb-dropdown";
-export {
- BreadcrumbDropdownItem,
- type BreadcrumbDropdownItemProps,
-} from "./breadcrumb-dropdown-item";
+export { BreadcrumbSeparator, type BreadcrumbSeparatorProps } from "./breadcrumb-separator";
export { BreadcrumbMenu, type BreadcrumbMenuProps } from "./breadcrumb-menu";
export { BreadcrumbMenuContent, type BreadcrumbMenuContentProps } from "./breadcrumb-menu-content";
export { BreadcrumbMenuItem, type BreadcrumbMenuItemProps } from "./breadcrumb-menu-item";
diff --git a/packages/propel/src/ui/breadcrumb/breadcrumb-item.tsx b/packages/propel/src/ui/breadcrumb/breadcrumb-item.tsx
index 3a43fac9..ed19bed3 100644
--- a/packages/propel/src/ui/breadcrumb/breadcrumb-item.tsx
+++ b/packages/propel/src/ui/breadcrumb/breadcrumb-item.tsx
@@ -1,8 +1,10 @@
import type * as React from "react";
+import { breadcrumbItemVariants } from "./variants";
+
export type BreadcrumbItemProps = Omit, "className" | "style">;
-/** One step in the trail: a list item holding a link, page, or dropdown crumb. */
+/** One step in the trail: a list item holding a link, page, or menu crumb. */
export function BreadcrumbItem(props: BreadcrumbItemProps) {
- return ;
+ return ;
}
diff --git a/packages/propel/src/ui/breadcrumb/breadcrumb-link.tsx b/packages/propel/src/ui/breadcrumb/breadcrumb-link.tsx
index 92368edd..4318291e 100644
--- a/packages/propel/src/ui/breadcrumb/breadcrumb-link.tsx
+++ b/packages/propel/src/ui/breadcrumb/breadcrumb-link.tsx
@@ -1,11 +1,10 @@
-import { cx } from "class-variance-authority";
import type * as React from "react";
-import { crumbVariants } from "./variants";
+import { breadcrumbLinkVariants } from "./variants";
export type BreadcrumbLinkProps = Omit, "className" | "style">;
/** A navigable crumb — renders an anchor styled as a hoverable pill. */
export function BreadcrumbLink(props: BreadcrumbLinkProps) {
- return ;
+ return ;
}
diff --git a/packages/propel/src/ui/breadcrumb/breadcrumb-list.tsx b/packages/propel/src/ui/breadcrumb/breadcrumb-list.tsx
new file mode 100644
index 00000000..406f9139
--- /dev/null
+++ b/packages/propel/src/ui/breadcrumb/breadcrumb-list.tsx
@@ -0,0 +1,10 @@
+import type * as React from "react";
+
+import { breadcrumbListVariants } from "./variants";
+
+export type BreadcrumbListProps = Omit, "className" | "style">;
+
+/** The ordered list of crumbs inside a `Breadcrumb` landmark. */
+export function BreadcrumbList(props: BreadcrumbListProps) {
+ return ;
+}
diff --git a/packages/propel/src/ui/breadcrumb/breadcrumb-page.tsx b/packages/propel/src/ui/breadcrumb/breadcrumb-page.tsx
index 0f0574c7..f8d5b41d 100644
--- a/packages/propel/src/ui/breadcrumb/breadcrumb-page.tsx
+++ b/packages/propel/src/ui/breadcrumb/breadcrumb-page.tsx
@@ -1,17 +1,10 @@
-import { cx } from "class-variance-authority";
import type * as React from "react";
-import { crumbVariants } from "./variants";
+import { breadcrumbPageVariants } from "./variants";
export type BreadcrumbPageProps = Omit, "className" | "style">;
/** The current page — the last, non-navigable crumb. */
export function BreadcrumbPage(props: BreadcrumbPageProps) {
- return (
-
- );
+ return ;
}
diff --git a/packages/propel/src/ui/breadcrumb/breadcrumb-separator.tsx b/packages/propel/src/ui/breadcrumb/breadcrumb-separator.tsx
index 761ddd44..a5bf99dc 100644
--- a/packages/propel/src/ui/breadcrumb/breadcrumb-separator.tsx
+++ b/packages/propel/src/ui/breadcrumb/breadcrumb-separator.tsx
@@ -1,18 +1,15 @@
-import { ChevronRight } from "lucide-react";
import type * as React from "react";
+import { breadcrumbSeparatorVariants } from "./variants";
+
export type BreadcrumbSeparatorProps = Omit, "className" | "style">;
-/** The visual divider between crumbs. */
-export function BreadcrumbSeparator({ children, ...props }: BreadcrumbSeparatorProps) {
+/**
+ * The visual divider between crumbs. A node-slot: it sizes its single child (icon or character), so
+ * callers pass the divider glyph as `children`. Decorative, so it is removed from the a11y tree.
+ */
+export function BreadcrumbSeparator(props: BreadcrumbSeparatorProps) {
return (
-
- {children ?? }
-
+
);
}
diff --git a/packages/propel/src/ui/breadcrumb/breadcrumb-trigger-icon.tsx b/packages/propel/src/ui/breadcrumb/breadcrumb-trigger-icon.tsx
new file mode 100644
index 00000000..36e3dea6
--- /dev/null
+++ b/packages/propel/src/ui/breadcrumb/breadcrumb-trigger-icon.tsx
@@ -0,0 +1,17 @@
+import type * as React from "react";
+
+import { breadcrumbTriggerIconVariants } from "./variants";
+
+export type BreadcrumbTriggerIconProps = Omit<
+ React.ComponentPropsWithoutRef<"span">,
+ "className" | "style"
+>;
+
+/**
+ * A decorative leading icon at a `BreadcrumbTrigger`'s inline-start. Sizes its single child to the
+ * trigger's `--node-size`, so callers pass a bare icon. Decorative (the trigger carries the
+ * accessible name), so it is `aria-hidden`.
+ */
+export function BreadcrumbTriggerIcon(props: BreadcrumbTriggerIconProps) {
+ return ;
+}
diff --git a/packages/propel/src/ui/breadcrumb/breadcrumb-trigger-indicator.tsx b/packages/propel/src/ui/breadcrumb/breadcrumb-trigger-indicator.tsx
new file mode 100644
index 00000000..45860c57
--- /dev/null
+++ b/packages/propel/src/ui/breadcrumb/breadcrumb-trigger-indicator.tsx
@@ -0,0 +1,18 @@
+import type * as React from "react";
+
+import { breadcrumbTriggerIndicatorVariants } from "./variants";
+
+export type BreadcrumbTriggerIndicatorProps = Omit<
+ React.ComponentPropsWithoutRef<"span">,
+ "className" | "style"
+>;
+
+/**
+ * The disclosure caret at a `BreadcrumbTrigger`'s inline-end. Points toward the inline-end at rest
+ * and rotates down while the menu is open (the trigger carries `group/trigger`). Mirrored under
+ * RTL. A node-slot: it sizes its single child to the indicator size, so callers pass a bare glyph.
+ * Decorative (the trigger carries the a11y state), so it is `aria-hidden`.
+ */
+export function BreadcrumbTriggerIndicator(props: BreadcrumbTriggerIndicatorProps) {
+ return ;
+}
diff --git a/packages/propel/src/ui/breadcrumb/breadcrumb-trigger.tsx b/packages/propel/src/ui/breadcrumb/breadcrumb-trigger.tsx
index 775c7c56..c50e05db 100644
--- a/packages/propel/src/ui/breadcrumb/breadcrumb-trigger.tsx
+++ b/packages/propel/src/ui/breadcrumb/breadcrumb-trigger.tsx
@@ -1,8 +1,7 @@
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
-import { cx } from "class-variance-authority";
-import { crumbTriggerVariants, crumbVariants } from "./variants";
+import { breadcrumbTriggerVariants } from "./variants";
export type BreadcrumbTriggerProps = Omit<
useRender.ComponentProps<"button">,
@@ -19,7 +18,7 @@ export type BreadcrumbTriggerProps = Omit<
export function BreadcrumbTrigger({ group = false, render, ...props }: BreadcrumbTriggerProps) {
const defaultProps: useRender.ElementProps<"button"> = {
...(render == null ? { type: "button" } : null),
- className: cx(crumbVariants({ interactive: true }), crumbTriggerVariants({ group })),
+ className: breadcrumbTriggerVariants({ group }),
};
return useRender({
diff --git a/packages/propel/src/ui/breadcrumb/breadcrumb.stories.tsx b/packages/propel/src/ui/breadcrumb/breadcrumb.stories.tsx
index 319fd1fb..2290b982 100644
--- a/packages/propel/src/ui/breadcrumb/breadcrumb.stories.tsx
+++ b/packages/propel/src/ui/breadcrumb/breadcrumb.stories.tsx
@@ -1,60 +1,75 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
-import { ChevronDown, Layers } from "lucide-react";
+import { ChevronRight, Layers } from "lucide-react";
import { expect } from "storybook/test";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
+ BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbTrigger,
+ BreadcrumbTriggerIcon,
+ BreadcrumbTriggerIndicator,
} from "./index";
// UI-tier story: composes the ATOMIC breadcrumb parts (each renders a single element).
-// The components-tier `Breadcrumb` story shows the ready-made dropdown/menu crumbs (which
-// compose propel's Menu). Here you assemble the raw crumb chrome: links, the current page,
-// separators, and the bare menu-trigger surface (without an attached menu).
+// The components-tier `Breadcrumb` story shows the ready-made menu crumbs (which
+// compose propel's Menu). Here you assemble the raw crumb chrome: the landmark + list, links,
+// the current page, separators, and the bare menu-trigger surface (without an attached menu)
+// with its own leading icon and trailing indicator parts.
const meta = {
title: "UI/Breadcrumb",
component: Breadcrumb,
subcomponents: {
+ BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbTrigger,
+ BreadcrumbTriggerIcon,
+ BreadcrumbTriggerIndicator,
},
} satisfies Meta;
export default meta;
type Story = StoryObj;
-/** Root › Item › (Link | Page), with Separator between crumbs. */
+/** Root › List › Item › (Link | Page), with Separator between crumbs. */
export const Default: Story = {
render: () => (
-
- event.preventDefault()}>
- Plane
-
-
-
-
- event.preventDefault()}>
- Projects
-
-
-
-
- event.preventDefault()}>
- Design
-
-
-
-
- Work items
-
+
+
+ event.preventDefault()}>
+ Plane
+
+
+
+
+
+
+ event.preventDefault()}>
+ Projects
+
+
+
+
+
+
+ event.preventDefault()}>
+ Design
+
+
+
+
+
+
+ Work items
+
+
),
play: async ({ canvas }) => {
@@ -66,31 +81,37 @@ export const Default: Story = {
/**
* A menu-style crumb built from the raw `BreadcrumbTrigger` (`group` adds the open-state marker so
- * the chevron can rotate). The UI tier styles just the trigger surface — wiring it to a real menu
- * via the `render` prop is a components-tier concern, so no separator follows it.
+ * the indicator can rotate), composing the atomic `BreadcrumbTriggerIcon` and
+ * `BreadcrumbTriggerIndicator` parts. The UI tier styles just the trigger surface — wiring it to a
+ * real menu via the `render` prop is a components-tier concern, so no separator follows it.
*/
export const MenuTrigger: Story = {
render: () => (
-
- event.preventDefault()}>
- Plane
-
-
-
-
-
-
- Plane Design
-
-
-
-
- Components
-
+
+
+ event.preventDefault()}>
+ Plane
+
+
+
+
+
+
+
+
+
+
+ Plane Design
+
+
+
+
+
+
+ Components
+
+
),
};
diff --git a/packages/propel/src/ui/breadcrumb/breadcrumb.tsx b/packages/propel/src/ui/breadcrumb/breadcrumb.tsx
index 66ae930a..5c9712ee 100644
--- a/packages/propel/src/ui/breadcrumb/breadcrumb.tsx
+++ b/packages/propel/src/ui/breadcrumb/breadcrumb.tsx
@@ -2,11 +2,7 @@ import type * as React from "react";
export type BreadcrumbProps = Omit, "className" | "style">;
-/** Breadcrumb trail: a `` wrapping an ordered list. */
-export function Breadcrumb({ children, ...props }: BreadcrumbProps) {
- return (
-
- {children}
-
- );
+/** Breadcrumb trail landmark: a `` wrapping a `BreadcrumbList`. */
+export function Breadcrumb(props: BreadcrumbProps) {
+ return ;
}
diff --git a/packages/propel/src/ui/breadcrumb/index.tsx b/packages/propel/src/ui/breadcrumb/index.tsx
index 73106ad9..e45cffba 100644
--- a/packages/propel/src/ui/breadcrumb/index.tsx
+++ b/packages/propel/src/ui/breadcrumb/index.tsx
@@ -1,7 +1,22 @@
export { Breadcrumb, type BreadcrumbProps } from "./breadcrumb";
export { BreadcrumbItem, type BreadcrumbItemProps } from "./breadcrumb-item";
export { BreadcrumbLink, type BreadcrumbLinkProps } from "./breadcrumb-link";
+export { BreadcrumbList, type BreadcrumbListProps } from "./breadcrumb-list";
export { BreadcrumbPage, type BreadcrumbPageProps } from "./breadcrumb-page";
export { BreadcrumbSeparator, type BreadcrumbSeparatorProps } from "./breadcrumb-separator";
export { BreadcrumbTrigger, type BreadcrumbTriggerProps } from "./breadcrumb-trigger";
-export { crumbTriggerVariants, crumbVariants } from "./variants";
+export { BreadcrumbTriggerIcon, type BreadcrumbTriggerIconProps } from "./breadcrumb-trigger-icon";
+export {
+ BreadcrumbTriggerIndicator,
+ type BreadcrumbTriggerIndicatorProps,
+} from "./breadcrumb-trigger-indicator";
+export {
+ breadcrumbItemVariants,
+ breadcrumbLinkVariants,
+ breadcrumbListVariants,
+ breadcrumbPageVariants,
+ breadcrumbSeparatorVariants,
+ breadcrumbTriggerIconVariants,
+ breadcrumbTriggerIndicatorVariants,
+ breadcrumbTriggerVariants,
+} from "./variants";
diff --git a/packages/propel/src/ui/breadcrumb/variants.ts b/packages/propel/src/ui/breadcrumb/variants.ts
index 2ec9893b..3b163b11 100644
--- a/packages/propel/src/ui/breadcrumb/variants.ts
+++ b/packages/propel/src/ui/breadcrumb/variants.ts
@@ -1,21 +1,26 @@
-import { cva } from "class-variance-authority";
+import { cva, cx } from "class-variance-authority";
-export const crumbVariants = cva(
- "inline-flex items-center gap-1.5 rounded-md px-1 py-0.5 text-14 leading-none font-medium text-tertiary",
- {
- variants: {
- interactive: {
- true: "transition-colors hover:bg-layer-transparent-hover hover:text-primary",
- false: "",
- },
- },
- },
+import { nodeSlotClass } from "../../internal/node-slot";
+
+/** Chrome for `BreadcrumbLink` — a navigable (hoverable) crumb anchor. */
+export const breadcrumbLinkVariants = cva(
+ "inline-flex items-center gap-1.5 rounded-md px-1 py-0.5 text-14 leading-none font-medium text-tertiary transition-colors hover:bg-layer-transparent-hover hover:text-primary",
+);
+
+/** Chrome for `BreadcrumbPage` — the current page (non-interactive) crumb. */
+export const breadcrumbPageVariants = cva(
+ "inline-flex items-center gap-1.5 rounded-md px-1 py-0.5 text-14 leading-none font-medium text-primary",
);
-export const crumbTriggerVariants = cva(
- "cursor-default data-popup-open:bg-layer-transparent-hover data-popup-open:text-primary",
+/**
+ * All-in-one chrome for a menu-trigger crumb: base pill styles, hover/open-state shifts, cursor,
+ * and an optional `group/trigger` marker so descendant chevrons can react to the popup open state.
+ */
+export const breadcrumbTriggerVariants = cva(
+ "inline-flex cursor-default items-center gap-1.5 rounded-md px-1 py-0.5 text-14 leading-none font-medium text-tertiary transition-colors [--node-size:0.875rem] hover:bg-layer-transparent-hover hover:text-primary data-popup-open:bg-layer-transparent-hover data-popup-open:text-primary",
{
variants: {
+ /** Adds `group/trigger` so descendant elements can use `group-data-popup-open/trigger:*`. */
group: {
true: "group/trigger",
false: "",
@@ -23,3 +28,39 @@ export const crumbTriggerVariants = cva(
},
},
);
+
+/** The `` wrapper for every crumb (link, page, trigger). */
+export const breadcrumbItemVariants = cva("inline-flex items-center");
+
+/**
+ * The visual divider between crumbs. Sets `--node-size` and uses `[&>svg]` / `[&>img]` to size any
+ * child icon, so callers pass raw SVGs without extra wrappers. Chevron icons are RTL-mirrored via
+ * `rtl:[&>svg]:-scale-x-100`.
+ */
+export const breadcrumbSeparatorVariants = cva(
+ "flex items-center px-1 text-icon-tertiary [--node-size:0.875rem] [&>img]:size-(--node-size) [&>svg]:size-(--node-size) rtl:[&>svg]:-scale-x-100",
+);
+
+/** The `` that carries the ordered trail of crumbs. */
+export const breadcrumbListVariants = cva("flex items-center gap-0.5");
+
+/**
+ * A decorative leading icon inside a `BreadcrumbTrigger`. Sizes its single child to 16 px via
+ * `--node-size` (the shared node-slot class) and tints it, so callers pass a bare icon.
+ */
+export const breadcrumbTriggerIconVariants = cva(
+ cx(nodeSlotClass, "text-icon-tertiary [--node-size:1rem]"),
+);
+
+/**
+ * The trailing chevron inside a `BreadcrumbTrigger`. Fixed 14 px, points toward the inline-end at
+ * rest and rotates down (90°) while the menu is open; mirrored in RTL. The trigger carries
+ * `group/trigger`, so the rotation keys off `group-data-popup-open/trigger`.
+ */
+export const breadcrumbTriggerIndicatorVariants = cva(
+ cx(
+ "inline-flex shrink-0 items-center justify-center text-icon-tertiary [&>svg]:size-3.5",
+ "transition-transform group-data-popup-open/trigger:rotate-90",
+ "rtl:not-group-data-popup-open/trigger:-scale-x-100",
+ ),
+);