Skip to content
IzzyBlazor 0.1.0-alpha.18

Enter all results ↑ ↓ move Esc close

Tabs

Panels that switch with no script, or links to pages.

An interactive page: C# runs over a Blazor Server connection.

Usage

razor
<Tabs Label="Account">
    <Tab Label="Profile" Icon="Lucide.User">...</Tab>
    <Tab Label="Billing" Badge="2">...</Tab>
</Tabs>

@* Links to pages of their own. *@
<Tabs Label="Settings">
    <TabLink Href="settings/profile" Label="Profile" />
    <TabLink Href="settings/billing" Label="Billing" />
</Tabs>

Picking a tab

Use Selected for the tab that opens first, and @bind-Selected to follow it from C# on interactive pages. A tab's value is its Value, or its label in lower case with dashes (billing-history).

Preview

Profile

Your name, photo and the email people see.

Billing

Your plan, payment method and invoices.

Security

Your password, passkeys and sessions.

C# has the tab as profile

Code

Modes.razor
@* The same Tabs in each mode: radios and CSS with no script, ARIA tabs with IzzyBlazor's script,
   and C# told of the change on an interactive page. *@
<Stack Gap="3" Grow>
    <Tabs @bind-Selected="_tab" Label="Account">
        <Tab Label="Profile" Icon="Lucide.User">
            <TextBlock Color="Color.Muted">Your name, photo and the email people see.</TextBlock>
        </Tab>
        <Tab Label="Billing" Icon="Lucide.CreditCard">
            <TextBlock Color="Color.Muted">Your plan, payment method and invoices.</TextBlock>
        </Tab>
        <Tab Label="Security" Icon="Lucide.Shield">
            <TextBlock Color="Color.Muted">Your password, passkeys and sessions.</TextBlock>
        </Tab>
    </Tabs>
    <TextBlock Size="TextSize.Sm" Color="Color.Muted">C# has the tab as <Code>@(_tab ?? "profile")</Code></TextBlock>
</Stack>

@code {
    private string? _tab;
}

Live: @bind-Selected tells C# each time the tab changes.

Every panel is rendered with the page, and picking a tab only shows another, so it's instant and keeps what's typed. Content that's slow to build or rarely opened is better on a page of its own, behind a TabLink.

Variants, icons and badges

Use Variant for an underline (the default), soft tabs or a segmented control. Each Tab can have an Icon and a Badge, or be Disabled.

Preview

Line

Overview

Builds, traffic and errors at a glance.

Activity

Three new deploys since yesterday.

Billing

Available on paid plans.

Settings

Domains, environment variables and members.

Soft

Overview

Builds, traffic and errors at a glance.

Activity

Three new deploys since yesterday.

Billing

Available on paid plans.

Settings

Domains, environment variables and members.

Segmented

Overview

Builds, traffic and errors at a glance.

Activity

Three new deploys since yesterday.

Billing

Available on paid plans.

Settings

Domains, environment variables and members.

Code

Variants.razor
<Stack Gap="6" Grow>
    @foreach (var variant in Enum.GetValues<TabsVariant>())
    {
        <Stack Gap="2">
            <TextBlock Size="TextSize.Sm" Color="Color.Muted">@variant</TextBlock>
            <Tabs Variant="variant" Label="@($"Project ({variant})")">
                <Tab Label="Overview" Icon="Lucide.LayoutDashboard">
                    <TextBlock>Builds, traffic and errors at a glance.</TextBlock>
                </Tab>
                <Tab Label="Activity" Badge="3">
                    <TextBlock>Three new deploys since yesterday.</TextBlock>
                </Tab>
                <Tab Label="Billing" Disabled>Available on paid plans.</Tab>
                <Tab Label="Settings" Icon="Lucide.Settings">
                    <TextBlock>Domains, environment variables and members.</TextBlock>
                </Tab>
            </Tabs>
        </Stack>
    }
</Stack>

Sizes and full width

