b530bb8c97
Co-authored-by: Copilot <copilot@github.com>
200 lines
6.1 KiB
Markdown
200 lines
6.1 KiB
Markdown
# Tabs
|
|
|
|
A row of clickable tabs that each reveal different content. Only one tab is visible at a time. Think of it like a filing cabinet with labelled dividers — you flip between sections without leaving the page.
|
|
|
|
---
|
|
|
|
## Quick example
|
|
|
|
```csharp
|
|
new Tabs(
|
|
id: "settings-tabs",
|
|
tabs: new[]
|
|
{
|
|
("general", "General", "<p>General settings here.</p>"),
|
|
("security", "Security", "<p>Password and 2FA here.</p>"),
|
|
("billing", "Billing", "<p>Payment details here.</p>"),
|
|
})
|
|
```
|
|
|
|
The first tab is active by default.
|
|
|
|
---
|
|
|
|
## All the options
|
|
|
|
```csharp
|
|
public Tabs(
|
|
string id,
|
|
IEnumerable<(string Id, string Label, string Content)> tabs)
|
|
```
|
|
|
|
| Parameter | What it does |
|
|
|---|---|
|
|
| `id` | A unique identifier for this tabs widget. Required if you have more than one `Tabs` on the same page. |
|
|
| `tabs` | The list of tabs. Each is a `(Id, Label, Content)` tuple. |
|
|
|
|
**Tab tuple fields:**
|
|
|
|
| Field | What it does |
|
|
|---|---|
|
|
| `Id` | A unique identifier for this tab within the widget. Used internally to link the trigger to the panel. |
|
|
| `Label` | The text shown on the tab button. |
|
|
| `Content` | The HTML content shown when this tab is active. |
|
|
|
|
---
|
|
|
|
## Real-world examples
|
|
|
|
### User profile page with tabbed sections
|
|
|
|
```csharp
|
|
new Tabs(
|
|
id: "profile-tabs",
|
|
tabs: new[]
|
|
{
|
|
("overview", "Overview", $"<p>Joined {user.CreatedAt:MMMM yyyy}</p>"),
|
|
("activity", "Activity", activityHtml),
|
|
("settings", "Settings", settingsFormHtml),
|
|
})
|
|
```
|
|
|
|
### Tab containing a full component
|
|
|
|
Pre-render inner components to HTML strings before embedding them:
|
|
|
|
```csharp
|
|
string Render(IHtmxComponent c)
|
|
{
|
|
var buf = new System.Buffers.ArrayBufferWriter<byte>();
|
|
c.Render(new HtmxRenderContext(buf));
|
|
return System.Text.Encoding.UTF8.GetString(buf.WrittenSpan);
|
|
}
|
|
|
|
new Tabs(
|
|
id: "report",
|
|
tabs: new[]
|
|
{
|
|
("table", "Table", Render(new Table(headers: cols, rows: rows))),
|
|
("summary", "Summary", summaryHtml),
|
|
})
|
|
```
|
|
|
|
### Code samples in multiple languages
|
|
|
|
```csharp
|
|
new Tabs(
|
|
id: "code-example",
|
|
tabs: new[]
|
|
{
|
|
("csharp", "C#", "<pre><code>var x = 42;</code></pre>"),
|
|
("fsharp", "F#", "<pre><code>let x = 42</code></pre>"),
|
|
})
|
|
```
|
|
|
|
---
|
|
|
|
## How it works
|
|
|
|
All tab panels are present in the HTML on page load. JavaScript in `components.js` hides all but the first using the HTML `hidden` attribute. When a tab button is clicked, its matching panel has `hidden` removed and all others get it added back. No server request is made — this is pure client-side switching.
|
|
{
|
|
("summary", "Summary", "<p>High level numbers.</p>"),
|
|
("detail", "Detail", tableHtml),
|
|
})
|
|
```
|
|
|
|
### Multiple independent tab groups
|
|
|
|
```csharp
|
|
new Tabs(id: "tabs-a", tabs: setA)
|
|
new Tabs(id: "tabs-b", tabs: setB)
|
|
```
|
|
|
|
The `id` scopes JS initialization — each Tabs instance is independent.
|
|
|
|
---
|
|
|
|
## Tips and tricks
|
|
|
|
- The `Id` of each tab tuple is used as the `data-tab` attribute — keep it URL-safe and unique within the instance.
|
|
- The first tab is always activated on page load regardless of which tab was active before navigation.
|
|
- Tab `Content` is raw HTML — HTML-encode any user-supplied values.
|
|
- For lazy-loaded tab content, place HTMX attributes in the `Content` string and use `hx-trigger="revealed"` to load content when the panel becomes visible.
|
|
- Tabs do not push to the URL hash by default. If you need deep-linkable tabs, listen to the `click` event on `.tabs-trigger` elements and update `location.hash`.
|
|
- Tabs do not push to the URL hash by default. If you need deep-linkable tabs, listen to the `click` event on `.tabs-trigger` elements and update `location.hash`.
|
|
|
|
---
|
|
|
|
## Complete page example
|
|
|
|
**`Templates/ProfileSettingsPage.htmx`**
|
|
```html
|
|
<div class="max-w-2xl mx-auto py-10">
|
|
<h1 class="text-2xl font-bold mb-6">Profile settings</h1>
|
|
$$SettingsTabs$$
|
|
</div>
|
|
```
|
|
|
|
**`Templates/ProfileSettingsPage.htmx.cs`**
|
|
```csharp
|
|
namespace Htmx.ApiDemo.Templates;
|
|
|
|
public sealed class ProfileSettingsPage : ProfileSettingsPageBase
|
|
{
|
|
private readonly IHtmxComponent _tabs;
|
|
|
|
public ProfileSettingsPage(ApplicationUser user, IAntiforgery af, HttpContext ctx)
|
|
{
|
|
var tokens = af.GetAndStoreTokens(ctx);
|
|
var afHtml = $"""<input type="hidden" name="{tokens.FormFieldName}" value="{tokens.RequestToken}">""";
|
|
|
|
// Build each tab's content as raw HTML strings rendered into the Tabs component
|
|
var generalContent = $"""
|
|
<form method="post" action="/profile/general">
|
|
{afHtml}
|
|
<label class="block mb-1 text-sm">Display name</label>
|
|
<input name="displayName" value="{System.Net.WebUtility.HtmlEncode(user.DisplayName ?? "")}"
|
|
class="input mb-4 w-full">
|
|
<button class="btn" type="submit">Save</button>
|
|
</form>
|
|
""";
|
|
|
|
var securityContent = $"""
|
|
<form method="post" action="/profile/password">
|
|
{afHtml}
|
|
<label class="block mb-1 text-sm">New password</label>
|
|
<input name="newPassword" type="password" class="input mb-4 w-full">
|
|
<button class="btn" type="submit">Change password</button>
|
|
</form>
|
|
""";
|
|
|
|
_tabs = new Components.Tabs(
|
|
defaultValue: "general",
|
|
tabs: new[]
|
|
{
|
|
new TabItem(Value: "general", Label: "General", Content: generalContent),
|
|
new TabItem(Value: "security", Label: "Security", Content: securityContent),
|
|
});
|
|
}
|
|
|
|
protected override void RenderSettingsTabs(HtmxRenderContext ctx) => _tabs.Render(ctx.Next());
|
|
}
|
|
```
|
|
|
|
**GET handler**
|
|
```csharp
|
|
[Handler]
|
|
[MapGet("/profile")]
|
|
public static partial class GetProfileSettingsHandler
|
|
{
|
|
public record Query();
|
|
private static async Task<IResult> HandleAsync(
|
|
Query _, HttpContext ctx, MongoDbService db, IAntiforgery af, CancellationToken ct)
|
|
{
|
|
var user = await db.GetCurrentUserAsync(ctx, ct);
|
|
return await ctx.WriteHtmxPage(
|
|
new ProfileSettingsPage(user, af, ctx), title: "Profile settings");
|
|
}
|
|
}
|
|
```
|