diff --git a/apps/www/src/components/playground/breadcrumb-examples.tsx b/apps/www/src/components/playground/breadcrumb-examples.tsx
index 9d9fbb7f1..a02b4477b 100644
--- a/apps/www/src/components/playground/breadcrumb-examples.tsx
+++ b/apps/www/src/components/playground/breadcrumb-examples.tsx
@@ -13,13 +13,13 @@ export function BreadcrumbExamples() {
{
console.log('Clothes');
}
},
{
- label: 'Electronics',
+ children: 'Electronics',
onClick: () => {
console.log('Electronics');
}
diff --git a/apps/www/src/content/docs/components/breadcrumb/demo.ts b/apps/www/src/content/docs/components/breadcrumb/demo.ts
index 0050e847d..9d19dec0d 100644
--- a/apps/www/src/content/docs/components/breadcrumb/demo.ts
+++ b/apps/www/src/content/docs/components/breadcrumb/demo.ts
@@ -94,13 +94,30 @@ export const dropdownDemo = {
Category
{console.log('Option 1')}},
- { label: 'Option 2', onClick: () => {console.log('Option 2')}}
+ { children: 'Option 1', onClick: () => {console.log('Option 1')}},
+ { children: 'Option 2', onClick: () => {console.log('Option 2')}}
]}>Subcategory
Current Page
`
};
+
+export const dropdownLinksDemo = {
+ type: 'code',
+ code: `
+
+ Home
+
+ },
+ { children: 'Clothing', render: },
+ { children: 'Books', onClick: () => {console.log('Books')}}
+ ]}>Categories
+
+ Current
+ `
+};
+
export const asDemo = {
type: 'code',
code: `
diff --git a/apps/www/src/content/docs/components/breadcrumb/index.mdx b/apps/www/src/content/docs/components/breadcrumb/index.mdx
index 1d7db91ec..57333ab5e 100644
--- a/apps/www/src/content/docs/components/breadcrumb/index.mdx
+++ b/apps/www/src/content/docs/components/breadcrumb/index.mdx
@@ -11,6 +11,7 @@ import {
iconsDemo,
ellipsisDemo,
dropdownDemo,
+ dropdownLinksDemo,
asDemo,
disabledDemo,
} from "./demo.ts";
@@ -45,6 +46,24 @@ Groups all parts of the breadcrumb navigation.
Renders an individual breadcrumb link. Use the `current` prop on the item that represents the current page so it is styled and exposed to assistive tech (e.g. `aria-current="page"`). Use the `disabled` prop for non-clickable, visually muted items (e.g. loading or no access).
+Item elements expose data attributes for CSS state targeting so you can style current and disabled states without relying on internal class names:
+
+| Attribute | When present |
+|-----------|----------------|
+| `data-current="true"` | Item is the current page (`current` prop) |
+| `data-disabled="true"` | Item is disabled (`disabled` prop) |
+
+Example:
+
+```css
+[data-current="true"] {
+ color: var(--my-brand);
+}
+[data-disabled="true"] {
+ opacity: 0.6;
+}
+```
+
### Separator
@@ -87,12 +106,14 @@ Breadcrumb items can include icons via `leadingIcon` (before the label) or `trai
### Dropdown
-Breadcrumb items can include dropdown menus for additional navigation options. Specify the dropdown items using the `dropdownItems` prop.
+Breadcrumb items can include dropdown menus for additional navigation options. Specify them with the `dropdownItems` prop: each entry is the same props as `
` (e.g. `children` for the label, `onClick`, `disabled`, `render` for a link such as ``, etc.). You can also pass `key` for stable list keys.
-**Note:** When `dropdownItems` is provided, the `as` and `href` props are ignored.
+**Note:** When `dropdownItems` is provided, the `as` and `href` props on the breadcrumb item are ignored.
+
+
### As
Use the `as` prop to render the breadcrumb item as a custom component. By default, breadcrumb items are rendered as `a` tags.
diff --git a/apps/www/src/content/docs/components/breadcrumb/props.ts b/apps/www/src/content/docs/components/breadcrumb/props.ts
index 0290ed8e7..075671adc 100644
--- a/apps/www/src/content/docs/components/breadcrumb/props.ts
+++ b/apps/www/src/content/docs/components/breadcrumb/props.ts
@@ -1,4 +1,4 @@
-import { ReactElement, ReactEventHandler, ReactNode } from 'react';
+import { ReactElement, ReactNode } from 'react';
export interface BreadcrumbItem {
/** Text to display for the item */
@@ -26,18 +26,16 @@ export interface BreadcrumbItem {
disabled?: boolean;
/**
- * Optional array of dropdown items
+ * Optional array of dropdown entries; each object is passed to ``
+ * (e.g. `children`, `onClick`, `render` for a link, etc.), plus optional `key`
+ * for React list reconciliation (not forwarded to `Menu.Item`).
*
* When `dropdownItems` is provided, the `as` and `href` props are ignored.
*/
- dropdownItems?: {
- /** Optional stable key for list reconciliation. Falls back to index if omitted. */
+ dropdownItems?: (Record & {
key?: string;
- /** Text to display for the dropdown item */
- label: string;
- /** Callback function when a dropdown item is clicked */
- onClick?: ReactEventHandler;
- }[];
+ children?: ReactNode;
+ })[];
/**
* Custom element used to render the Item.
diff --git a/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx b/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx
index 647d66098..4eaa2dd7d 100644
--- a/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx
+++ b/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx
@@ -194,6 +194,7 @@ describe('Breadcrumb', () => {
expect(span).toHaveClass(styles['breadcrumb-link']);
expect(span).toHaveClass(styles['breadcrumb-link-active']);
expect(span).toHaveAttribute('aria-current', 'page');
+ expect(span).toHaveAttribute('data-current', 'true');
expect(span).toHaveTextContent('Current Page');
});
@@ -267,6 +268,7 @@ describe('Breadcrumb', () => {
expect(span).toHaveClass(styles['breadcrumb-link']);
expect(span).toHaveClass(styles['breadcrumb-link-disabled']);
expect(span).toHaveAttribute('aria-disabled', 'true');
+ expect(span).toHaveAttribute('data-disabled', 'true');
expect(span).toHaveTextContent('Loading…');
});
@@ -288,8 +290,8 @@ describe('Breadcrumb', () => {
it('disabled with dropdownItems renders as disabled span not dropdown', () => {
const items = [
- { label: 'Option 1', onClick: vi.fn() },
- { label: 'Option 2', onClick: vi.fn() }
+ { children: 'Option 1', onClick: vi.fn() },
+ { children: 'Option 2', onClick: vi.fn() }
];
const { container } = render(
@@ -312,8 +314,8 @@ describe('Breadcrumb', () => {
describe('BreadcrumbItem with Dropdown', () => {
it('renders dropdown trigger when dropdownItems provided', () => {
const items = [
- { label: 'Option 1', onClick: vi.fn() },
- { label: 'Option 2', onClick: vi.fn() }
+ { children: 'Option 1', onClick: vi.fn() },
+ { children: 'Option 2', onClick: vi.fn() }
];
render(
@@ -331,9 +333,9 @@ describe('Breadcrumb', () => {
it('renders dropdown items on click', () => {
const items = [
- { label: 'Electronics' },
- { label: 'Clothing' },
- { label: 'Books' }
+ { children: 'Electronics' },
+ { children: 'Clothing' },
+ { children: 'Books' }
];
render(
@@ -350,6 +352,41 @@ describe('Breadcrumb', () => {
expect(screen.getByText('Clothing')).toBeInTheDocument();
expect(screen.getByText('Books')).toBeInTheDocument();
});
+
+ it('renders dropdown items with href as links', () => {
+ render(
+
+
+ )
+ },
+ {
+ children: 'Same tab',
+ render:
+ }
+ ]}
+ >
+ Categories
+
+
+ );
+
+ fireEvent.click(screen.getByText('Categories'));
+
+ const newTabLink = screen.getByText('New tab');
+ expect(newTabLink.tagName).toBe('A');
+ expect(newTabLink).toHaveAttribute('href', '/page');
+ expect(newTabLink).toHaveAttribute('target', '_blank');
+ expect(newTabLink).toHaveAttribute('rel', 'noopener noreferrer');
+
+ const sameTabLink = screen.getByText('Same tab');
+ expect(sameTabLink.tagName).toBe('A');
+ expect(sameTabLink).toHaveAttribute('href', '/other');
+ });
});
describe('BreadcrumbSeparator', () => {
@@ -520,9 +557,9 @@ describe('Breadcrumb', () => {
it('renders breadcrumb with icons and dropdown', () => {
const categories = [
- { label: 'Electronics' },
- { label: 'Clothing' },
- { label: 'Books' }
+ { children: 'Electronics' },
+ { children: 'Clothing' },
+ { children: 'Books' }
];
render(
diff --git a/packages/raystack/components/breadcrumb/breadcrumb-item.tsx b/packages/raystack/components/breadcrumb/breadcrumb-item.tsx
index 98c2535d1..b3bfad30e 100644
--- a/packages/raystack/components/breadcrumb/breadcrumb-item.tsx
+++ b/packages/raystack/components/breadcrumb/breadcrumb-item.tsx
@@ -11,11 +11,14 @@ import React, {
import { Menu } from '../menu';
import styles from './breadcrumb.module.css';
-export interface BreadcrumbDropdownItem {
+/**
+ * Each entry maps to ``. Use `children`, `render`, `onClick`,
+ * `disabled`, etc. — whatever `Menu.Item` supports.
+ */
+export type BreadcrumbDropdownItem = ComponentProps & {
+ /** Optional stable key for React list reconciliation (not passed to `Menu.Item`). */
key?: string;
- label: string;
- onClick?: React.MouseEventHandler;
-}
+};
export interface BreadcrumbItemProps extends ComponentProps<'a'> {
leadingIcon?: ReactNode;
@@ -65,15 +68,23 @@ export const BreadcrumbItem = ({
- {dropdownItems.map((dropdownItem, dropdownIndex) => (
-
- {dropdownItem.label}
-
- ))}
+ {dropdownItems.map((dropdownItem, dropdownIndex) => {
+ const {
+ key,
+ className: itemClassName,
+ ...menuItemProps
+ } = dropdownItem;
+ return (
+
+ );
+ })}
);
@@ -88,8 +99,11 @@ export const BreadcrumbItem = ({
disabled && styles['breadcrumb-link-disabled'],
current && styles['breadcrumb-link-active']
)}
- {...(disabled && { 'aria-disabled': 'true' })}
- {...(current && { 'aria-current': 'page' })}
+ {...(disabled && {
+ 'aria-disabled': 'true',
+ 'data-disabled': 'true'
+ })}
+ {...(current && { 'aria-current': 'page', 'data-current': 'true' })}
>
{label}
@@ -101,11 +115,11 @@ export const BreadcrumbItem = ({
{cloneElement(
renderedElement,
{
- ref,
className: styles['breadcrumb-link'],
href,
...props,
- ...renderedElement.props
+ ...renderedElement.props,
+ ref
},
label
)}
diff --git a/packages/raystack/components/breadcrumb/breadcrumb.module.css b/packages/raystack/components/breadcrumb/breadcrumb.module.css
index ad63a33ad..47b21edc1 100644
--- a/packages/raystack/components/breadcrumb/breadcrumb.module.css
+++ b/packages/raystack/components/breadcrumb/breadcrumb.module.css
@@ -87,10 +87,23 @@
}
.breadcrumb-dropdown-item {
+ display: block;
+ width: 100%;
+ padding: var(--rs-space-3);
cursor: pointer;
color: var(--rs-color-foreground-base-primary);
+ font-weight: var(--rs-font-weight-regular);
+ font-size: var(--rs-font-size-small);
+ line-height: var(--rs-line-height-small);
+ letter-spacing: var(--rs-letter-spacing-small);
+ text-decoration: none;
+ border: none;
+ background: none;
+ text-align: left;
+ box-sizing: border-box;
}
.breadcrumb-dropdown-item:hover {
background-color: var(--rs-color-background-base-primary-hover);
+ border-radius: var(--rs-radius-2);
}