Rask
Rask
v0.23.1-alpha.0.440

Components

Tabs

A row of tabs, a segmented control or pills, over panels or on their own. Switching is drawn in the browser, and the selected tab is a value like any other control's: bound, handed to a handler, or left to the row.

Drawn and behaving as Flux UI's tabs does, measured against its live docs.

Tabs

Over panels, with the selected tab's name held by the page: Value, and OnChange to hear it.

TabsExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class TabsExample : Component
{
    private string _settings = "profile";

    protected override Component? Render() =>
        Div[
            Ui.TabGroup[
                Ui.Tabs.Value(_settings).OnChange(tab => _settings = tab)[
                    Ui.Tab.Name("profile")["Profile"],
                    Ui.Tab.Name("account")["Account"],
                    Ui.Tab.Name("billing")["Billing"]
                ],
                Ui.TabPanel.Name("profile").Class("pt-4! text-sm")["Your name, your photo and what others see."],
                Ui.TabPanel.Name("account").Class("pt-4! text-sm")["Your email address and how you sign in."],
                Ui.TabPanel.Name("billing").Class("pt-4! text-sm")["Your plan and your invoices."]
            ],
            Ui.Text.Class("mt-2").Data("testid", "ui-tabs-state")[$"Selected: {_settings}."]
        ];
}
Live result
Your name, your photo and what others see.

Selected: profile.

Findable tabs

Search the page for "INV-1042": the browser finds it on a hidden panel and selects its tab.

TabsFindableExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class TabsFindableExample : Component
{
    protected override Component? Render() =>
        Ui.TabGroup.Findable()[
            Ui.Tabs[
                Ui.Tab.Name("profile").Selected()["Profile"],
                Ui.Tab.Name("account")["Account"],
                Ui.Tab.Name("billing")["Billing"]
            ],
            Ui.TabPanel.Name("profile").Class("px-2 pt-6! text-sm")["Manage your public profile and personal information."],
            Ui.TabPanel.Name("account").Class("px-2 pt-6! text-sm")["Update your email address, password, and security settings."],
            Ui.TabPanel.Name("billing").Class("px-2 pt-6! text-sm")["Invoice INV-1042 is due on September 1."]
        ];
}
Live result
Manage your public profile and personal information.

With icons

TabsIconsExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class TabsIconsExample : Component
{
    protected override Component? Render() =>
        Ui.Tabs[
            Ui.Tab.Name("profile").Icon(Ui.IconName.User)["Profile"],
            Ui.Tab.Name("account").Icon(Ui.IconName.Cog6Tooth)["Account"],
            Ui.Tab.Name("billing").Icon(Ui.IconName.Banknotes)["Billing"]
        ];
}
Live result

Padded edges

A class of yours moves the first tab in from the hairline's start.

TabsPaddedExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class TabsPaddedExample : Component
{
    protected override Component? Render() =>
        Ui.Tabs.Class("px-4")[
            Ui.Tab.Name("profile")["Profile"],
            Ui.Tab.Name("account")["Account"],
            Ui.Tab.Name("billing")["Billing"]
        ];
}
Live result

Scrollable tabs

More tabs than room: the row scrolls sideways instead of pushing the page wider than a phone.

TabsScrollableExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class TabsScrollableExample : Component
{
    private static readonly string[] Sections =
        ["Profile", "Account", "Billing", "Security", "Notifications", "Integrations", "API"];

    protected override Component? Render() =>
        Div.Class("max-w-sm")[
            Ui.Tabs.Scrollable()[Sections.Select(name => Ui.Tab.Key(name).Name(name.ToLowerInvariant())[name])]
        ];
}
Live result

Scrollable with a fade

ScrollableFade() fades the trailing edge while there are tabs past it.

TabsScrollableFadeExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class TabsScrollableFadeExample : Component
{
    private static readonly string[] Sections =
        ["Profile", "Account", "Billing", "Security", "Notifications", "Integrations", "API"];

    protected override Component? Render() =>
        Div.Class("max-w-sm")[
            Ui.Tabs.Scrollable().ScrollableFade()[Sections.Select(name => Ui.Tab.Key(name).Name(name.ToLowerInvariant())[name])]
        ];
}
Live result

Segmented tabs

A choice on its own, bound to a property. The page has the last word: the button sets the property, and the row shows it.

TabsSegmentedExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class TabsSegmentedExample : Component
{
    private string View { get; set; } = "list";

    protected override Component? Render() =>
        Div.Class("flex flex-wrap items-center gap-4")[
            Ui.Tabs.Segmented.Bind(() => View)[
                Ui.Tab.Name("list")["List"],
                Ui.Tab.Name("board")["Board"],
                Ui.Tab.Name("timeline")["Timeline"]
            ],
            Ui.Button.Sm.Ghost.OnClick(() => View = "list")["Back to the list"]
        ];
}
Live result