Use Size to match the buttons and fields around the tabs, and FullWidth to share the row between them.

Preview

Day

412 visits today.

Week

2,980 visits this week.

Month

12,406 visits this month.

Day

412 visits today.

Week

2,980 visits this week.

Month

12,406 visits this month.

Day

412 visits today.

Week

2,980 visits this week.

Month

12,406 visits this month.

Code

Sizes.razor
<Stack Gap="5" Grow>
    <Tabs Size="ControlSize.Sm" Variant="TabsVariant.Segmented" FullWidth Label="Range (small)">
        <Tab Label="Day">412 visits today.</Tab>
        <Tab Label="Week">2,980 visits this week.</Tab>
        <Tab Label="Month">12,406 visits this month.</Tab>
    </Tabs>
    <Tabs Variant="TabsVariant.Segmented" FullWidth Label="Range">
        <Tab Label="Day">412 visits today.</Tab>
        <Tab Label="Week">2,980 visits this week.</Tab>
        <Tab Label="Month">12,406 visits this month.</Tab>
    </Tabs>
    <Tabs Size="ControlSize.Lg" Variant="TabsVariant.Segmented" FullWidth Label="Range (large)">
        <Tab Label="Day">412 visits today.</Tab>
        <Tab Label="Week">2,980 visits this week.</Tab>
        <Tab Label="Month">12,406 visits this month.</Tab>
    </Tabs>
</Stack>

Vertical

Use Orientation="Orientation.Vertical" to put the tabs in a column beside the panels from 640px up; on phones they stay a row above, which scrolls when it doesn't fit.

Preview

General

General

Name, description and visibility.

Members

Members

Four people have access to this project.

Integrations

Integrations

GitHub and Slack are connected.

Danger zone

Danger zone

Transfer or delete the project.

Code

Vertical.razor
<Tabs Orientation="Orientation.Vertical" Label="Project settings" Grow>
    <Tab Label="General" Icon="Lucide.Settings">
        <TextBlock Weight="FontWeight.Medium">General</TextBlock>
        <TextBlock Size="TextSize.Sm" Color="Color.Muted">Name, description and visibility.</TextBlock>
    </Tab>
    <Tab Label="Members" Icon="Lucide.UsersRound" Badge="4">
        <TextBlock Weight="FontWeight.Medium">Members</TextBlock>
        <TextBlock Size="TextSize.Sm" Color="Color.Muted">Four people have access to this project.</TextBlock>
    </Tab>
    <Tab Label="Integrations" Icon="Lucide.Plug">
        <TextBlock Weight="FontWeight.Medium">Integrations</TextBlock>
        <TextBlock Size="TextSize.Sm" Color="Color.Muted">GitHub and Slack are connected.</TextBlock>
    </Tab>
    <Tab Label="Danger zone" Icon="Lucide.TriangleAlert">
        <TextBlock Weight="FontWeight.Medium">Danger zone</TextBlock>
        <TextBlock Size="TextSize.Sm" Color="Color.Muted">Transfer or delete the project.</TextBlock>
    </Tab>
</Tabs>

In the address

Use QueryKey to keep the selected tab in the query string, so a link to ?tab=billing opens Billing and a copied address keeps the tab. The address wins over Selected.

Preview

Profile

Your name, photo and the email people see.

Billing

Your plan, payment method and invoices.

Security

Your password, passkeys and sessions.

Code

Url.razor
@inject NavigationManager Navigation

<Stack Gap="3" Grow>
    <Tabs QueryKey="tab" Label="Account">
        <Tab Label="Profile" Icon="Lucide.User">
            <TextBlock Color="Color.Muted">Your name, photo and the email people see.</TextBlock>
        </Tab>
        <Tab Label="Billing" Icon="Lucide.CreditCard">
            <TextBlock Color="Color.Muted">Your plan, payment method and invoices.</TextBlock>
        </Tab>
        <Tab Label="Security" Icon="Lucide.ShieldCheck">
            <TextBlock Color="Color.Muted">Your password, passkeys and sessions.</TextBlock>
        </Tab>
    </Tabs>
    <Row>
        <Button Href="@Navigation.GetUriWithQueryParameter("tab", "billing")" Size="ControlSize.Sm"
                Variant="ButtonVariant.Outline" Accent="Accent.Neutral" StartIcon="Lucide.Link">Link to Billing</Button>
    </Row>
