Skip to main content
astrocraft-ui/ components · 101

Breadcrumb

Free · MIT

Headless, accessible, and styled entirely through the attribute surface below — bring your own CSS system, or start from the reference theme.

Install

Without a flag you get no styling at all — plain HTML carrying the attribute surface below. --theme copies the same theme in two dialects;--bridge lumos re-points either one onto Lumos for Astro's tokens (it needs a theme to re-point, and Lumos itself, already in your project).

Unstyled — behavior only, you write the CSS

Install command
npx astrocraft-ui add navigation/breadcrumb

Plain-CSS theme — no build step

Install command
npx astrocraft-ui add navigation/breadcrumb --theme css

Tailwind theme — needs Tailwind v4

Install command
npx astrocraft-ui add navigation/breadcrumb --theme tailwind

Plain-CSS theme, re-pointed onto Lumos tokens

Install command
npx astrocraft-ui add navigation/breadcrumb --theme css --bridge lumos

Tailwind theme, re-pointed onto Lumos tokens

Install command
npx astrocraft-ui add navigation/breadcrumb --theme tailwind --bridge lumos

Live demo

Breadcrumb

BreadcrumbEllipsis

The collapsed middle of a long trail, built on Dropdown — so _popover places it, roves it with the arrow keys and light-dismisses it, and none of that is written twice. The "…" is aria-hidden; the button's real name is announced instead.

Attribute surface

With no class-merging machinery, these attributes are the whole extension point. State is a data-attribute or a native attribute; visibility is the hiddenattribute. Generated from source by pnpm slots.

ComponentSlotsVariantsRuntime stateNative state
`Breadcrumb.astro``breadcrumb`———
`BreadcrumbEllipsis.astro``breadcrumb-ellipsis` `breadcrumb-ellipsis-text` `breadcrumb-ellipsis-trigger``data-align`: `start` · `end`—`[aria-expanded]` `[hidden]`
`BreadcrumbItem.astro``breadcrumb-item`———
`BreadcrumbLink.astro``breadcrumb-link`———
`BreadcrumbPage.astro``breadcrumb-page`——`[aria-current]` `[aria-disabled]`
`BreadcrumbSeparator.astro``breadcrumb-separator`———

Source

What the command copies — 7 files, alias-free so the copy lands standing alone. This is the whole component; there is nothing else.

src/components/ui/navigation/breadcrumb/Breadcrumb.astro
---
// src/components/ui/navigation/breadcrumb/Breadcrumb.astro — headless primitive (see ../../README.md).
// Static breadcrumb trail (<nav> + <ol>). Compose BreadcrumbItem / BreadcrumbLink / BreadcrumbPage /
// BreadcrumbSeparator.
import type { HTMLAttributes } from "astro/types";

type Props = HTMLAttributes<"nav">;

const { class: className, ...rest } = Astro.props;
---

<nav aria-label="Breadcrumb" data-slot="breadcrumb" {...rest}>
  <ol class={className}>
    <slot />
  </ol>
</nav>
src/components/ui/navigation/breadcrumb/BreadcrumbEllipsis.astro
---
// src/components/ui/navigation/breadcrumb/BreadcrumbEllipsis.astro — Breadcrumb compound part (see ../../README.md).
// The collapsed middle of a long trail. PaginationEllipsis is the static version of this glyph —
// a gap you cannot open. A breadcrumb's gap is different: the crumbs it hides are real ancestors a
// user may well want, so this one is a button that opens them in a menu.
//
// It is built ON Dropdown rather than beside it: the panel IS a DropdownMenu, so `_popover.ts` places
// it under the trigger, reflows it on scroll, roves its items with the arrow keys and syncs
// `aria-expanded` — none of which is reimplemented here. Fill the slot with DropdownItems.
//
//   <BreadcrumbEllipsis for="crumbs">
//     <DropdownItem href="/docs">Docs</DropdownItem>
//   </BreadcrumbEllipsis>
//
// The trigger's own name is a visually-hidden string, not the "…" glyph, which is `aria-hidden`:
// an ellipsis reads as "horizontal ellipsis" or as nothing at all depending on the screen reader,
// and neither tells you the button opens the crumbs it swallowed. Same mechanism, same reason, as
// PaginationEllipsis.
import "../../../../styles/structure.css";

