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

176 lines
4.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```csharp
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
```csharp
new Breadcrumb(new[]
{
("Home", "/"),
("Settings", "/settings"),
("Profile", ""), // current page — href is ignored for the last item
})
```
### Dynamic breadcrumb from a data path
```csharp
// 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
```html
<!-- MyPage.htmx -->
<div class="mb-6">
$$Breadcrumb$$
</div>
```
```csharp
// 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`**
```html
<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`**
```csharp
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**
```csharp
[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);
}
}
```