# Tabs A tabbed interface. One tab panel is visible at a time. The active tab has a highlighted style; all others are hidden. Client-side JS switches panels without a server round-trip. --- ## HTML structure ``` div[id].tabs-root div.tabs-list.flex.gap-1.border-b.mb-4 ← tab button strip button.tabs-trigger[data-tab={tabId}] ← one per tab; ACTIVE/INACTIVE variant {label} div.tabs-panel[data-tab={tabId}] ← one per tab; hidden or visible {content} ``` --- ## CSS mechanics | Class | Effect | |---|---| | `tabs-trigger` | `px-4 py-2 text-sm font-medium rounded-t-md -mb-px` | | Active trigger | `bg-background border border-b-0 border-border text-foreground` | | Inactive trigger | `text-muted-foreground hover:text-foreground hover:bg-muted/40` | | `tabs-panel[hidden]` | `display: none` via the HTML `hidden` attribute | --- ## JavaScript (`initTabs` in `components.js`) Runs on `DOMContentLoaded` and `htmx:afterSwap`. **Per-instance initialization:** 1. Guard `_tabsInit` prevents double-binding 2. Reads all `.tabs-trigger` and `.tabs-panel` elements within the root 3. Activates the first tab on init (removes `hidden`, applies active class) 4. On trigger click: - Deactivate all panels (set `hidden`, downgrade trigger class to inactive) - Activate the clicked panel by matching `data-tab` attribute - Apply active class to the clicked trigger --- ## Constructor signature ```csharp public Tabs( string id, IEnumerable<(string Id, string Label, string Content)> tabs) ``` | Parameter | Description | |---|---| | `id` | Root element id — must be unique per page if multiple Tabs are rendered | | `tabs` | List of `(Id, Label, Content)` tuples; `Id` must be unique within this instance | --- ## Usage examples ### Simple tabbed content ```csharp new Tabs( id: "settings-tabs", tabs: new[] { ("general", "General", "

General settings content here.

"), ("security", "Security", "

Security settings content here.

"), ("billing", "Billing", "

Billing details here.

"), }) ``` ### HTML-rich content in a tab ```csharp new Tabs( id: "code-tabs", tabs: new[] { ("csharp", "C#", "
var x = 42;
"), ("fsharp", "F#", "
let x = 42
"), ("vb", "VB.NET", "
Dim x As Integer = 42
"), }) ``` ### Embedding a full component in a tab ```csharp // Pre-render the inner component to HTML string var buf = new System.Buffers.ArrayBufferWriter(); new Table(headers: cols, rows: data).Render(new HtmxRenderContext(buf)); var tableHtml = System.Text.Encoding.UTF8.GetString(buf.WrittenSpan); new Tabs( id: "report", tabs: new[] { ("summary", "Summary", "

High level numbers.

"), ("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

Profile settings

$$SettingsTabs$$
``` **`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 = $""""""; // Build each tab's content as raw HTML strings rendered into the Tabs component var generalContent = $"""
{afHtml}
"""; var securityContent = $"""
{afHtml}
"""; _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 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"); } } ```