Segmented with icons

TabsSegmentedIconsExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class TabsSegmentedIconsExample : Component
{
    protected override Component? Render() =>
        Ui.Tabs.Segmented[
            Ui.Tab.Icon(Ui.IconName.ListBullet)["List"],
            Ui.Tab.Icon(Ui.IconName.Squares2x2)["Board"],
            Ui.Tab.Icon(Ui.IconName.CalendarDays)["Timeline"]
        ];
}
Live result

Small segmented tabs

TabsSegmentedSmallExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class TabsSegmentedSmallExample : Component
{
    protected override Component? Render() =>
        Ui.Tabs.Segmented.Size(Ui.TabsSize.Sm)[Ui.Tab["Demo"], Ui.Tab["Code"]];
}
Live result

Pill tabs

TabsPillsExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class TabsPillsExample : Component
{
    protected override Component? Render() =>
        Ui.Tabs.Pills[Ui.Tab["List"], Ui.Tab["Board"], Ui.Tab["Timeline"]];
}
Live result

Dynamic tabs

An Action() tab is a button in the row: it adds a tab and selects nothing.

TabsDynamicExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class TabsDynamicExample : Component
{
    private readonly List<string> _tabs = ["Tab #1", "Tab #2"];

    protected override Component? Render() =>
        Ui.TabGroup[
            Ui.Tabs[
                _tabs.Select(tab => Ui.Tab.Key(tab).Name(tab)[tab]),
                Ui.Tab.Key("add").Icon(Ui.IconName.Plus).Action().OnClick(() => _tabs.Add($"Tab #{_tabs.Count + 1}"))["Add tab"]
            ],
            _tabs.Select(tab => Ui.TabPanel.Key("panel " + tab).Name(tab).Class("pt-4! text-sm")[$"What {tab} holds."])
        ];
}
Live result
What Tab #1 holds.

A disabled tab, and counts

A count is a child like any other; a disabled tab is passed over by the pointer and the arrows.

TabsDisabledExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class TabsDisabledExample : Component
{
    protected override Component? Render() =>
        Ui.Tabs[
            Ui.Tab.Name("open")["Open", Ui.Badge.Sm["12"]],
            Ui.Tab.Name("archived").Disabled()["Archived"],
            Ui.Tab.Name("failed")["Failed", Ui.Badge.Sm.Solid.Color(Ui.Color.Red)["3"]]
        ];
}
Live result

Behaviour

The selected tab is the row's value

The row's value is the selected tab's Name, where Flux has wire:model. Bind(() => Tab) two-way binds it to a property, Value(_tab).OnChange(t => _tab = t) leaves it with the page, and with neither the row keeps track itself, starting on the tab that says Selected() or else the first one that is not disabled. A tab with no Name is known by its place in the row: "0", "1"…

Bind writes the property and redraws the row and its panels; like every bound control it does not redraw the page around them. A page that shows the selected name somewhere else takes it from OnChange, which runs in every mode.

Panels

A row needs no group: a segmented List, Board, Timeline on its own is a choice the page reads from OnChange. Inside a Ui.TabGroup, write the row before its panels: a panel learns which tab is selected from the row above it. The group ties each tab to the panel of the same name, aria-controls one way and aria-labelledby the other, so a screen reader announces a panel with the words on its tab.

Every panel is rendered and the ones not shown are hidden; in a Findable() group they are hidden="until-found" instead, so the browser's find-in-page reaches them and a match selects its tab. A panel has Flux's top padding (pt-8); another is said as Flux's own examples say it, Class("pt-6!"). A count beside a label is a child, like any other content: Ui.Tab.Name("open")["Open", Ui.Badge["12"]].

Switching is the browser's

Selecting a tab is the browser's, as it is Flux's script's: a press moves aria-selected, the tab stop and data-selected, shows the tab's panel, and by itself sends the server nothing. What the page reads is the row's value, by its binding's timing: .Bind(() => Tab) writes it with the reader's next action, .Bind(() => Tab).Live() and .OnChange(…) hear it at once, and so does a Ui.Tab's own OnClick.

The page keeps the last word: set the bound property (or pass another Value) from a handler and that tab is shown, whatever the reader last pressed. That is what makes a long form in tabs work: the fields on the panels that are not shown stay in the form with what was typed into them and are submitted with it, and a save that is refused sets the tab to the one with the first mistake.

A tab switch never counts as an unsaved change in a Form.Model(m).ConfirmLeave("…") form, bound or not: the field the row writes the chosen tab into says data-rask-unguarded, because a tab is never data the reader would lose.

The keyboard

