# 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", "

General settings here.

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

Password and 2FA here.

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

Payment details here.

"), }) ``` 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", $"

Joined {user.CreatedAt:MMMM yyyy}

"), ("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(); 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#", "
var x = 42;
"), ("fsharp", "F#", "
let x = 42
"), }) ``` --- ## 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", "

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