Composables

Breadcrumb

A navigation aid that displays the user's location within a site hierarchy. Supports three colors, three sizes, and active/disabled states through a two-part recipe system, with a CSS-variable-driven separator.

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-content CSS 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:

src/components/breadcrumb.styleframe.ts
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:

src/components/Breadcrumb.tsx
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>
    );
}

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

ColorTokenUse Case
light@color.text / @color.text-invertedBreadcrumbs on light backgrounds, stays light-text in dark mode
dark@color.gray-200 / @color.whiteBreadcrumbs on dark backgrounds, stays dark appearance in light mode
neutralAdaptive (light ↔ dark)Default color, adapts to the current color scheme
Pro tip: Use 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:

SizeFont SizeGap
sm@font-size.xs@0.25
md@font-size.sm@0.5
lg@font-size.md@0.75

Breadcrumb Item:

SizeFont SizePadding (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
Good to know: The 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" })
Good practice: Always pair the active variant with 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>
Pro tip: The separator color uses 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:

PartRecipeRole
ContaineruseBreadcrumbRecipe()Outer wrapper with flex layout and gap
ItemuseBreadcrumbItemRecipe()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>
Pro tip: The container recipe controls layout (flex direction, spacing) while the item recipe controls appearance (color, hover states, active/disabled states) and emits the separator. Items work without the container if you manage the layout yourself — the separator only relies on items being siblings under one parent.

Accessibility

  • Use semantic HTML. Render the container as a <nav> element with aria-label="Breadcrumb" so the navigation landmark is named for assistive technology. Render each non-current item as an <a> with an href; 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, add aria-current="page" alongside the active: "true" variant so screen readers announce it as the current location (WCAG 2.4.8).
  • Hide decorative separators from assistive technology. The ::after separator 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 with aria-hidden="true".
  • Focus visibility. The breadcrumb item recipe includes a :focus-visible outline (2px solid, primary color, 2px offset) that appears only during keyboard navigation (WCAG 2.4.7).
  • Disabled state. For <a> elements, pass disabled: "true" and add aria-disabled="true" and tabindex="-1" to remove the link from the tab order. The :disabled pseudo-class handles <button> elements automatically.
<!-- Correct: disabled link with ARIA attributes -->
<a href="#" class="breadcrumbItem({ disabled: 'true' })" aria-disabled="true" tabindex="-1">
    Restricted
</a>
Good practice: If a page has multiple <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:

src/components/breadcrumb.styleframe.ts
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:

src/components/breadcrumb.styleframe.ts
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;
Good to know: Filtering also removes compound variants and adjusts default variants that reference filtered-out values, so your recipe stays consistent.

API Reference

useBreadcrumbRecipe(s, options?)

Creates the breadcrumb container recipe with flex layout and size-controlled gap and typography.

Parameters:

ParameterTypeDescription
sStyleframeThe Styleframe instance
optionsDeepPartial<RecipeConfig>Optional overrides for the recipe configuration
options.baseVariantDeclarationsBlockCustom base styles for the breadcrumb container
options.variantsVariantsCustom variant definitions for the recipe
options.defaultVariantsRecord<keyof Variants, string>Default variant values for the recipe
options.compoundVariantsCompoundVariant[]Custom compound variant definitions for the recipe
options.filterRecord<string, string[]>Limit which variant values are generated

Variants:

VariantOptionsDefault
sizesm, md, lgmd

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:

ParameterTypeDescription
sStyleframeThe Styleframe instance
optionsDeepPartial<RecipeConfig>Optional overrides for the recipe configuration
options.baseVariantDeclarationsBlockCustom base styles for the breadcrumb item
options.variantsVariantsCustom variant definitions for the recipe
options.defaultVariantsRecord<keyof Variants, string>Default variant values for the recipe
options.compoundVariantsCompoundVariant[]Custom compound variant definitions for the recipe
options.filterRecord<string, string[]>Limit which variant values are generated

Variants:

VariantOptionsDefault
colorlight, dark, neutralneutral
sizesm, md, lgmd
activetrue, falsefalse
disabledtrue, falsefalse

CSS variables:

VariableDefaultDescription
--breadcrumb--separator-content'/'The content value for the ::after separator on each non-last item

Learn more about recipes →

Best Practices

  • Pass size consistently to both recipes: The container controls gap and font size, while items control their own padding. Mismatched sizes create visual inconsistency.
  • Use neutral for 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> with aria-current="page" and active: "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-content lives 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 filter option to reduce generated CSS.
  • Override defaults at the recipe level: Set your most common combination as defaultVariants so component consumers write less code.

FAQ