The keys are the runtime's, for every role="tablist". The arrows select as they go, as Flux's do, and only the selected tab is a tab stop. Home and End are not handled, as Flux does not handle them.

Key
What happens
ArrowRight, ArrowDownSelects the next tab that is not disabled, wrapping from the last to the first.
ArrowLeft, ArrowUpSelects the previous one, wrapping from the first to the last.
TabLeaves the row for the panel the selected tab shows, which is a tab stop.
An Action() tabIs an ordinary button in the row: a tab stop of its own, which the arrows pass over.

A tab is not a link

Flux's tab takes no href, and neither does this one: for full-page navigation Flux says to use the navbar. A row whose choice is part of the address reads it from OnChange and goes there: Ui.Tabs.Value(view).OnChange(v => Go.To(Routes.LogsPage(View: v))).

From Flux

Flux's attributes, as steps:

Flux
What happens
<flux:tabs variant="segmented" size="sm">Ui.Tabs.Segmented.Size(Ui.TabsSize.Sm)
<flux:tabs variant="pills">Ui.Tabs.Pills
<flux:tabs scrollable scrollable:fade scrollable:scrollbar="hide">Ui.Tabs.Scrollable().ScrollableFade().ScrollableScrollbar(Ui.TabsScrollbar.Hide)
<flux:tabs class="px-4">Ui.Tabs.Class("px-4")
<flux:tab icon="user" icon:trailing="chevron-down" icon:variant="solid">Ui.Tab.Icon(Ui.IconName.User).IconTrailing(Ui.IconName.ChevronDown).IconVariant(Ui.IconVariant.Solid)
<flux:tab selected>, disabled, :accent="false".Selected(), .Disabled(), .Accent(false)
<flux:tab icon="plus" wire:click="addTab" action>Ui.Tab.Icon(Ui.IconName.Plus).Action().OnClick(AddTab)
<flux:tab.group findable>Ui.TabGroup.Findable()

Reference

Ui.TabGroup

flux:tab.group

A row of tabs and the panels it switches between.

Step
Takes
What it does
.Findable()boolLets find-in-page reach the panels not shown; a match selects its tab.

Ui.Tabs

flux:tabs

The row of tabs, and the owner of which one is selected.

Step
Takes
What it does
.Variant(…)
Ui.TabsVariant
Default
Segmented
Pills

Each is a step of its own too, unless an HTML tag has its name.

How the tabs are drawn. A row on a hairline when unset.
.Size(…)
Ui.TabsSize
Base
Sm

Each is a step of its own too, unless an HTML tag has its name.

How large they are. Only a segmented row has a second size.
.Scrollable()boolScrolls the row sideways instead of pushing the page wider than a phone.
.ScrollableScrollbar(…)
Ui.TabsScrollbar
Show
Hide

Each is a step of its own too, unless an HTML tag has its name.

Whether a scrollable row shows its scrollbar.
.ScrollableFade()boolFades a scrollable row's trailing edge while there are tabs past it.
.Value(…)stringThe selected tab's name, when the page holds it. Unset, the row keeps track itself.
.Bind(…)() => a stringThe property the selected tab's name is read from and written to: () => Tab.
.Live()boolWrites the bound property as soon as a tab is chosen, rather than with the next action.
.OnChange(…)a handler given the new valueRuns with the tab's name when one is selected.

Ui.Tab

flux:tab

One tab: a <button role="tab">. Its children are its label.

Step
Takes
What it does
.Name(…)stringThe row's value when it is selected, and the panel it shows. Its place in the row, "0", when unset.
.Icon(…)Ui.IconName — one of 317 namesAn icon before the label.
.IconTrailing(…)Ui.IconName — one of 317 namesAn icon after the label.
.IconVariant(…)
Ui.IconVariant
Outline
Solid
Mini
Micro

Each is a step of its own too, unless an HTML tag has its name.

Which drawing of the icons. Outline, or mini in a segmented row, when unset.
.Selected()boolSelects it until one is chosen. The row's own value decides instead, when it has one.
.Action()boolA plain button in the row that selects nothing and the arrows pass over: "Add tab".
.Accent()boolWhether it is painted in the accent when selected. On unless false.
.Size(…)
Ui.TabsSize
Base
Sm

Each is a step of its own too, unless an HTML tag has its name.

Its size in a segmented row. The row's when unset.
.Disabled()boolDims it and takes it out of reach of the pointer and the arrow keys.
.OnClick(…)a handler, with no argumentRuns when it is pressed: after it is selected, or instead, for an Action.

Ui.TabPanel

flux:tab.panel

What the tab of the same name shows.

Step
Takes
What it does
.Name(…)stringThe name of the tab it belongs to.
.Selected()boolShows it when no row says which tab is selected.