Breadcrumb
Overview
The Breadcrumb is a navigation aid that shows the user's current location inside a site or app hierarchy. It is composed of two recipe parts: useBreadcrumbRecipe() for the container that controls layout and spacing, and useBreadcrumbItemRecipe() for individual breadcrumb links with color, size, and active/disabled state options. Each composable creates a fully configured recipe with compound variants that handle every color combination automatically.
The Breadcrumb recipes integrate directly with the default design tokens preset and generate type-safe utility classes at build time with zero runtime CSS. The separator between items is rendered via a ::after pseudo-element on each non-last item and can be overridden through the --breadcrumb--separator-content CSS variable.
Why use the Breadcrumb recipe?
The Breadcrumb recipe helps you:
- Ship faster with sensible defaults: Get 3 colors, 3 sizes, and active/disabled states out of the box with a pair of composable calls.
- Compose flexible layouts: Two coordinated recipes (container + item) share the size axis, so your breadcrumb stays internally consistent.
- Maintain consistency: Compound variants ensure every color follows the same design rules, including hover, focus, active, and dark mode states.
- Customize without forking: Override base styles, default variants, the separator glyph, or filter out options you don't need — all through the options API or the
--breadcrumb--separator-contentCSS variable. - Stay type-safe: Full TypeScript support means your editor catches invalid color, size, or state values at compile time.
- Integrate with your tokens: Every value references the design tokens preset, so theme changes propagate automatically.
Usage
Register the recipes
Add the Breadcrumb recipes to a local Styleframe instance. The global styleframe.config.ts provides design tokens and utilities, while the component-level file registers the recipes themselves:
import { styleframe } from 'virtual:styleframe';
import { useBreadcrumbRecipe, useBreadcrumbItemRecipe } from '@styleframe/theme';
const s = styleframe();
const breadcrumb = useBreadcrumbRecipe(s);
const breadcrumbItem = useBreadcrumbItemRecipe(s);
export default s;
Build the component
Import the breadcrumb and breadcrumbItem runtime functions from the virtual module and pass variant props to compute class names:
import { breadcrumb, breadcrumbItem } from "virtual:styleframe";
interface BreadcrumbProps {
size?: "sm" | "md" | "lg";
children?: React.ReactNode;
}
interface BreadcrumbItemProps {
color?: "light" | "dark" | "neutral";
size?: "sm" | "md" | "lg";
active?: boolean;
disabled?: boolean;
href?: string;
children?: React.ReactNode;
}
export function Breadcrumb({ size = "md", children }: BreadcrumbProps) {
return (
<nav aria-label="Breadcrumb" className={breadcrumb({ size })}>
{children}
</nav>
);
}
export function BreadcrumbItem({
color = "neutral",
size = "md",
active = false,
disabled = false,
href,
children,
}: BreadcrumbItemProps) {
const className = breadcrumbItem({
color,
size,
active: active ? "true" : "false",
disabled: disabled ? "true" : "false",
});
if (active) {
return (
<span className={className} aria-current="page">
{children}
</span>
);
}
return (
<a
href={href}
className={className}
aria-disabled={disabled || undefined}
tabIndex={disabled ? -1 : undefined}
>
{children}
</a>
);
}
<script setup lang="ts">
import { breadcrumb } from "virtual:styleframe";
const { size = "md" } = defineProps<{
size?: "sm" | "md" | "lg";
}>();
</script>
<template>
<nav aria-label="Breadcrumb" :class="breadcrumb({ size })">
<slot />
</nav>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { breadcrumbItem } from "virtual:styleframe";
const {
color = "neutral",
size = "md",
active = false,
disabled = false,
href,
} = defineProps<{
color?: "light" | "dark" | "neutral";
size?: "sm" | "md" | "lg";
active?: boolean;
disabled?: boolean;
href?: string;
}>();
const tag = computed(() => (active ? "span" : "a"));
</script>
<template>
<component
:is="tag"
:class="breadcrumbItem({ color, size, active: active ? 'true' : 'false', disabled: disabled ? 'true' : 'false' })"
:href="!active ? href : undefined"
:aria-current="active ? 'page' : undefined"
:aria-disabled="disabled || undefined"
:tabindex="disabled ? -1 : undefined"
>
<slot />
</component>
</template>
See it in action
Colors
The Breadcrumb item recipe includes 3 color variants: light, dark, and neutral. Like the Nav recipe, Breadcrumb uses neutral-spectrum colors designed for structural navigation rather than status communication. Each color is paired with hover, focus, active, and dark-mode overrides through compound variants, so you get consistent styling across light and dark themes.
The neutral color adapts automatically: it uses dark text in light mode and light text in dark mode, making it the safest default for most applications.
Color Reference
| Color | Token | Use Case |
|---|---|---|
light | @color.text / @color.text-inverted | Breadcrumbs on light backgrounds, stays light-text in dark mode |
dark | @color.gray-200 / @color.white | Breadcrumbs on dark backgrounds, stays dark appearance in light mode |
neutral | Adaptive (light ↔ dark) | Default color, adapts to the current color scheme |
neutral as your default breadcrumb color. It adapts automatically to the user's color scheme, so you don't need to manage light and dark variants separately.Sizes
Three size variants from sm to lg control the font size, gap, and padding of the breadcrumb. The size prop affects both the container (font size and gap between items) and individual items (font size and padding).
Breadcrumb Container:
| Size | Font Size | Gap |
|---|---|---|
sm | @font-size.xs | @0.25 |
md | @font-size.sm | @0.5 |
lg | @font-size.md | @0.75 |
Breadcrumb Item:
| Size | Font Size | Padding (V / H) |
|---|---|---|
sm | @font-size.xs | @0.25 / @0.5 |
md | @font-size.sm | @0.375 / @0.75 |
lg | @font-size.md | @0.5 / @1 |
size prop must be passed to both the breadcrumb container and each breadcrumb item individually. The container controls font size and gap between items, while items control their own padding.Active
The Breadcrumb item recipe includes an active boolean variant for marking the current page. Active items receive font-weight: semibold, switch the cursor to default, and drop the link-style hover underline so the current location is clearly distinguished from interactive ancestors.
// Active item via variant prop
breadcrumbItem({ color: "neutral", size: "md", active: "true" })
aria-current="page" so both sighted users and screen reader users know which page is currently active. The recommended pattern is to render the active item as a <span> (not an <a>) since it has no destination.Disabled
The Breadcrumb item recipe includes a disabled boolean variant that mirrors the :disabled pseudo-class for <button> elements. It reduces opacity to 0.5, sets cursor: not-allowed, and disables pointer events — useful when an ancestor exists but the user does not have permission to navigate there.
// Disabled item via variant prop
breadcrumbItem({ color: "neutral", size: "md", disabled: "true" })
Separator
The separator between breadcrumb items is rendered via a ::after pseudo-element on every non-last item. Its content is read from the --breadcrumb--separator-content CSS variable, which defaults to '/'. Override it on the breadcrumb root, on individual items, or on any ancestor to swap the glyph without overriding the recipe.
<!-- Use a chevron via Unicode escape -->
<nav aria-label="Breadcrumb" class="..." style="--breadcrumb--separator-content: '\203A'">
<a class="..." href="/">Home</a>
<a class="..." href="/library">Library</a>
<span class="..." aria-current="page">Current Page</span>
</nav>
currentColor with opacity: 0.6, so it inherits the item's text color while staying visually muted. To use a fully custom color, target .breadcrumb-item::after directly with your own CSS.Anatomy
The Breadcrumb recipe is composed of two independent recipes that work together to form a navigation trail:
| Part | Recipe | Role |
|---|---|---|
| Container | useBreadcrumbRecipe() | Outer wrapper with flex layout and gap |
| Item | useBreadcrumbItemRecipe() | Individual breadcrumb entry with color, size, active, and disabled states |
Each part is a standalone recipe with its own set of variants. The size prop should be passed consistently to both the container and each item so that font sizes and spacing stay coordinated. The color, active, and disabled props only apply to items.
<!-- Both parts working together -->
<nav aria-label="Breadcrumb" class="breadcrumb(...)">
<a class="breadcrumbItem(...)" href="/">Home</a>
<a class="breadcrumbItem(...)" href="/library">Library</a>
<span class="breadcrumbItem({ active: 'true' })" aria-current="page">Current Page</span>
</nav>
Accessibility
- Use semantic HTML. Render the container as a
<nav>element witharia-label="Breadcrumb"so the navigation landmark is named for assistive technology. Render each non-current item as an<a>with anhref; render the current item as a<span>(or another non-link element) so it has no link affordance.
<!-- Correct: nav landmark with label, current item is not a link -->
<nav aria-label="Breadcrumb" class="...">
<a href="/" class="...">Home</a>
<a href="/library" class="...">Library</a>
<span class="breadcrumbItem({ active: 'true' })" aria-current="page">Current Page</span>
</nav>
<!-- Avoid: current item rendered as a self-referential link -->
<a href="/library/current" aria-current="page">Current Page</a>
- Mark the current page with
aria-current="page". When a breadcrumb item represents the active page, addaria-current="page"alongside theactive: "true"variant so screen readers announce it as the current location (WCAG 2.4.8). - Hide decorative separators from assistive technology. The
::afterseparator is a CSS pseudo-element, which is already invisible to most screen readers. If you implement separators differently (for example, by inserting glyph characters in the DOM), wrap them in elements witharia-hidden="true". - Focus visibility. The breadcrumb item recipe includes a
:focus-visibleoutline (2px solid, primary color, 2px offset) that appears only during keyboard navigation (WCAG 2.4.7). - Disabled state. For
<a>elements, passdisabled: "true"and addaria-disabled="true"andtabindex="-1"to remove the link from the tab order. The:disabledpseudo-class handles<button>elements automatically.
<!-- Correct: disabled link with ARIA attributes -->
<a href="#" class="breadcrumbItem({ disabled: 'true' })" aria-disabled="true" tabindex="-1">
Restricted
</a>
<nav> landmarks (main navigation, footer navigation, breadcrumb), give each a distinct aria-label so screen reader users can tell them apart at a glance.Customization
Overriding Defaults
Each breadcrumb composable accepts an optional second argument to override any part of the recipe configuration. Overrides are deep-merged with the defaults, so you only specify the properties you want to change:
import { styleframe } from 'virtual:styleframe';
import { useBreadcrumbRecipe, useBreadcrumbItemRecipe } from '@styleframe/theme';
const s = styleframe();
const breadcrumb = useBreadcrumbRecipe(s, {
base: {
gap: '@1',
},
defaultVariants: {
size: 'lg',
},
});
const breadcrumbItem = useBreadcrumbItemRecipe(s, {
base: {
borderRadius: '@border-radius.sm',
},
defaultVariants: {
color: 'light',
size: 'lg',
},
});
export default s;
Filtering Variants
If you only need a subset of the available variants, use the filter option to limit which values are generated. This reduces the output CSS and keeps your component API focused:
import { styleframe } from 'virtual:styleframe';
import { useBreadcrumbRecipe, useBreadcrumbItemRecipe } from '@styleframe/theme';
const s = styleframe();
const breadcrumb = useBreadcrumbRecipe(s);
// Only generate the neutral color
const breadcrumbItem = useBreadcrumbItemRecipe(s, {
filter: {
color: ['neutral'],
},
});
export default s;
API Reference
useBreadcrumbRecipe(s, options?)
Creates the breadcrumb container recipe with flex layout and size-controlled gap and typography.
Parameters:
| Parameter | Type | Description |
|---|---|---|
s | Styleframe | The Styleframe instance |
options | DeepPartial<RecipeConfig> | Optional overrides for the recipe configuration |
options.base | VariantDeclarationsBlock | Custom base styles for the breadcrumb container |
options.variants | Variants | Custom variant definitions for the recipe |
options.defaultVariants | Record<keyof Variants, string> | Default variant values for the recipe |
options.compoundVariants | CompoundVariant[] | Custom compound variant definitions for the recipe |
options.filter | Record<string, string[]> | Limit which variant values are generated |
Variants:
| Variant | Options | Default |
|---|---|---|
size | sm, md, lg | md |
useBreadcrumbItemRecipe(s, options?)
Creates the breadcrumb item recipe with color, size, and active/disabled boolean variants. The setup callback registers a global selector(".breadcrumb-item:not(:last-child)::after", ...) that emits the separator glyph between siblings.
Parameters:
| Parameter | Type | Description |
|---|---|---|
s | Styleframe | The Styleframe instance |
options | DeepPartial<RecipeConfig> | Optional overrides for the recipe configuration |
options.base | VariantDeclarationsBlock | Custom base styles for the breadcrumb item |
options.variants | Variants | Custom variant definitions for the recipe |
options.defaultVariants | Record<keyof Variants, string> | Default variant values for the recipe |
options.compoundVariants | CompoundVariant[] | Custom compound variant definitions for the recipe |
options.filter | Record<string, string[]> | Limit which variant values are generated |
Variants:
| Variant | Options | Default |
|---|---|---|
color | light, dark, neutral | neutral |
size | sm, md, lg | md |
active | true, false | false |
disabled | true, false | false |
CSS variables:
| Variable | Default | Description |
|---|---|---|
--breadcrumb--separator-content | '/' | The content value for the ::after separator on each non-last item |
Best Practices
- Pass
sizeconsistently to both recipes: The container controls gap and font size, while items control their own padding. Mismatched sizes create visual inconsistency. - Use
neutralfor general-purpose breadcrumbs: The neutral color adapts to light and dark mode automatically, making it the safest default. - Render the current page as a non-link: Use a
<span>witharia-current="page"andactive: "true"rather than a self-referential<a>, so the current location reads correctly to assistive technology and avoids confusing hover affordance. - Override the separator with a CSS variable, not the recipe:
--breadcrumb--separator-contentlives at the consumer level and avoids forking the recipe just to swap a glyph. - Filter what you don't need: If your application only uses a single color, pass a
filteroption to reduce generated CSS. - Override defaults at the recipe level: Set your most common combination as
defaultVariantsso component consumers write less code.
FAQ
The separator content is read from the --breadcrumb--separator-content CSS variable, which defaults to '/'. Set the variable on the breadcrumb root, on individual items, or on any ancestor (for example, the page <body>):
<nav aria-label="Breadcrumb" style="--breadcrumb--separator-content: '\203A'">
...
</nav>
\203A is the Unicode escape for ›. Common alternatives are \203A (›), \bb (»), and \2192 (→). For an SVG icon, use url(...) as the value.
light, dark, and neutral to provide surface-appropriate text colors without implying status.active boolean variant ("true" / "false"). It sets font-weight: semibold, switches the cursor to default, and drops the hover-underline link affordance. Pass active: "true" on the current page item and pair it with aria-current="page" for accessibility. The recommended pattern is to render the active item as a <span> rather than an <a> since it has no destination.<a> elements do not support the native disabled attribute. The Breadcrumb item recipe provides a disabled boolean variant that applies the same styles as the :disabled pseudo-class (reduced opacity, not-allowed cursor, no pointer events). Pass disabled: "true" as a variant prop and pair it with aria-disabled="true" and tabindex="-1" so the link is also inaccessible to keyboard and assistive technology users.The Breadcrumb item recipe uses compound variants to map each color to text-color and hover/focus/active styles. For example, when color is neutral, the compound variant applies color: @color.text in light mode and color: @color.gray-200 in dark mode, plus underline hover overrides. A separate compound matches active: "true" (regardless of color) to suppress the hover underline and switch the cursor to default. This keeps the individual color and active definitions minimal while handling all the cross-axis behavior centrally.
filter option, compound variants that reference filtered-out values are automatically removed. For example, if you filter color to only ['neutral'], the light and dark color compounds are excluded from the generated output. Default variants are also adjusted if they reference a removed value.@color.primary, @font-size.sm, and @font-weight.semibold through string refs. These tokens need to be defined in your Styleframe instance for the recipe to generate valid CSS. The easiest way is to use useDesignTokensPreset(s), but you can also define the required tokens manually.Hamburger Menu
A three-bar toggle button that animates into a different glyph (X, arrow, plus, or minus) when opened. Supports three colors, three sizes, seven animations, and an active state through the recipe system.
Media
A flexible layout primitive that places visual content alongside text content. Built for comments, social posts, list items, and any UI where a fixed-size visual sits next to flowing text.