Files
Htmx/docs/Components/Breadcrumb.md
T
2026-05-04 19:57:48 +05:00

4.8 KiB
Raw Blame History

Breadcrumb

A navigation trail showing the user's location in the app hierarchy. Items are separated by chevron icons. The last item is always rendered as plain text (current page); earlier items are links.


HTML structure

nav[aria-label=Breadcrumb]
  ol.flex.flex-wrap.items-center.gap-1.5.text-sm.text-muted-foreground
    li.inline-flex.items-center.gap-1.5    ← one per item
      a | span                              ← a = link, span = non-linked or current
      span[role=presentation, aria-hidden]  ← chevron separator (omitted after last item)
        svg (3.5×3.5, chevron-right)

CSS mechanics

Class Effect
text-muted-foreground Dimmed color for all non-current items
font-normal text-foreground Full-opacity color applied to the last (current) item
hover:text-foreground transition-colors Link hover state
flex-wrap Items wrap on narrow screens

Constructor signature

public Breadcrumb(IEnumerable<(string Label, string Href)> items)
Parameter Description
items Ordered list of (Label, Href) tuples. The last item is always the current page.

Rules:

  • The last item is always non-linked and rendered in full text-foreground color, regardless of its Href value.
  • Any non-last item with an empty Href is rendered as a plain <span> rather than a link.

Usage examples

Simple three-level breadcrumb

new Breadcrumb(new[]
{
    ("Home",     "/"),
    ("Settings", "/settings"),
    ("Profile",  ""),          // current page — href is ignored for the last item
})

Dynamic breadcrumb from a data path

// Build items from a category tree
var crumbs = categoryPath
    .Select((cat, i) => (cat.Name, i < categoryPath.Count - 1 ? cat.Url : ""))
    .ToArray();

new Breadcrumb(crumbs)

Embedded in a page

<!-- MyPage.htmx -->
<div class="mb-6">
  $$Breadcrumb$$
</div>
// MyPage.htmx.cs
public IHtmxComponent Breadcrumb { get; }

public MyPage()
{
    Breadcrumb = new Breadcrumb(new[]
    {
        ("Home",    "/"),
        ("Reports", "/reports"),
        ("Monthly", ""),
    });
}

protected override void RenderBreadcrumb(HtmxRenderContext ctx)
    => Breadcrumb.Render(ctx.Next());

Tips and tricks

  • Always make the last item the current page — its href is ignored anyway, and it gets the visual "active" treatment automatically.
  • If you have a non-navigable segment (e.g. a category separator with no URL), pass an empty Href for that item and it will render as a plain span.
  • For very deep hierarchies, consider truncating the middle items and replacing them with a span — build the items list conditionally before passing to the constructor.
  • The chevron separator is aria-hidden so screen readers announce only the labels in sequence.

Complete page example

Templates/ArticlePage.htmx

<div class="max-w-3xl mx-auto py-10">
  <div class="mb-6">$$Breadcrumb$$</div>
  <h1 class="text-3xl font-bold mb-4">$$ArticleTitle$$</h1>
  <div class="prose">$$ArticleBody$$</div>
</div>

Templates/ArticlePage.htmx.cs

namespace Htmx.ApiDemo.Templates;

public sealed class ArticlePage : ArticlePageBase
{
    private readonly IHtmxComponent _breadcrumb;
    private readonly byte[] _title;
    private readonly byte[] _body;

    public ArticlePage(string category, string categorySlug, Article article)
    {
        _breadcrumb = new Components.Breadcrumb(new[]
        {
            ("Home",          "/"),
            ("Blog",          "/blog"),
            (category,        $"/blog/{categorySlug}"),
            (article.Title,   ""),   // current page
        });

        _title = System.Net.WebUtility.HtmlEncode(article.Title).ToUtf8Bytes();
        _body  = article.HtmlContent.ToUtf8Bytes();
    }

    protected override void RenderBreadcrumb(HtmxRenderContext ctx)
        => _breadcrumb.Render(ctx.Next());
    protected override void RenderArticleTitle(HtmxRenderContext ctx)
        => ctx.Writer.WriteUtf8(_title);
    protected override void RenderArticleBody(HtmxRenderContext ctx)
        => ctx.Writer.WriteUtf8(_body);
}

GET handler

[Handler]
[MapGet("/blog/{category}/{slug}")]
public static partial class GetArticleHandler
{
    public record Query(
        [property: FromRoute] string Category,
        [property: FromRoute] string Slug);

    private static async Task<IResult> HandleAsync(
        Query q,
        HttpContext ctx,
        ArticleService articles,
        CancellationToken ct)
    {
        var article = await articles.GetBySlugAsync(q.Slug, ct);
        if (article is null) return Results.NotFound();
        var page = new ArticlePage(q.Category, q.Category.ToLower(), article);
        return await ctx.WriteHtmxPage(page, title: article.Title);
    }
}