f6ae86617c
Co-authored-by: Copilot <copilot@github.com>
4.5 KiB
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
Hrefis ignored. - Any non-last item with an empty
Hrefrenders 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);
}
}