Files
Htmx/docs/Components/Breadcrumb.md
T
2026-05-05 23:55:26 +05:00

4.5 KiB

Breadcrumb

A "you are here" trail — a row of links showing how the user got to the current page. Like breadcrumbs leading back through a forest.


Quick example

new Breadcrumb(new[]
{
    ("Home",     "/"),
    ("Settings", "/settings"),
    ("Profile",  ""),   // current page
})

The last item is always the current page. Its link is ignored — the component automatically renders it as plain text with full colour instead of a dimmed link.


All the options

public Breadcrumb(IEnumerable<(string Label, string Href)> items)
Parameter What it does
items An ordered list of (Label, Href) pairs from root to current page.

Two rules:

  • The last item is always rendered as plain text (current page). Its Href is ignored.
  • Any non-last item with an empty Href renders as a plain <span> — useful for non-navigable category labels.

Real-world examples

Three-level app navigation

new Breadcrumb(new[]
{
    ("Home",    "/"),
    ("Reports", "/reports"),
    ("Monthly", ""),   // current — href not needed
})

Built dynamically from a category tree

var crumbs = categoryPath
    .Select((cat, i) => (cat.Name, i < categoryPath.Count - 1 ? cat.Url : ""))
    .ToArray();

new Breadcrumb(crumbs)

Inside a page

<!-- ArticlePage.htmx -->
<div class="mb-6">$$Breadcrumb$$</div>
<h1 class="text-3xl font-bold">$$ArticleTitle$$</h1>
// ArticlePage.htmx.cs
public sealed class ArticlePage : ArticlePageBase
{
    private readonly IHtmxComponent _breadcrumb;
    private readonly byte[] _titleData;

    public ArticlePage(string articleTitle, string categoryName, string categoryUrl)
    {
        _titleData   = articleTitle.ToUtf8Bytes();
        _breadcrumb  = new Breadcrumb(new[]
        {
            ("Home",        "/"),
            (categoryName,  categoryUrl),
            (articleTitle,  ""),
        });
    }

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

    protected override void RenderArticleTitle(HtmxRenderContext ctx)
        => ctx.Writer.WriteUtf8(_titleData);
}

How it works

Each item renders as a <li> inside an <ol> inside a <nav aria-label="Breadcrumb">. All items except the last are rendered as <a> links; the last is a <span>. Between items the component inserts a small SVG chevron that is marked aria-hidden so screen readers skip it and only announce the text labels.


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);
    }
}