</Stack>

In a form

Put Tabs inside a Form to split a long form: every panel is part of it, so every field posts. When the server finds errors, as here with NativeValidation="false", the first tab with errors opens, unless the open one has some, and each tab shows how many it has.

Preview

Code

AccountForm.razor
@using System.ComponentModel.DataAnnotations

@* Every panel is inside the form, so every field posts. NativeValidation="false" lets the
   server check, so Save opens the first tab with errors and each tab counts its own. *@
<Form Model="Model" FormName="tabs-account-form" OnValidSubmit="Save" NativeValidation="false" MaxWidth="36.Rem">
    <Heading Level="2" Size="TextSize.Xl">Account details</Heading>
    <FormErrors />
    <Tabs Label="Account details">
        <Tab Label="Profile" Icon="Lucide.User">
            <Stack Gap="4">
                <TextField @bind-Value="Model!.Name" AutoComplete="name" />
                <TextField @bind-Value="Model!.Email" AutoComplete="email" />
            </Stack>
        </Tab>
        <Tab Label="Address" Icon="Lucide.MapPin">
            <Stack Gap="4">
                <TextField @bind-Value="Model!.Street" AutoComplete="street-address" />
                <Row Gap="3">
                    <TextField @bind-Value="Model!.City" AutoComplete="address-level2" Grow />
                    <TextField @bind-Value="Model!.PostalCode" AutoComplete="postal-code" MaxWidth="9.Rem" />
                </Row>
            </Stack>
        </Tab>
    </Tabs>
    <FormActions><Button Submit>Save</Button></FormActions>
    @if (_saved)
    {
        <Alert Status="Status.Success">Saved: @Model!.Name, @Model.City.</Alert>
    }
</Form>

@code {
    [SupplyParameterFromForm(FormName = "tabs-account-form")]
    private Account? Model { get; set; }

    private bool _saved;

    // The profile is filled in and the address isn't, so Save opens the Address tab.
    protected override void OnInitialized() => Model ??= new() { Name = "Ada Lovelace", Email = "ada@example.com" };

    private void Save() => _saved = true;

    public sealed class Account
    {
        [Required(ErrorMessage = "Enter your name")]
        public string? Name { get; set; }

        [Required(ErrorMessage = "Enter your email"), EmailAddress(ErrorMessage = "Enter an email like name@example.com")]
        public string? Email { get; set; }

        [Required(ErrorMessage = "Enter a street address")]
        public string? Street { get; set; }

        [Required(ErrorMessage = "Enter a city")]
        public string? City { get; set; }

        [Required(ErrorMessage = "Enter a postal code"), Display(Name = "Postal code")]
        public string? PostalCode { get; set; }
    }
}

Live: Save runs C# and opens the Address tab; each tab's count follows its errors as you fix them.

With the browser's own checks on (the default), the post waits until they pass, and the script opens the tab of the first field the browser stops at, so its message shows.

Panel padding

Use ContentPadding to pad every panel on all sides, and a Tab's own ContentPadding="0" to run a table edge to edge.

Preview

Summary

Three deploys today, all healthy. The last one finished 12 minutes ago.

History

Deploys today
CommitAuthorTime
a1f9c3eAda Lovelace14:02
7be20d4Grace Hopper11:47
c03e8a1Alan Turing09:15

Code

Padding.razor
@* ContentPadding pads every panel on all sides (the tab list keeps its place), so the text sits
   clear of the border; the History tab sets its own 0, so the table, without its own border,
   runs edge to edge. *@
