Files
Htmx/docs/Components/Tabs.md
T
2026-05-05 23:55:26 +05:00

6.1 KiB

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

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

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

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:

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

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

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

<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

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

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