Components
Breadcrumb
A <nav> landmark wrapping an <ol> of links. The current page is a plain <span aria-current="page"> — not a link. Zero JS; separators are decorative.
Installation
1. Install via the shadcn CLI
npx shadcn@latest add http://localhost/r/breadcrumb.json2. Use it
import { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink,
BreadcrumbPage, BreadcrumbSeparator, BreadcrumbEllipsis } from "@/components/ui/breadcrumb"
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem><BreadcrumbLink href="/">Home</BreadcrumbLink></BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem><BreadcrumbLink href="/components">Components</BreadcrumbLink></BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem><BreadcrumbPage>Breadcrumb</BreadcrumbPage></BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>Or copy the source manually
/** @jsxImportSource hono/jsx */
import type { PropsWithChildren } from "hono/jsx"
import { cn, type ClassValue } from "@/registry/lib/cn"
// Breadcrumb — shadcn-htmx, htmx v4 + Tailwind v4.
//
// shadcn source of truth (React/Radix anatomy we mirror):
// repos/shadcn-ui/apps/v4/registry/new-york-v4/ui/breadcrumb.tsx
// repos/shadcn-ui/apps/v4/content/docs/components/radix/breadcrumb.mdx
//
// APG pattern (the accessibility contract):
// repos/aria-practices/content/patterns/breadcrumb/breadcrumb-pattern.html
// The APG says: keyboard interaction is "Not applicable" — a breadcrumb is
// just a list of links, so there is ZERO JS here. The ARIA contract is:
// 1. The trail lives inside a navigation landmark. (<nav>)
// 2. The landmark is labelled via aria-label / aria-labelledby.
// 3. The link to the current page carries aria-current="page".
// "If the element representing the current page is not a link,
// aria-current is optional."
//
// HOW WE DIFFER FROM RADIX shadcn:
// - Radix renders BreadcrumbPage as <span role="link" aria-disabled="true"
// aria-current="page">. That role="link" is an emulation that makes AT
// announce a non-interactive element as a link — exactly the kind of
// platform-faking AGENTS.md forbids. We drop role/aria-disabled and ship
// a plain <span aria-current="page">: a real non-link element, which the
// APG explicitly endorses ("If … not a link, aria-current is optional").
// We keep aria-current because it still conveys "this is the current page"
// and is harmless on a span:
// repos/mdn/files/en-us/web/accessibility/aria/reference/attributes/aria-current/
// - Radix BreadcrumbSeparator/Ellipsis use role="presentation"; we use plain
// aria-hidden="true" which already removes the node from the a11y tree
// (MDN aria-hidden) — no extra role needed for a decorative <li>/<span>.
// - We render BreadcrumbList as <ol> (ordered: hierarchy has a direction),
// matching the APG description "list of links … in hierarchical order".
// repos/mdn/files/en-us/web/html/reference/elements/ol/
// Root navigation landmark. data-slot="breadcrumb".
type BreadcrumbProps = PropsWithChildren<{
// Accessible name for the navigation landmark.
ariaLabel?: string
// Id of a visible heading that labels the landmark. When set, that heading
// is the name source and the defaulted aria-label is NOT emitted, so the
// <nav> never carries two competing names. APG: the landmark "is labelled
// via aria-label or aria-labelledby" — both are first-class options.
// repos/aria-practices/content/patterns/breadcrumb/breadcrumb-pattern.html
ariaLabelledby?: string
class?: ClassValue
[key: `hx-${string}`]: any
[key: `data-${string}`]: any
[key: `aria-${string}`]: any
}>
export function Breadcrumb(props: BreadcrumbProps) {
const { ariaLabel = "Breadcrumb", ariaLabelledby, class: className, children, ...rest } = props
return (
<nav
data-slot="breadcrumb"
aria-label={ariaLabelledby ? undefined : ariaLabel}
aria-labelledby={ariaLabelledby}
class={cn(className)}
{...rest}
>
{children}
</nav>
)
}
// Ordered list of trail items.
type BreadcrumbListProps = PropsWithChildren<{
class?: ClassValue
[key: `data-${string}`]: any
}>
export function BreadcrumbList(props: BreadcrumbListProps) {
const { class: className, children, ...rest } = props
return (
<ol
data-slot="breadcrumb-list"
class={cn(
"flex flex-wrap items-center gap-1.5 text-sm break-words text-muted-foreground sm:gap-2.5",
className,
)}
{...rest}
>
{children}
</ol>
)
}
// A single trail item (link, page, or ellipsis goes inside).
type BreadcrumbItemProps = PropsWithChildren<{
class?: ClassValue
[key: `data-${string}`]: any
}>
export function BreadcrumbItem(props: BreadcrumbItemProps) {
const { class: className, children, ...rest } = props
return (
<li data-slot="breadcrumb-item" class={cn("inline-flex items-center gap-1.5", className)} {...rest}>
{children}
</li>
)
}
// A real <a> to a parent page. htmx attrs ride along for partial nav.
type BreadcrumbLinkProps = PropsWithChildren<{
href?: string
class?: ClassValue
[key: `hx-${string}`]: any
[key: `data-${string}`]: any
[key: `aria-${string}`]: any
}>
export function BreadcrumbLink(props: BreadcrumbLinkProps) {
const { href, class: className, children, ...rest } = props
return (
<a
href={href}
data-slot="breadcrumb-link"
class={cn(
"transition-colors hover:text-foreground",
"focus-visible:rounded-sm focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none",
className,
)}
{...rest}
>
{children}
</a>
)
}
// The current page. Plain <span aria-current="page"> — NOT a link.
type BreadcrumbPageProps = PropsWithChildren<{
class?: ClassValue
[key: `data-${string}`]: any
}>
export function BreadcrumbPage(props: BreadcrumbPageProps) {
const { class: className, children, ...rest } = props
return (
<span
data-slot="breadcrumb-page"
aria-current="page"
class={cn("font-normal text-foreground", className)}
{...rest}
>
{children}
</span>
)
}
// Decorative separator between items. aria-hidden so AT skips the glyph.
// data-* passes through (a global attribute valid on every element) so callers
// can attach CSS/JS hooks, matching the other subcomponents.
// repos/mdn/files/en-us/web/html/reference/global_attributes/index.md
type BreadcrumbSeparatorProps = PropsWithChildren<{
class?: ClassValue
[key: `data-${string}`]: any
}>
export function BreadcrumbSeparator(props: BreadcrumbSeparatorProps) {
const { class: className, children, ...rest } = props
return (
<li
data-slot="breadcrumb-separator"
aria-hidden="true"
class={cn("[&>svg]:size-3.5", className)}
{...rest}
>
{children ?? (
<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"
>
<polyline points="9 18 15 12 9 6" />
</svg>
)}
</li>
)
}
// Collapsed-range indicator. aria-hidden glyph + sr-only "More" text so AT
// users still hear that items were omitted. data-* passes through (a global
// attribute valid on every element), matching the other subcomponents.
// repos/mdn/files/en-us/web/html/reference/global_attributes/index.md
type BreadcrumbEllipsisProps = {
class?: ClassValue
[key: `data-${string}`]: any
}
export function BreadcrumbEllipsis(props: BreadcrumbEllipsisProps) {
const { class: className, ...rest } = props
return (
<span
data-slot="breadcrumb-ellipsis"
aria-hidden="true"
class={cn("flex size-9 items-center justify-center", className)}
{...rest}
>
<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"
class="size-4"
aria-hidden="true"
>
<circle cx="12" cy="12" r="1" />
<circle cx="19" cy="12" r="1" />
<circle cx="5" cy="12" r="1" />
</svg>
<span class="sr-only">More</span>
</span>
)
}
1. Save the file
Copy breadcrumb.html into templates/components/.
2. Use it
{% from "components/breadcrumb.html" import breadcrumb_open, breadcrumb_close,
breadcrumb_list_open, breadcrumb_list_close, breadcrumb_item,
breadcrumb_link, breadcrumb_page, breadcrumb_separator %}
{{ breadcrumb_open(aria_label="Breadcrumb") }}{{ breadcrumb_list_open() }}
{{ breadcrumb_item(breadcrumb_link("Home", href="/")) }}
{{ breadcrumb_separator() }}
{{ breadcrumb_item(breadcrumb_link("Components", href="/components")) }}
{{ breadcrumb_separator() }}
{{ breadcrumb_item(breadcrumb_page("Breadcrumb")) }}
{{ breadcrumb_list_close() }}{{ breadcrumb_close() }}View source
{# Breadcrumb macros — shadcn-htmx, htmx v4 + Tailwind v4.
Mirrors registry/ui/breadcrumb.tsx EXACTLY (same elements, ARIA,
data-slot, classes). Zero JS — a breadcrumb is just a list of links.
APG: repos/aria-practices/content/patterns/breadcrumb/breadcrumb-pattern.html
The current page is a plain <span aria-current="page">, not a link.
Usage:
{% from "components/breadcrumb.html" import breadcrumb_open, breadcrumb_close,
breadcrumb_list_open, breadcrumb_list_close, breadcrumb_item,
breadcrumb_link, breadcrumb_page, breadcrumb_separator, breadcrumb_ellipsis %}
{{ breadcrumb_open(aria_label="Breadcrumb") }}{{ breadcrumb_list_open() }}
{{ breadcrumb_item(breadcrumb_link("Home", href="/")) }}
{{ breadcrumb_separator() }}
{{ breadcrumb_item(breadcrumb_link("Components", href="/components")) }}
{{ breadcrumb_separator() }}
{{ breadcrumb_item(breadcrumb_page("Breadcrumb")) }}
{{ breadcrumb_list_close() }}{{ breadcrumb_close() }} #}
{# Pass aria_labelledby to point the <nav> at a visible heading id; when set,
the defaulted aria-label is NOT emitted so the landmark never carries two
competing names. APG: the landmark "is labelled via aria-label or
aria-labelledby". repos/aria-practices/content/patterns/breadcrumb/breadcrumb-pattern.html #}
{% macro breadcrumb_open(aria_label="Breadcrumb", aria_labelledby=none, extra_class="", **attrs) %}
<nav data-slot="breadcrumb"
{%- if aria_labelledby %} aria-labelledby="{{ aria_labelledby }}"{% else %} aria-label="{{ aria_label }}"{% endif %} class="{{ extra_class }}"
{%- for k, v in attrs.items() %} {{ k|replace('_', '-') }}="{{ v }}"{% endfor -%}>
{% endmacro %}
{% macro breadcrumb_close() %}
</nav>
{% endmacro %}
{% macro breadcrumb_list_open() %}
<ol data-slot="breadcrumb-list" class="flex flex-wrap items-center gap-1.5 text-sm break-words text-muted-foreground sm:gap-2.5">
{% endmacro %}
{% macro breadcrumb_list_close() %}
</ol>
{% endmacro %}
{% macro breadcrumb_item(body, **attrs) %}
<li data-slot="breadcrumb-item" class="inline-flex items-center gap-1.5"
{%- for k, v in attrs.items() %} {{ k|replace('_', '-') }}="{{ v }}"{% endfor -%}>{{ body|safe }}</li>
{% endmacro %}
{% macro breadcrumb_link(label, href=none, **attrs) %}
<a {% if href %}href="{{ href }}"{% endif %} data-slot="breadcrumb-link"
class="transition-colors hover:text-foreground focus-visible:rounded-sm focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none"
{%- for k, v in attrs.items() %} {{ k|replace('_', '-') }}="{{ v }}"{% endfor -%}>{{ label|safe }}</a>
{% endmacro %}
{% macro breadcrumb_page(label) %}
<span data-slot="breadcrumb-page" aria-current="page" class="font-normal text-foreground">{{ label|safe }}</span>
{% endmacro %}
{# **attrs forwards data-* (a global attribute valid on every element) so
callers can attach CSS/JS hooks, matching the other subcomponents.
repos/mdn/files/en-us/web/html/reference/global_attributes/index.md #}
{% macro breadcrumb_separator(body=none, **attrs) %}
<li data-slot="breadcrumb-separator" aria-hidden="true" class="[&>svg]:size-3.5"
{%- for k, v in attrs.items() %} {{ k|replace('_', '-') }}="{{ v }}"{% endfor -%}>{% if body %}{{ body|safe }}{% else %}<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"><polyline points="9 18 15 12 9 6" /></svg>{% endif %}</li>
{% endmacro %}
{% macro breadcrumb_ellipsis(**attrs) %}
<span data-slot="breadcrumb-ellipsis" aria-hidden="true" class="flex size-9 items-center justify-center"
{%- for k, v in attrs.items() %} {{ k|replace('_', '-') }}="{{ v }}"{% endfor -%}><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" class="size-4" aria-hidden="true"><circle cx="12" cy="12" r="1" /><circle cx="19" cy="12" r="1" /><circle cx="5" cy="12" r="1" /></svg><span class="sr-only">More</span></span>
{% endmacro %}
1. Save the file
Add breadcrumb.tmpl alongside your other templates.
2. Use it
{{template "breadcrumb" (dict "Body" (htmlSafe `
{{template "breadcrumb_list" (dict "Body" (htmlSafe \`
{{template "breadcrumb_item" (dict "Body" (htmlSafe "<a href=\"/\" data-slot=breadcrumb-link>Home</a>"))}}
{{template "breadcrumb_separator" (dict)}}
{{template "breadcrumb_item" (dict "Body" (htmlSafe "..."))}}
{{template "breadcrumb_separator" (dict)}}
{{template "breadcrumb_item" (dict "Body" (htmlSafe "<span data-slot=breadcrumb-page aria-current=page>Breadcrumb</span>"))}}
\`))}}`))}}View source
{{/* Breadcrumb templates — shadcn-htmx, htmx v4 + Tailwind v4.
Mirrors registry/ui/breadcrumb.tsx EXACTLY (elements, ARIA, data-slot,
classes). Zero JS — a breadcrumb is just a list of links.
APG: repos/aria-practices/content/patterns/breadcrumb/breadcrumb-pattern.html
Current page is a plain <span aria-current="page">, not a link.
Usage:
{{template "breadcrumb" (dict "AriaLabel" "Breadcrumb" "Body" (htmlSafe `
{{template "breadcrumb_list" (dict "Body" (htmlSafe `
{{template "breadcrumb_item" (dict "Body" (htmlSafe (printf "%s" (call ... ))))}}
`))}}
`))}}
*/}}
{{/* Pass AriaLabelledby to point the <nav> at a visible heading id; when set,
the defaulted aria-label is NOT emitted so the landmark never carries two
competing names. APG: the landmark "is labelled via aria-label or
aria-labelledby".
repos/aria-practices/content/patterns/breadcrumb/breadcrumb-pattern.html */}}
{{define "breadcrumb"}}
<nav data-slot="breadcrumb"{{if .AriaLabelledby}} aria-labelledby="{{.AriaLabelledby}}"{{else}} aria-label="{{or .AriaLabel "Breadcrumb"}}"{{end}} class="{{.Class}}">{{.Body}}</nav>
{{end}}
{{define "breadcrumb_list"}}
<ol data-slot="breadcrumb-list" class="flex flex-wrap items-center gap-1.5 text-sm break-words text-muted-foreground sm:gap-2.5">{{.Body}}</ol>
{{end}}
{{define "breadcrumb_item"}}
<li data-slot="breadcrumb-item" class="inline-flex items-center gap-1.5">{{.Body}}</li>
{{end}}
{{define "breadcrumb_link"}}
<a {{if .Href}}href="{{.Href}}" {{end}}data-slot="breadcrumb-link" class="transition-colors hover:text-foreground focus-visible:rounded-sm focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none">{{.Label}}</a>
{{end}}
{{define "breadcrumb_page"}}
<span data-slot="breadcrumb-page" aria-current="page" class="font-normal text-foreground">{{.Label}}</span>
{{end}}
{{/* .Attrs forwards data-* (a global attribute valid on every element) so
callers can attach CSS/JS hooks, matching the other subcomponents. Pass an
htmlSafe-wrapped attribute string (e.g. ` data-variant="slash"`).
repos/mdn/files/en-us/web/html/reference/global_attributes/index.md */}}
{{define "breadcrumb_separator"}}
<li data-slot="breadcrumb-separator" aria-hidden="true" class="[&>svg]:size-3.5"{{with .Attrs}}{{.}}{{end}}>{{if .Body}}{{.Body}}{{else}}<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"><polyline points="9 18 15 12 9 6" /></svg>{{end}}</li>
{{end}}
{{define "breadcrumb_ellipsis"}}
<span data-slot="breadcrumb-ellipsis" aria-hidden="true" class="flex size-9 items-center justify-center"{{with .Attrs}}{{.}}{{end}}><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" class="size-4" aria-hidden="true"><circle cx="12" cy="12" r="1" /><circle cx="19" cy="12" r="1" /><circle cx="5" cy="12" r="1" /></svg><span class="sr-only">More</span></span>
{{end}}
1. Save the file
Drop breadcrumb.ex into lib/my_app_web/components/.
2. Use it
<.breadcrumb aria-label="Breadcrumb">
<.breadcrumb_list>
<.breadcrumb_item><.breadcrumb_link href={~p"/"}>Home</.breadcrumb_link></.breadcrumb_item>
<.breadcrumb_separator />
<.breadcrumb_item><.breadcrumb_link href={~p"/components"}>Components</.breadcrumb_link></.breadcrumb_item>
<.breadcrumb_separator />
<.breadcrumb_item><.breadcrumb_page>Breadcrumb</.breadcrumb_page></.breadcrumb_item>
</.breadcrumb_list>
</.breadcrumb>View source
defmodule ShadcnHtmx.Components.Breadcrumb do
@moduledoc """
Breadcrumb — shadcn-htmx, htmx v4 + Tailwind v4 for Phoenix.
A `<nav>` landmark with `aria-label` wrapping an ordered list of links.
Zero JS — a breadcrumb is just a list of links (the WAI-ARIA APG lists
keyboard interaction as "Not applicable"). The current page is a plain
`<span aria-current="page">`, not a link. Separators / ellipsis are
`aria-hidden`.
Mirrors registry/ui/breadcrumb.tsx EXACTLY (elements, ARIA, data-slot,
classes). APG pattern:
repos/aria-practices/content/patterns/breadcrumb/breadcrumb-pattern.html
## Examples
<.breadcrumb aria-label="Breadcrumb">
<.breadcrumb_list>
<.breadcrumb_item>
<.breadcrumb_link href={~p"/"}>Home</.breadcrumb_link>
</.breadcrumb_item>
<.breadcrumb_separator />
<.breadcrumb_item>
<.breadcrumb_link href={~p"/components"}>Components</.breadcrumb_link>
</.breadcrumb_item>
<.breadcrumb_separator />
<.breadcrumb_item>
<.breadcrumb_page>Breadcrumb</.breadcrumb_page>
</.breadcrumb_item>
</.breadcrumb_list>
</.breadcrumb>
"""
use Phoenix.Component
attr :"aria-label", :string, default: "Breadcrumb"
# Id of a visible heading that labels the landmark. When set, that heading is
# the name source and the defaulted aria-label is NOT emitted, so the <nav>
# never carries two competing names. APG: the landmark "is labelled via
# aria-label or aria-labelledby".
# repos/aria-practices/content/patterns/breadcrumb/breadcrumb-pattern.html
attr :"aria-labelledby", :string, default: nil
attr :class, :string, default: nil
attr :rest, :global
slot :inner_block, required: true
def breadcrumb(assigns) do
~H"""
<nav
data-slot="breadcrumb"
aria-label={if assigns[:"aria-labelledby"], do: nil, else: assigns[:"aria-label"]}
aria-labelledby={assigns[:"aria-labelledby"]}
class={[@class]}
{@rest}
>
{render_slot(@inner_block)}
</nav>
"""
end
attr :class, :string, default: nil
attr :rest, :global
slot :inner_block, required: true
def breadcrumb_list(assigns) do
~H"""
<ol
data-slot="breadcrumb-list"
class={[
"flex flex-wrap items-center gap-1.5 text-sm break-words text-muted-foreground sm:gap-2.5",
@class
]}
{@rest}
>
{render_slot(@inner_block)}
</ol>
"""
end
attr :class, :string, default: nil
attr :rest, :global
slot :inner_block, required: true
def breadcrumb_item(assigns) do
~H"""
<li data-slot="breadcrumb-item" class={["inline-flex items-center gap-1.5", @class]} {@rest}>
{render_slot(@inner_block)}
</li>
"""
end
attr :href, :string, default: nil
attr :class, :string, default: nil
attr :rest, :global
slot :inner_block, required: true
def breadcrumb_link(assigns) do
~H"""
<a
href={@href}
data-slot="breadcrumb-link"
class={[
"transition-colors hover:text-foreground",
"focus-visible:rounded-sm focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none",
@class
]}
{@rest}
>
{render_slot(@inner_block)}
</a>
"""
end
attr :class, :string, default: nil
slot :inner_block, required: true
def breadcrumb_page(assigns) do
~H"""
<span data-slot="breadcrumb-page" aria-current="page" class={["font-normal text-foreground", @class]}>
{render_slot(@inner_block)}
</span>
"""
end
attr :class, :string, default: nil
# :rest forwards data-* (a global attribute valid on every element) so callers
# can attach CSS/JS hooks, matching the other subcomponents.
# repos/mdn/files/en-us/web/html/reference/global_attributes/index.md
attr :rest, :global
slot :inner_block
def breadcrumb_separator(assigns) do
~H"""
<li data-slot="breadcrumb-separator" aria-hidden="true" class={["[&>svg]:size-3.5", @class]} {@rest}>
<%= if @inner_block != [] do %>
{render_slot(@inner_block)}
<% else %>
<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"
>
<polyline points="9 18 15 12 9 6" />
</svg>
<% end %>
</li>
"""
end
attr :class, :string, default: nil
# :rest forwards data-* (a global attribute valid on every element) so callers
# can attach CSS/JS hooks, matching the other subcomponents.
# repos/mdn/files/en-us/web/html/reference/global_attributes/index.md
attr :rest, :global
def breadcrumb_ellipsis(assigns) do
~H"""
<span
data-slot="breadcrumb-ellipsis"
aria-hidden="true"
class={["flex size-9 items-center justify-center", @class]}
{@rest}
>
<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"
class="size-4"
aria-hidden="true"
>
<circle cx="12" cy="12" r="1" />
<circle cx="19" cy="12" r="1" />
<circle cx="5" cy="12" r="1" />
</svg>
<span class="sr-only">More</span>
</span>
"""
end
end
1. Save the file
Paste the markup; it relies only on the theme tokens in styles.css.
2. Use it
<nav data-slot="breadcrumb" aria-label="Breadcrumb">
<ol data-slot="breadcrumb-list" class="flex flex-wrap items-center gap-1.5 text-sm …">
<li data-slot="breadcrumb-item" class="inline-flex items-center gap-1.5">
<a href="/" data-slot="breadcrumb-link" class="hover:text-foreground …">Home</a>
</li>
<li data-slot="breadcrumb-separator" aria-hidden="true">›</li>
<li data-slot="breadcrumb-item">
<span data-slot="breadcrumb-page" aria-current="page" class="text-foreground">Breadcrumb</span>
</li>
</ol>
</nav>View source
<!--
shadcn-htmx — raw HTML breadcrumb snippet.
A <nav> landmark with aria-label wrapping an <ol> of links. The current
page is a plain <span aria-current="page"> (not a link). Separators are
decorative <li aria-hidden="true">. Zero JS — relies only on theme tokens
in styles.css.
Labelling: the landmark is named via aria-label OR aria-labelledby — both
are first-class per the APG. To point the <nav> at an existing visible
heading instead, drop aria-label and use aria-labelledby="heading-id" (do
not set both, or the <nav> carries two competing names).
Separators / ellipsis are decorative and accept any global attribute
(e.g. a data-* hook for CSS/JS) directly on the <li>/<span>.
APG: https://www.w3.org/WAI/ARIA/apg/patterns/breadcrumb/
-->
<nav data-slot="breadcrumb" aria-label="Breadcrumb">
<ol data-slot="breadcrumb-list"
class="flex flex-wrap items-center gap-1.5 text-sm break-words text-muted-foreground sm:gap-2.5">
<li data-slot="breadcrumb-item" class="inline-flex items-center gap-1.5">
<a href="/" data-slot="breadcrumb-link"
class="transition-colors hover:text-foreground focus-visible:rounded-sm focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none">Home</a>
</li>
<li data-slot="breadcrumb-separator" aria-hidden="true" class="[&>svg]:size-3.5">
<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"><polyline points="9 18 15 12 9 6" /></svg>
</li>
<li data-slot="breadcrumb-item" class="inline-flex items-center gap-1.5">
<a href="/components" data-slot="breadcrumb-link"
class="transition-colors hover:text-foreground focus-visible:rounded-sm focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none">Components</a>
</li>
<li data-slot="breadcrumb-separator" aria-hidden="true" class="[&>svg]:size-3.5">
<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"><polyline points="9 18 15 12 9 6" /></svg>
</li>
<li data-slot="breadcrumb-item" class="inline-flex items-center gap-1.5">
<span data-slot="breadcrumb-page" aria-current="page" class="font-normal text-foreground">Breadcrumb</span>
</li>
</ol>
</nav>
Examples
Basic — real links + current page
Parent pages are real <a href> links; the current page is a non-interactive <span aria-current="page">.
The <nav aria-label="Breadcrumb"> landmark lets AT users jump straight to the trail. Per the APG, the current page carries aria-current="page" and is rendered as a plain span — not a faked link — so the semantics match what the element actually is.
import { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink,
BreadcrumbPage, BreadcrumbSeparator, BreadcrumbEllipsis } from "@/components/ui/breadcrumb"
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem><BreadcrumbLink href="/">Home</BreadcrumbLink></BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem><BreadcrumbLink href="/components">Components</BreadcrumbLink></BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem><BreadcrumbPage>Breadcrumb</BreadcrumbPage></BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>{% from "components/breadcrumb.html" import breadcrumb_open, breadcrumb_close,
breadcrumb_list_open, breadcrumb_list_close, breadcrumb_item,
breadcrumb_link, breadcrumb_page, breadcrumb_separator %}
{{ breadcrumb_open(aria_label="Breadcrumb") }}{{ breadcrumb_list_open() }}
{{ breadcrumb_item(breadcrumb_link("Home", href="/")) }}
{{ breadcrumb_separator() }}
{{ breadcrumb_item(breadcrumb_link("Components", href="/components")) }}
{{ breadcrumb_separator() }}
{{ breadcrumb_item(breadcrumb_page("Breadcrumb")) }}
{{ breadcrumb_list_close() }}{{ breadcrumb_close() }}{{template "breadcrumb" (dict "Body" (htmlSafe `
{{template "breadcrumb_list" (dict "Body" (htmlSafe \`
{{template "breadcrumb_item" (dict "Body" (htmlSafe "<a href=\"/\" data-slot=breadcrumb-link>Home</a>"))}}
{{template "breadcrumb_separator" (dict)}}
{{template "breadcrumb_item" (dict "Body" (htmlSafe "..."))}}
{{template "breadcrumb_separator" (dict)}}
{{template "breadcrumb_item" (dict "Body" (htmlSafe "<span data-slot=breadcrumb-page aria-current=page>Breadcrumb</span>"))}}
\`))}}`))}}<.breadcrumb aria-label="Breadcrumb">
<.breadcrumb_list>
<.breadcrumb_item><.breadcrumb_link href={~p"/"}>Home</.breadcrumb_link></.breadcrumb_item>
<.breadcrumb_separator />
<.breadcrumb_item><.breadcrumb_link href={~p"/components"}>Components</.breadcrumb_link></.breadcrumb_item>
<.breadcrumb_separator />
<.breadcrumb_item><.breadcrumb_page>Breadcrumb</.breadcrumb_page></.breadcrumb_item>
</.breadcrumb_list>
</.breadcrumb><div class="p-4">
<nav data-slot="breadcrumb" aria-label="Breadcrumb" class="">
<ol data-slot="breadcrumb-list" class="flex flex-wrap items-center gap-1.5 text-sm break-words text-muted-foreground sm:gap-2.5">
<li data-slot="breadcrumb-item" class="inline-flex items-center gap-1.5">
<a href="#" data-slot="breadcrumb-link" class="transition-colors hover:text-foreground focus-visible:rounded-sm focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none">Home</a>
</li>
<li data-slot="breadcrumb-separator" aria-hidden="true" class="[&>svg]:size-3.5">
<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">
<polyline points="9 18 15 12 9 6">
</polyline>
</svg>
</li>
<li data-slot="breadcrumb-item" class="inline-flex items-center gap-1.5">
<a href="#" data-slot="breadcrumb-link" class="transition-colors hover:text-foreground focus-visible:rounded-sm focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none">Components</a>
</li>
<li data-slot="breadcrumb-separator" aria-hidden="true" class="[&>svg]:size-3.5">
<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">
<polyline points="9 18 15 12 9 6">
</polyline>
</svg>
</li>
<li data-slot="breadcrumb-item" class="inline-flex items-center gap-1.5">
<span data-slot="breadcrumb-page" aria-current="page" class="font-normal text-foreground">Breadcrumb</span>
</li>
</ol>
</nav>
</div>Further reading
Custom separator
Pass children to <BreadcrumbSeparator> to override the default chevron — e.g. a slash.
The separator is a decorative <li aria-hidden="true">, so changing the glyph never affects what AT announces — the links alone carry the meaning.
<BreadcrumbSeparator>/</BreadcrumbSeparator>{{ breadcrumb_separator("/") }}{{template "breadcrumb_separator" (dict "Body" (htmlSafe "/"))}}<.breadcrumb_separator>/</.breadcrumb_separator><div class="p-4">
<nav data-slot="breadcrumb" aria-label="Breadcrumb" class="">
<ol data-slot="breadcrumb-list" class="flex flex-wrap items-center gap-1.5 text-sm break-words text-muted-foreground sm:gap-2.5">
<li data-slot="breadcrumb-item" class="inline-flex items-center gap-1.5">
<a href="#" data-slot="breadcrumb-link" class="transition-colors hover:text-foreground focus-visible:rounded-sm focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none">Docs</a>
</li>
<li data-slot="breadcrumb-separator" aria-hidden="true" class="[&>svg]:size-3.5">/</li>
<li data-slot="breadcrumb-item" class="inline-flex items-center gap-1.5">
<a href="#" data-slot="breadcrumb-link" class="transition-colors hover:text-foreground focus-visible:rounded-sm focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none">Guides</a>
</li>
<li data-slot="breadcrumb-separator" aria-hidden="true" class="[&>svg]:size-3.5">/</li>
<li data-slot="breadcrumb-item" class="inline-flex items-center gap-1.5">
<span data-slot="breadcrumb-page" aria-current="page" class="font-normal text-foreground">Routing</span>
</li>
</ol>
</nav>
</div>Further reading
Collapsed — ellipsis for long trails
Use <BreadcrumbEllipsis> to collapse middle items. The glyph is aria-hidden but ships an sr-only "More" label.
When a trail is too deep, collapse the middle. The ellipsis is decorative but includes a visually-hidden More so AT users still hear that items were omitted.
<BreadcrumbItem><BreadcrumbEllipsis /></BreadcrumbItem>{{ breadcrumb_item(breadcrumb_ellipsis()) }}{{template "breadcrumb_item" (dict "Body" (htmlSafe (printf "%s" "<span ...ellipsis...>")))}}<.breadcrumb_item><.breadcrumb_ellipsis /></.breadcrumb_item><div class="p-4">
<nav data-slot="breadcrumb" aria-label="Breadcrumb" class="">
<ol data-slot="breadcrumb-list" class="flex flex-wrap items-center gap-1.5 text-sm break-words text-muted-foreground sm:gap-2.5">
<li data-slot="breadcrumb-item" class="inline-flex items-center gap-1.5">
<a href="#" data-slot="breadcrumb-link" class="transition-colors hover:text-foreground focus-visible:rounded-sm focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none">Home</a>
</li>
<li data-slot="breadcrumb-separator" aria-hidden="true" class="[&>svg]:size-3.5">
<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">
<polyline points="9 18 15 12 9 6">
</polyline>
</svg>
</li>
<li data-slot="breadcrumb-item" class="inline-flex items-center gap-1.5">
<span data-slot="breadcrumb-ellipsis" aria-hidden="true" class="flex size-9 items-center justify-center">
<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" class="size-4" aria-hidden="true">
<circle cx="12" cy="12" r="1">
</circle>
<circle cx="19" cy="12" r="1">
</circle>
<circle cx="5" cy="12" r="1">
</circle>
</svg>
<span class="sr-only">More</span>
</span>
</li>
<li data-slot="breadcrumb-separator" aria-hidden="true" class="[&>svg]:size-3.5">
<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">
<polyline points="9 18 15 12 9 6">
</polyline>
</svg>
</li>
<li data-slot="breadcrumb-item" class="inline-flex items-center gap-1.5">
<a href="#" data-slot="breadcrumb-link" class="transition-colors hover:text-foreground focus-visible:rounded-sm focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none">Components</a>
</li>
<li data-slot="breadcrumb-separator" aria-hidden="true" class="[&>svg]:size-3.5">
<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">
<polyline points="9 18 15 12 9 6">
</polyline>
</svg>
</li>
<li data-slot="breadcrumb-item" class="inline-flex items-center gap-1.5">
<span data-slot="breadcrumb-page" aria-current="page" class="font-normal text-foreground">Breadcrumb</span>
</li>
</ol>
</nav>
</div>Further reading
API Reference
<Breadcrumb>
| Prop | Type | Default | Description |
|---|---|---|---|
ariaLabelledby | string | — | Id of a visible heading that names the <nav> landmark (Breadcrumb root). When set, aria-label is not emitted so the landmark has a single name source. Use aria-label OR aria-labelledby, not both.APGBreadcrumb pattern |
data-* | string | — | Global data-* attribute, forwarded onto the rendered element. Now also accepted on BreadcrumbSeparator and BreadcrumbEllipsis (e.g. a CSS/JS hook), matching the other subcomponents.MDNdata-* global attribute |
ariaLabel | string | "Breadcrumb" | Accessible name for the <nav> landmark (Breadcrumb root).APGBreadcrumb pattern |
href | string | — | Destination for a BreadcrumbLink. Rendered as a real <a href>; omit on the current page (use BreadcrumbPage instead).MDN<a> element |
children | Child | — | Trail content. BreadcrumbSeparator accepts children to override the default chevron glyph. |
hx-* | any | — | Any htmx attribute. Forwarded onto the underlying element.htmxAttribute reference |
class | string | — | Extra Tailwind classes appended to the root element. |