<Box Border="1" Radius="Radius.Lg" Grow MaxWidth="34.Rem">
    <Tabs Label="Deploys" ContentPadding="4">
        <Tab Label="Summary" Icon="Lucide.Activity">
            <TextBlock>Three deploys today, all healthy. The last one finished 12 minutes ago.</TextBlock>
        </Tab>
        <Tab Label="History" Icon="Lucide.Clock" ContentPadding="0">
            <Table Items="Deploys" Caption="Deploys today" Bordered="false">
                <PropertyColumn Property="d => d.Commit" />
                <PropertyColumn Property="d => d.Author" />
                <PropertyColumn Property="d => d.Time" />
            </Table>
        </Tab>
    </Tabs>
</Box>

Use TabLink in place of Tab for sections that are pages of their own: the tabs are a nav of plain links, and the one for the current address is marked on the server.

A tab stays current on the pages below its link, so a settings tab is current on settings/billing too; Match="NavLinkMatch.All" stops that. Tabs for one page told apart by the query, as here, mark one at a time.

Parts

Written inside Tabs. The API page lists their parameters.

  • Tab: one tab of a Tabs: its label goes in the tab list and its content in a panel.
  • TabLink: a tab that's a link to its own page, for sections split across routes (/settings/profile, /settings/billing).

Mode support

What works in each mode, Tabs
FeatureOffStaticLive
Switch panels, keeping what's typedYesYesYes
Heard as tabs, with Home and EndNoYesYes
Open a tab from a ?tab= linkYesYesYes
The address follows the picked tabNoYesYes
In a form: error counts, the tab with errors opensNoYesYes
Selected bound to C#NoNoYes
Tabs as links (TabLink)YesYesYes

Off: JavaScript disabled. Static: a static SSR page with IzzyBlazor's script. Live: an interactive page (Server, WebAssembly or Auto).

Accessibility

  • With script, the list is ARIA tabs (tablist, tab, tabpanel, aria-selected) with only the selected tab in the tab order; with JavaScript off it's heard as a radio group.
  • Arrow keys select the next or previous tab, skipping disabled ones; with script, Home and End go to the first and last, and vertical tabs laid out as a column use Up and Down.
  • In a form with JavaScript off, there are no tabs: every panel shows in order, each after its tab's label as plain text, so every field can be filled in.
  • With script, each panel is named by its tab and takes focus, so Tab moves from the tab to the panel.
  • Label names the tab list (or the nav of TabLinks). It's optional, as in ARIA and other libraries, with no default; set it when the tabs' purpose isn't clear from the heading before them, and give each nav of TabLinks its own when a page has more than one.
  • In a form, with script, a tab whose fields have errors shows a count, read as part of its name ("Billing (1 error)").
  • TabLinks render a nav of plain links that Tab moves between (no arrow keys); the current page's link has aria-current, and a disabled one is a link with no href, announced as disabled (aria-disabled).

Parameters

Tabs parameters
PropTypeDefault
@bind-SelectedThe selected tab's Value.string?computed
LabelThe tab list's (or the TabLinks' nav's) name for screen readers, such as "Account settings".string?
QueryKeyKeeps the selected tab in the URL under this query-string key, such as tab: a link to ?tab=billing opens that tab, and with JavaScript switching tabs updates the URL.string?
VariantHow the tabs look.TabsVariantTabsVariant.Line
SizeTab height and text size, shared with buttons and fields.ControlSizeControlSize.Md
FullWidthStretches the tabs to share the full width (or, vertical, the list's width).boolfalse
ContentPaddingPadding on every side of each panel, in spacing steps, in place of the default space above and below (the default has none at the sides).Responsive<int>
OrientationA row above the panels (the default), or a column beside them from 640px up.OrientationOrientation.Horizontal
IdThe element id; tab and panel ids build on it.string?
ChildContentThe Tabs.RenderFragment?

See also