Breadcrumb source
Inspect and copy the reviewed source included with this Free component.
Free source · 4 files
recipe.rs
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Breadcrumb recipe — static named slots for navigation hierarchy parts.
use crate::tw_join;
use proa_core::shared::attr_value::AttrValue;
use proa_core::{DataLoader, WebContext, WriteBuf, WriteError};
mod base {
use super::*;
pub const ROOT: &str = "";
pub const LIST: &str = tw_join!(
"flex",
"flex-wrap",
"items-center",
"gap-1.5",
"break-words",
"text-sm",
"text-muted-foreground",
"sm:gap-2.5"
);
pub const ITEM: &str = tw_join!("inline-flex", "items-center", "gap-1.5");
pub const LINK: &str = tw_join!("transition-colors", "hover:text-foreground");
pub const PAGE: &str = tw_join!("font-normal", "text-foreground");
pub const SEPARATOR: &str = tw_join!("[&>svg]:h-3.5", "[&>svg]:w-3.5");
pub const ELLIPSIS: &str = tw_join!("flex", "h-9", "w-9", "items-center", "justify-center");
pub const ELLIPSIS_LABEL: &str = "sr-only";
}
/// Optional inputs accepted by [`BreadcrumbRecipe::resolve`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct BreadcrumbRecipeProps;
/// Static named classes for every Breadcrumb part.
#[derive(Debug, Clone, Copy)]
pub struct BreadcrumbRecipe {
pub root: &'static str,
pub list: &'static str,
pub item: &'static str,
pub link: &'static str,
pub page: &'static str,
pub separator: &'static str,
pub ellipsis: &'static str,
pub ellipsis_label: &'static str,
}
impl BreadcrumbRecipe {
pub const fn resolve(&'static self, _props: BreadcrumbRecipeProps) -> BreadcrumbStyle {
BreadcrumbStyle { recipe: self }
}
}
/// Fully resolved, streamable Breadcrumb root style.
#[must_use]
#[derive(Debug, Clone, Copy)]
pub struct BreadcrumbStyle {
recipe: &'static BreadcrumbRecipe,
}
impl AttrValue for BreadcrumbStyle {
#[inline(always)]
fn should_render(&self) -> bool {
!self.recipe.root.is_empty()
}
#[inline(always)]
fn render_attr_value<B: WriteBuf, L: DataLoader>(
&self,
cx: &mut WebContext<L, B>,
) -> Result<(), WriteError> {
cx.out.extend_static(self.recipe.root.as_bytes())
}
}
pub const BREADCRUMB_RECIPE: BreadcrumbRecipe = BreadcrumbRecipe {
root: base::ROOT,
list: base::LIST,
item: base::ITEM,
link: base::LINK,
page: base::PAGE,
separator: base::SEPARATOR,
ellipsis: base::ELLIPSIS,
ellipsis_label: base::ELLIPSIS_LABEL,
};
pub const fn breadcrumb_recipe() -> &'static BreadcrumbRecipe {
&BREADCRUMB_RECIPE
}
impl crate::variant_spec::ComponentSpec for BreadcrumbRecipe {
const NAME: &'static str = "Breadcrumb";
fn base_classes(&self) -> &'static str {
self.root
}
fn variant_metadata(&self) -> Vec<crate::variant_spec::VariantMetadata> {
Vec::new()
}
fn anatomy(&self) -> Option<crate::Anatomy> {
Some(super::BREADCRUMB_ANATOMY)
}
}
/// Compatibility accessors for the former standalone helpers.
pub const fn breadcrumb_classes() -> &'static str {
BREADCRUMB_RECIPE.root
}
pub const fn breadcrumb_list_classes() -> &'static str {
BREADCRUMB_RECIPE.list
}
pub const fn breadcrumb_item_classes() -> &'static str {
BREADCRUMB_RECIPE.item
}
pub const fn breadcrumb_link_classes() -> &'static str {
BREADCRUMB_RECIPE.link
}
pub const fn breadcrumb_page_classes() -> &'static str {
BREADCRUMB_RECIPE.page
}
pub const fn breadcrumb_separator_classes() -> &'static str {
BREADCRUMB_RECIPE.separator
}
pub const fn breadcrumb_ellipsis_classes() -> &'static str {
BREADCRUMB_RECIPE.ellipsis
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recipe_exposes_every_named_slot() {
let recipe = breadcrumb_recipe();
assert!(recipe.root.is_empty());
assert!(recipe.list.contains("flex-wrap"));
assert!(recipe.item.contains("inline-flex"));
assert!(recipe.link.contains("hover:text-foreground"));
assert!(recipe.page.contains("text-foreground"));
assert!(recipe.separator.contains("[&>svg]:h-3.5"));
assert!(recipe.ellipsis.contains("justify-center"));
assert_eq!(recipe.ellipsis_label, "sr-only");
}
#[test]
fn recipe_is_const_and_copy() {
const STYLE: BreadcrumbStyle = BREADCRUMB_RECIPE.resolve(BreadcrumbRecipeProps);
let copied = STYLE;
assert!(copied.recipe.root.is_empty());
}
#[test]
fn recipe_metadata_exposes_the_complete_anatomy() {
let metadata = crate::variant_spec::component_metadata(breadcrumb_recipe());
let anatomy = metadata.anatomy.as_ref().unwrap();
assert_eq!(metadata.name, "Breadcrumb");
assert!(metadata.variants.is_empty());
assert_eq!(anatomy.scope, "breadcrumb");
assert_eq!(anatomy.parts, super::super::BREADCRUMB_ANATOMY.parts());
metadata.validate().unwrap();
}
}
FreeBreadcrumb
Page hierarchy navigation.
Install with the Proa CLI
$ proa ui add breadcrumbInstalls the reviewed component source, dependencies, shared support files, and required legal notices.
CLI and registry setup →recipe.rs
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Breadcrumb recipe — static named slots for navigation hierarchy parts.
use crate::tw_join;
use proa_core::shared::attr_value::AttrValue;
use proa_core::{DataLoader, WebContext, WriteBuf, WriteError};
mod base {
use super::*;
pub const ROOT: &str = "";
pub const LIST: &str = tw_join!(
"flex",
"flex-wrap",
"items-center",
"gap-1.5",
"break-words",
"text-sm",
"text-muted-foreground",
"sm:gap-2.5"
Breadcrumb source
Browse and copy the reviewed source included with this Free component.
breadcrumb.rsOpen raw ↗
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: (MIT OR Apache-2.0) AND (ISC AND MIT)
//! Breadcrumb component implementation
//!
//! Renders a navigation hierarchy with accessible breadcrumb markup.
use super::recipe::{BreadcrumbRecipeProps, BREADCRUMB_RECIPE};
use super::BREADCRUMB_ANATOMY;
use proa_core::{UnsafeRaw, WebContext, WebRenderSync, WriteBuf};
use proa_macros::html_sync;
/// ChevronRight SVG icon used as the default separator
const CHEVRON_RIGHT_SVG: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>"#;
/// MoreHorizontal SVG icon used for the ellipsis indicator
const MORE_HORIZONTAL_SVG: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg>"#;
/// Breadcrumb component - `<nav>` root wrapper with `aria-label="breadcrumb"`
pub struct Breadcrumb<C = ()> {
/// Additional CSS classes
pub class: Option<&'static str>,
/// The breadcrumb content
pub children: Option<C>,
}
impl<C> Default for Breadcrumb<C> {
fn default() -> Self {
Self {
class: None,
children: None,
}
}
}
impl<B: WriteBuf, C: WebRenderSync<L, B>, L: ::proa_core::DataLoader> WebRenderSync<L, B>
for Breadcrumb<C>
{
fn render(
self,
cx: &mut WebContext<L, B>,
) -> Result<(), proa_core::shared::write_buf::WriteError> {
let style = BREADCRUMB_RECIPE.resolve(BreadcrumbRecipeProps);
html_sync! {
<nav
data-breadcrumb
data-scope={BREADCRUMB_ANATOMY.scope()}
data-part="root"
aria-label="breadcrumb"
data-slot="breadcrumb"
class={proa_macros::text!("{} {}", style, self.class)}
>
{self.children}
</nav>
}
.render(cx)
}
}
/// BreadcrumbList component - `<ol>` ordered list container with flexbox layout
pub struct BreadcrumbList<C = ()> {
/// Additional CSS classes
pub class: Option<&'static str>,
/// The list content
pub children: Option<C>,
}
impl<C> Default for BreadcrumbList<C> {
fn default() -> Self {
Self {
class: None,
children: None,
}
}
}
impl<B: WriteBuf, C: WebRenderSync<L, B>, L: ::proa_core::DataLoader> WebRenderSync<L, B>
for BreadcrumbList<C>
{
fn render(
self,
cx: &mut WebContext<L, B>,
) -> Result<(), proa_core::shared::write_buf::WriteError> {
html_sync! {
<ol
data-scope={BREADCRUMB_ANATOMY.scope()}
data-part="list"
data-slot="breadcrumb-list"
class={proa_macros::text!("{} {}", BREADCRUMB_RECIPE.list, self.class)}
>
{self.children}
</ol>
}
.render(cx)
}
}
/// BreadcrumbItem component - `<li>` individual breadcrumb wrapper
pub struct BreadcrumbItem<C = ()> {
/// Additional CSS classes
pub class: Option<&'static str>,
/// The item content
pub children: Option<C>,
}
impl<C> Default for BreadcrumbItem<C> {
fn default() -> Self {
Self {
class: None,
children: None,
}
}
}
impl<B: WriteBuf, C: WebRenderSync<L, B>, L: ::proa_core::DataLoader> WebRenderSync<L, B>
for BreadcrumbItem<C>
{
fn render(
self,
cx: &mut WebContext<L, B>,
) -> Result<(), proa_core::shared::write_buf::WriteError> {
html_sync! {
<li
data-scope={BREADCRUMB_ANATOMY.scope()}
data-part="item"
data-slot="breadcrumb-item"
class={proa_macros::text!("{} {}", BREADCRUMB_RECIPE.item, self.class)}
>
{self.children}
</li>
}
.render(cx)
}
}
/// BreadcrumbLink component - `<a>` clickable navigation element
///
/// When `href` is `Some(..)` this renders a real `<a href="...">` anchor. When
/// `href` is `None` there is nothing to navigate to, so it degrades to a
/// `<span>` with the same `data-slot` and classes - an anchor without an `href`
/// is not a link, and rendering one invites accidental clicks that go nowhere.
pub struct BreadcrumbLink<C = ()> {
/// Additional CSS classes
pub class: Option<&'static str>,
/// The link href
pub href: Option<&'static str>,
/// The link content
pub children: Option<C>,
}
impl<C> Default for BreadcrumbLink<C> {
fn default() -> Self {
Self {
class: None,
href: None,
children: None,
}
}
}
impl<B: WriteBuf, C: WebRenderSync<L, B>, L: ::proa_core::DataLoader> WebRenderSync<L, B>
for BreadcrumbLink<C>
{
fn render(
self,
cx: &mut WebContext<L, B>,
) -> Result<(), proa_core::shared::write_buf::WriteError> {
match self.href {
Some(href) => html_sync! {
<a
data-scope={BREADCRUMB_ANATOMY.scope()}
data-part="link"
data-slot="breadcrumb-link"
class={proa_macros::text!("{} {}", BREADCRUMB_RECIPE.link, self.class)}
href={href}
>
{self.children}
</a>
}
.render(cx),
None => html_sync! {
<span
data-scope={BREADCRUMB_ANATOMY.scope()}
data-part="link"
data-slot="breadcrumb-link"
class={proa_macros::text!("{} {}", BREADCRUMB_RECIPE.link, self.class)}
>
{self.children}
</span>
}
.render(cx),
}
}
}
/// BreadcrumbPage component - `<span>` non-clickable current page indicator
pub struct BreadcrumbPage<C = ()> {
/// Additional CSS classes
pub class: Option<&'static str>,
/// The page content
pub children: Option<C>,
}
impl<C> Default for BreadcrumbPage<C> {
fn default() -> Self {
Self {
class: None,
children: None,
}
}
}
impl<B: WriteBuf, C: WebRenderSync<L, B>, L: ::proa_core::DataLoader> WebRenderSync<L, B>
for BreadcrumbPage<C>
{
fn render(
self,
cx: &mut WebContext<L, B>,
) -> Result<(), proa_core::shared::write_buf::WriteError> {
html_sync! {
<span
data-scope={BREADCRUMB_ANATOMY.scope()}
data-part="page"
data-slot="breadcrumb-page"
aria-disabled="true"
aria-current="page"
class={proa_macros::text!("{} {}", BREADCRUMB_RECIPE.page, self.class)}
>
{self.children}
</span>
}
.render(cx)
}
}
/// BreadcrumbSeparator component - `<li>` divider between breadcrumb items
///
/// Defaults to a ChevronRight SVG icon if no children are provided.
pub struct BreadcrumbSeparator<C = ()> {
/// Additional CSS classes
pub class: Option<&'static str>,
/// Custom separator content; defaults to ChevronRight SVG if None
pub children: Option<C>,
}
impl<C> Default for BreadcrumbSeparator<C> {
fn default() -> Self {
Self {
class: None,
children: None,
}
}
}
impl<B: WriteBuf, C: WebRenderSync<L, B>, L: ::proa_core::DataLoader> WebRenderSync<L, B>
for BreadcrumbSeparator<C>
{
fn render(
self,
cx: &mut WebContext<L, B>,
) -> Result<(), proa_core::shared::write_buf::WriteError> {
match self.children {
Some(children) => html_sync! {
<li
data-scope={BREADCRUMB_ANATOMY.scope()}
data-part="separator"
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
class={proa_macros::text!("{} {}", BREADCRUMB_RECIPE.separator, self.class)}
>
{children}
</li>
}
.render(cx),
None => html_sync! {
<li
data-scope={BREADCRUMB_ANATOMY.scope()}
data-part="separator"
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
class={proa_macros::text!("{} {}", BREADCRUMB_RECIPE.separator, self.class)}
>
{UnsafeRaw(CHEVRON_RIGHT_SVG)} // proa-perf-ok: trusted static markup emitted through the low-level writer
</li>
}
.render(cx),
}
}
}
/// BreadcrumbEllipsis component - collapsed state indicator with MoreHorizontal icon
#[derive(Default)]
pub struct BreadcrumbEllipsis {
/// Additional CSS classes
pub class: Option<&'static str>,
}
impl<B: WriteBuf, L: ::proa_core::DataLoader> WebRenderSync<L, B> for BreadcrumbEllipsis {
fn render(
self,
cx: &mut WebContext<L, B>,
) -> Result<(), proa_core::shared::write_buf::WriteError> {
html_sync! {
<span
data-scope={BREADCRUMB_ANATOMY.scope()}
data-part="ellipsis"
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
class={proa_macros::text!("{} {}", BREADCRUMB_RECIPE.ellipsis, self.class)}
>
{UnsafeRaw(MORE_HORIZONTAL_SVG)} // proa-perf-ok: trusted static markup emitted through the low-level writer
<span
data-scope={BREADCRUMB_ANATOMY.scope()}
data-part="ellipsis-label"
data-slot="breadcrumb-ellipsis-label"
class={BREADCRUMB_RECIPE.ellipsis_label}
>
"More"
</span>
</span>
}
.render(cx)
}
}
#[cfg(test)]
mod tests {
use super::*;
use proa_core::ctx::Ctx;
use proa_core::text::text;
#[test]
fn breadcrumb_renders_nav_element() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
Breadcrumb {
class: None,
children: Some(text("Content")),
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("<nav"));
assert!(html.contains("aria-label=\"breadcrumb\""));
assert!(html.contains("data-slot=\"breadcrumb\""));
assert!(html.contains("Content"));
}
#[test]
fn breadcrumb_root_accepts_custom_class() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
Breadcrumb {
class: Some("custom-root"),
children: Some(text("Content")),
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("custom-root"));
}
#[test]
fn breadcrumb_list_renders_ol_element() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
BreadcrumbList {
class: None,
children: Some(text("Items")),
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("<ol"));
assert!(html.contains("data-slot=\"breadcrumb-list\""));
assert!(html.contains("flex"));
assert!(html.contains("flex-wrap"));
assert!(html.contains("text-sm"));
assert!(html.contains("Items"));
}
#[test]
fn breadcrumb_list_accepts_custom_class() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
BreadcrumbList {
class: Some("custom-class"),
children: Some(text("Items")),
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("custom-class"));
}
#[test]
fn breadcrumb_item_renders_li_element() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
BreadcrumbItem {
class: None,
children: Some(text("Item")),
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("<li"));
assert!(html.contains("data-slot=\"breadcrumb-item\""));
assert!(html.contains("inline-flex"));
assert!(html.contains("items-center"));
assert!(html.contains("Item"));
}
#[test]
fn breadcrumb_link_renders_anchor_element() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
BreadcrumbLink {
class: None,
href: Some("/home"),
children: Some(text("Home")),
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("<a"));
assert!(html.contains("data-slot=\"breadcrumb-link\""));
assert!(html.contains("href=\"/home\""));
assert!(html.contains("transition-colors"));
assert!(html.contains("Home"));
}
#[test]
fn breadcrumb_link_renders_span_without_href() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
BreadcrumbLink {
class: None,
href: None,
children: Some(text("No Link")),
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("<span"));
assert!(html.contains("data-slot=\"breadcrumb-link\""));
assert!(html.contains("No Link"));
// Regression: an href-less breadcrumb link must not be an anchor at all,
// otherwise it still reads as clickable to the browser.
assert!(!html.contains("<a"));
assert!(!html.contains("href"));
// ...but everything else about it is unchanged.
assert!(html.contains("transition-colors"));
}
#[test]
fn breadcrumb_link_keeps_anchor_when_href_present() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
BreadcrumbLink {
class: None,
href: Some("/docs"),
children: Some(text("Docs")),
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("<a "));
assert!(html.contains("href=\"/docs\""));
assert!(html.contains("data-slot=\"breadcrumb-link\""));
assert!(!html.contains("<span"));
assert!(html.contains("Docs"));
}
#[test]
fn breadcrumb_link_and_page_share_markup_shape_when_not_navigable() {
let mut link_out = Vec::new();
let mut link_cx = Ctx::with_buffer(&mut link_out);
BreadcrumbLink {
class: None,
href: None,
children: Some(text("Home")),
}
.render(&mut link_cx)
.unwrap();
let link_html = String::from_utf8(link_out).unwrap();
let mut page_out = Vec::new();
let mut page_cx = Ctx::with_buffer(&mut page_out);
BreadcrumbPage {
class: None,
children: Some(text("Current")),
}
.render(&mut page_cx)
.unwrap();
let page_html = String::from_utf8(page_out).unwrap();
// Neither the non-navigable link nor the current page is an anchor.
assert!(link_html.starts_with("<span"));
assert!(page_html.starts_with("<span"));
assert!(!link_html.contains("<a"));
assert!(!page_html.contains("<a"));
}
#[test]
fn breadcrumb_page_renders_span_element() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
BreadcrumbPage {
class: None,
children: Some(text("Current Page")),
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("<span"));
assert!(html.contains("data-slot=\"breadcrumb-page\""));
assert!(html.contains("aria-disabled=\"true\""));
assert!(html.contains("aria-current=\"page\""));
assert!(html.contains("font-normal"));
assert!(html.contains("text-foreground"));
assert!(html.contains("Current Page"));
}
#[test]
fn breadcrumb_separator_renders_default_chevron() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
BreadcrumbSeparator::<()> {
class: None,
children: None,
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("<li"));
assert!(html.contains("data-slot=\"breadcrumb-separator\""));
assert!(html.contains("role=\"presentation\""));
assert!(html.contains("aria-hidden=\"true\""));
assert!(html.contains("<svg"));
assert!(html.contains("m9 18 6-6-6-6"));
}
#[test]
fn breadcrumb_separator_renders_custom_children() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
BreadcrumbSeparator {
class: None,
children: Some(text("/")),
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("<li"));
assert!(html.contains("data-slot=\"breadcrumb-separator\""));
assert!(html.contains("/"));
assert!(!html.contains("<svg"));
}
#[test]
fn breadcrumb_ellipsis_renders_more_icon() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
BreadcrumbEllipsis { class: None }.render(&mut cx).unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("<span"));
assert!(html.contains("data-slot=\"breadcrumb-ellipsis\""));
assert!(html.contains("role=\"presentation\""));
assert!(html.contains("aria-hidden=\"true\""));
assert!(html.contains("<svg"));
assert!(html.contains("sr-only"));
assert!(html.contains("More"));
assert!(html.contains(r#"data-scope="breadcrumb" data-part="ellipsis""#));
assert!(html.contains(r#"data-scope="breadcrumb" data-part="ellipsis-label""#));
}
#[test]
fn breadcrumb_ellipsis_accepts_custom_class() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
BreadcrumbEllipsis {
class: Some("custom-ellipsis"),
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("custom-ellipsis"));
}
#[test]
fn full_breadcrumb_composition() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
Breadcrumb {
class: None,
children: Some(html_sync! {
{BreadcrumbList {
class: None,
children: Some(html_sync! {
{BreadcrumbItem {
class: None,
children: Some(html_sync! {
{BreadcrumbLink {
class: None,
href: Some("/"),
children: Some(text("Home")),
}}
}),
}}
{BreadcrumbSeparator::<() > { class: None, children: None, }}
{BreadcrumbItem {
class: None,
children: Some(html_sync! {
{BreadcrumbLink {
class: None,
href: Some("/docs"),
children: Some(text("Documentation")),
}}
}),
}}
{BreadcrumbSeparator::<() > { class: None, children: None, }}
{BreadcrumbItem {
class: None,
children: Some(html_sync! {
{BreadcrumbPage {
class: None,
children: Some(text("Components")),
}}
}),
}}
}),
}}
}),
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("data-slot=\"breadcrumb\""));
assert!(html.contains("data-slot=\"breadcrumb-list\""));
assert!(html.contains("data-slot=\"breadcrumb-item\""));
assert!(html.contains("data-slot=\"breadcrumb-link\""));
assert!(html.contains("data-slot=\"breadcrumb-separator\""));
assert!(html.contains("data-slot=\"breadcrumb-page\""));
assert!(html.contains("aria-label=\"breadcrumb\""));
assert!(html.contains("aria-current=\"page\""));
assert!(html.contains("Home"));
assert!(html.contains("Documentation"));
assert!(html.contains("Components"));
assert!(html.contains("href=\"/\""));
assert!(html.contains("href=\"/docs\""));
assert!(html.contains(r#"data-scope="breadcrumb" data-part="root""#));
for part in ["list", "item", "link", "separator", "page"] {
assert!(html.contains(&format!(r#"data-part="{part}""#)));
}
}
}
md.rsOpen raw ↗
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0
use super::{
Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage,
BreadcrumbSeparator,
};
use proa_core::WebContext;
use proa_core::{MdRenderSync, WriteBuf, WriteError};
use proa_macros::md_sync;
/// Breadcrumb is a passthrough.
impl<Loader: ::proa_core::DataLoader, B: WriteBuf, C: MdRenderSync<Loader, B>>
MdRenderSync<Loader, B> for Breadcrumb<C>
{
fn render_md(self, cx: &mut WebContext<Loader, B>) -> Result<(), WriteError> {
self.children.render_md(cx)?;
cx.md_blank_line()?;
Ok(())
}
}
/// BreadcrumbList is a passthrough.
impl<Loader: ::proa_core::DataLoader, B: WriteBuf, C: MdRenderSync<Loader, B>>
MdRenderSync<Loader, B> for BreadcrumbList<C>
{
fn render_md(self, cx: &mut WebContext<Loader, B>) -> Result<(), WriteError> {
self.children.render_md(cx)?;
Ok(())
}
}
/// BreadcrumbItem is a passthrough.
impl<Loader: ::proa_core::DataLoader, B: WriteBuf, C: MdRenderSync<Loader, B>>
MdRenderSync<Loader, B> for BreadcrumbItem<C>
{
fn render_md(self, cx: &mut WebContext<Loader, B>) -> Result<(), WriteError> {
self.children.render_md(cx)?;
Ok(())
}
}
/// BreadcrumbLink renders as `[text](url)`.
impl<Loader: ::proa_core::DataLoader, B: WriteBuf, C: MdRenderSync<Loader, B>>
MdRenderSync<Loader, B> for BreadcrumbLink<C>
{
fn render_md(self, cx: &mut WebContext<Loader, B>) -> Result<(), WriteError> {
if let Some(href) = self.href {
cx.md_link_with(href, |cx| self.children.render_md(cx))?;
} else {
self.children.render_md(cx)?;
}
Ok(())
}
}
/// BreadcrumbPage renders as plain text (current page, no link).
impl<Loader: ::proa_core::DataLoader, B: WriteBuf, C: MdRenderSync<Loader, B>>
MdRenderSync<Loader, B> for BreadcrumbPage<C>
{
fn render_md(self, cx: &mut WebContext<Loader, B>) -> Result<(), WriteError> {
self.children.render_md(cx)?;
Ok(())
}
}
/// BreadcrumbSeparator renders as ` / `.
impl<Loader: ::proa_core::DataLoader, B: WriteBuf, C> MdRenderSync<Loader, B>
for BreadcrumbSeparator<C>
{
fn render_md(self, cx: &mut WebContext<Loader, B>) -> Result<(), WriteError> {
md_sync! { " / " }.render_md(cx)
}
}
/// BreadcrumbEllipsis renders as `...`.
impl<Loader: ::proa_core::DataLoader, B: WriteBuf> MdRenderSync<Loader, B> for BreadcrumbEllipsis {
fn render_md(self, cx: &mut WebContext<Loader, B>) -> Result<(), WriteError> {
md_sync! { "..." }.render_md(cx)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn breadcrumb_link_renders_markdown_link() {
let mut buf = Vec::new();
let mut cx = WebContext::with_buffer(&mut buf);
BreadcrumbLink {
class: None,
href: Some("/docs"),
children: Some("Documentation"),
}
.render_md(&mut cx)
.unwrap();
assert_eq!(String::from_utf8(buf).unwrap(), "[Documentation](/docs)");
}
#[test]
fn breadcrumb_separator_renders_slash() {
let mut buf = Vec::new();
let mut cx = WebContext::with_buffer(&mut buf);
BreadcrumbSeparator::<()> {
class: None,
children: None,
}
.render_md(&mut cx)
.unwrap();
assert_eq!(String::from_utf8(buf).unwrap(), " / ");
}
#[test]
fn breadcrumb_ellipsis_renders_dots() {
let mut buf = Vec::new();
let mut cx = WebContext::with_buffer(&mut buf);
BreadcrumbEllipsis { class: None }
.render_md(&mut cx)
.unwrap();
assert_eq!(String::from_utf8(buf).unwrap(), "...");
}
}
mod.rsOpen raw ↗
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0
#![allow(clippy::module_inception)]
//! Breadcrumb component - navigation hierarchy display
mod breadcrumb;
mod md;
mod recipe;
/// Stable scope/part contract shared by every Breadcrumb renderer.
pub const BREADCRUMB_ANATOMY: crate::Anatomy = crate::Anatomy::new(
"breadcrumb",
&[
"root",
"list",
"item",
"link",
"page",
"separator",
"ellipsis",
"ellipsis-label",
],
);
pub use breadcrumb::{
Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage,
BreadcrumbSeparator,
};
pub use recipe::{
breadcrumb_classes, breadcrumb_ellipsis_classes, breadcrumb_item_classes,
breadcrumb_link_classes, breadcrumb_list_classes, breadcrumb_page_classes, breadcrumb_recipe,
breadcrumb_separator_classes, BreadcrumbRecipe, BreadcrumbRecipeProps, BreadcrumbStyle,
BREADCRUMB_RECIPE,
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn anatomy_names_every_public_breadcrumb_part() {
assert_eq!(BREADCRUMB_ANATOMY.scope(), "breadcrumb");
for part in [
"root",
"list",
"item",
"link",
"page",
"separator",
"ellipsis",
"ellipsis-label",
] {
assert!(BREADCRUMB_ANATOMY.contains(part));
}
}
}
recipe.rsOpen raw ↗
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Breadcrumb recipe — static named slots for navigation hierarchy parts.
use crate::tw_join;
use proa_core::shared::attr_value::AttrValue;
use proa_core::{DataLoader, WebContext, WriteBuf, WriteError};
mod base {
use super::*;
pub const ROOT: &str = "";
pub const LIST: &str = tw_join!(
"flex",
"flex-wrap",
"items-center",
"gap-1.5",
"break-words",
"text-sm",
"text-muted-foreground",
"sm:gap-2.5"
);
pub const ITEM: &str = tw_join!("inline-flex", "items-center", "gap-1.5");
pub const LINK: &str = tw_join!("transition-colors", "hover:text-foreground");
pub const PAGE: &str = tw_join!("font-normal", "text-foreground");
pub const SEPARATOR: &str = tw_join!("[&>svg]:h-3.5", "[&>svg]:w-3.5");
pub const ELLIPSIS: &str = tw_join!("flex", "h-9", "w-9", "items-center", "justify-center");
pub const ELLIPSIS_LABEL: &str = "sr-only";
}
/// Optional inputs accepted by [`BreadcrumbRecipe::resolve`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct BreadcrumbRecipeProps;
/// Static named classes for every Breadcrumb part.
#[derive(Debug, Clone, Copy)]
pub struct BreadcrumbRecipe {
pub root: &'static str,
pub list: &'static str,
pub item: &'static str,
pub link: &'static str,
pub page: &'static str,
pub separator: &'static str,
pub ellipsis: &'static str,
pub ellipsis_label: &'static str,
}
impl BreadcrumbRecipe {
pub const fn resolve(&'static self, _props: BreadcrumbRecipeProps) -> BreadcrumbStyle {
BreadcrumbStyle { recipe: self }
}
}
/// Fully resolved, streamable Breadcrumb root style.
#[must_use]
#[derive(Debug, Clone, Copy)]
pub struct BreadcrumbStyle {
recipe: &'static BreadcrumbRecipe,
}
impl AttrValue for BreadcrumbStyle {
#[inline(always)]
fn should_render(&self) -> bool {
!self.recipe.root.is_empty()
}
#[inline(always)]
fn render_attr_value<B: WriteBuf, L: DataLoader>(
&self,
cx: &mut WebContext<L, B>,
) -> Result<(), WriteError> {
cx.out.extend_static(self.recipe.root.as_bytes())
}
}
pub const BREADCRUMB_RECIPE: BreadcrumbRecipe = BreadcrumbRecipe {
root: base::ROOT,
list: base::LIST,
item: base::ITEM,
link: base::LINK,
page: base::PAGE,
separator: base::SEPARATOR,
ellipsis: base::ELLIPSIS,
ellipsis_label: base::ELLIPSIS_LABEL,
};
pub const fn breadcrumb_recipe() -> &'static BreadcrumbRecipe {
&BREADCRUMB_RECIPE
}
impl crate::variant_spec::ComponentSpec for BreadcrumbRecipe {
const NAME: &'static str = "Breadcrumb";
fn base_classes(&self) -> &'static str {
self.root
}
fn variant_metadata(&self) -> Vec<crate::variant_spec::VariantMetadata> {
Vec::new()
}
fn anatomy(&self) -> Option<crate::Anatomy> {
Some(super::BREADCRUMB_ANATOMY)
}
}
/// Compatibility accessors for the former standalone helpers.
pub const fn breadcrumb_classes() -> &'static str {
BREADCRUMB_RECIPE.root
}
pub const fn breadcrumb_list_classes() -> &'static str {
BREADCRUMB_RECIPE.list
}
pub const fn breadcrumb_item_classes() -> &'static str {
BREADCRUMB_RECIPE.item
}
pub const fn breadcrumb_link_classes() -> &'static str {
BREADCRUMB_RECIPE.link
}
pub const fn breadcrumb_page_classes() -> &'static str {
BREADCRUMB_RECIPE.page
}
pub const fn breadcrumb_separator_classes() -> &'static str {
BREADCRUMB_RECIPE.separator
}
pub const fn breadcrumb_ellipsis_classes() -> &'static str {
BREADCRUMB_RECIPE.ellipsis
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recipe_exposes_every_named_slot() {
let recipe = breadcrumb_recipe();
assert!(recipe.root.is_empty());
assert!(recipe.list.contains("flex-wrap"));
assert!(recipe.item.contains("inline-flex"));
assert!(recipe.link.contains("hover:text-foreground"));
assert!(recipe.page.contains("text-foreground"));
assert!(recipe.separator.contains("[&>svg]:h-3.5"));
assert!(recipe.ellipsis.contains("justify-center"));
assert_eq!(recipe.ellipsis_label, "sr-only");
}
#[test]
fn recipe_is_const_and_copy() {
const STYLE: BreadcrumbStyle = BREADCRUMB_RECIPE.resolve(BreadcrumbRecipeProps);
let copied = STYLE;
assert!(copied.recipe.root.is_empty());
}
#[test]
fn recipe_metadata_exposes_the_complete_anatomy() {
let metadata = crate::variant_spec::component_metadata(breadcrumb_recipe());
let anatomy = metadata.anatomy.as_ref().unwrap();
assert_eq!(metadata.name, "Breadcrumb");
assert!(metadata.variants.is_empty());
assert_eq!(anatomy.scope, "breadcrumb");
assert_eq!(anatomy.parts, super::super::BREADCRUMB_ANATOMY.parts());
metadata.validate().unwrap();
}
}