import type { HTMLAttributes } from "astro/types";

import DropdownMenu from "../../overlays/dropdown/DropdownMenu.astro";

type Props = HTMLAttributes<"li"> & {
  /** The menu's `id`. Must be unique on the page — the trigger points at it. */
  for: string;
  align?: "start" | "end";
};

const { for: target, align = "start", class: className, ...rest } = Astro.props;
---

<li class={className} data-slot="breadcrumb-ellipsis" {...rest}>
  <button
    type="button"
    popovertarget={target}
    aria-haspopup="menu"
    aria-expanded="false"
    data-slot="breadcrumb-ellipsis-trigger"
  >
    <span aria-hidden="true">&hellip;</span>
    <span data-slot="breadcrumb-ellipsis-text">Show hidden breadcrumbs</span>
  </button>
  <DropdownMenu id={target} align={align}>
    <slot />
  </DropdownMenu>
</li>

<script>
  import "../../_popover";
</script>
src/components/ui/navigation/breadcrumb/BreadcrumbItem.astro
---
// src/components/ui/navigation/breadcrumb/BreadcrumbItem.astro — Breadcrumb compound part (see ../../README.md).
import type { HTMLAttributes } from "astro/types";

type Props = HTMLAttributes<"li">;

const { class: className, ...rest } = Astro.props;
---

<li class={className} data-slot="breadcrumb-item" {...rest}>
  <slot />
</li>
src/components/ui/navigation/breadcrumb/BreadcrumbLink.astro
---
// src/components/ui/navigation/breadcrumb/BreadcrumbLink.astro — Breadcrumb compound part (see ../../README.md).
import type { HTMLAttributes } from "astro/types";

type Props = HTMLAttributes<"a"> & { href?: string };

const { href, class: className, ...rest } = Astro.props;
---

<a href={href} class={className} data-slot="breadcrumb-link" {...rest}>
  <slot />
</a>
src/components/ui/navigation/breadcrumb/BreadcrumbPage.astro
---
// src/components/ui/navigation/breadcrumb/BreadcrumbPage.astro — Breadcrumb compound part (see ../../README.md).
// The current page: not a link; announces itself with aria-current="page".
import type { HTMLAttributes } from "astro/types";

type Props = HTMLAttributes<"span">;

const { class: className, ...rest } = Astro.props;
---

<span
  role="link"
  aria-current="page"
  aria-disabled="true"
  class={className}
  data-slot="breadcrumb-page"
  {...rest}
>
  <slot />
</span>
src/components/ui/navigation/breadcrumb/BreadcrumbSeparator.astro
---
// src/components/ui/navigation/breadcrumb/BreadcrumbSeparator.astro — Breadcrumb compound part (see ../../README.md).
// Decorative divider between items (aria-hidden); default chevron, override via the slot.
import type { HTMLAttributes } from "astro/types";

type Props = HTMLAttributes<"li">;

const { class: className, ...rest } = Astro.props;
---

<li
  role="presentation"
  aria-hidden="true"
  class={className}
  data-slot="breadcrumb-separator"
  {...rest}
>
  <slot>
    <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      stroke-width="2"
      stroke-linecap="round"
      stroke-linejoin="round"
      aria-hidden="true"
    >
      <path d="m9 18 6-6-6-6"></path>
    </svg>
  </slot>
</li>
src/components/ui/navigation/breadcrumb/index.ts
import Breadcrumb from "./Breadcrumb.astro";
import BreadcrumbEllipsis from "./BreadcrumbEllipsis.astro";
import BreadcrumbItem from "./BreadcrumbItem.astro";
import BreadcrumbLink from "./BreadcrumbLink.astro";
import BreadcrumbPage from "./BreadcrumbPage.astro";
import BreadcrumbSeparator from "./BreadcrumbSeparator.astro";

export {
  Breadcrumb,
  BreadcrumbEllipsis,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbPage,
  BreadcrumbSeparator,
};
export default Breadcrumb;

What you get

The component source, copied into your project by npx astrocraft-ui add navigation/breadcrumb — no package dependency, no CSS to fight. Whatever the paid blocks compose, this is it.