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.
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}."]
];
}Selected: profile.
Findable tabs
Search the page for "INV-1042": the browser finds it on a hidden panel and selects its tab.
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."]
];
}With icons
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"]
];
}Padded edges
A class of yours moves the first tab in from the hairline's start.
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"]
];
}Scrollable tabs
More tabs than room: the row scrolls sideways instead of pushing the page wider than a phone.
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])]
];
}Scrollable with a fade
ScrollableFade() fades the trailing edge while there are tabs past it.
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])]
];
}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.
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"]
];
}Segmented with icons
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"]
];
}Small segmented tabs
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"]];
}Pill tabs
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"]];
}Dynamic tabs
An Action() tab is a button in the row: it adds a tab and selects nothing.
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."])
];
}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.
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"]]
];
}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, ArrowDown | Selects the next tab that is not disabled, wrapping from the last to the first. |
| ArrowLeft, ArrowUp | Selects the previous one, wrapping from the first to the last. |
| Tab | Leaves the row for the panel the selected tab shows, which is a tab stop. |
An Action() tab | Is 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
A row of tabs and the panels it switches between.
Step | Takes | What it does |
|---|---|---|
.Findable() | bool | Lets find-in-page reach the panels not shown; a match selects its tab. |
Ui.Tabs
The row of tabs, and the owner of which one is selected.
Step | Takes | What it does |
|---|---|---|
.Variant(…) | Ui.TabsVariantDefault 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.TabsSizeBase 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() | bool | Scrolls the row sideways instead of pushing the page wider than a phone. |
.ScrollableScrollbar(…) | Ui.TabsScrollbarShow Hide Each is a step of its own too, unless an HTML tag has its name. | Whether a scrollable row shows its scrollbar. |
.ScrollableFade() | bool | Fades a scrollable row's trailing edge while there are tabs past it. |
.Value(…) | string | The selected tab's name, when the page holds it. Unset, the row keeps track itself. |
.Bind(…) | () => a string | The property the selected tab's name is read from and written to: () => Tab. |
.Live() | bool | Writes the bound property as soon as a tab is chosen, rather than with the next action. |
.OnChange(…) | a handler given the new value | Runs with the tab's name when one is selected. |
Ui.Tab
One tab: a <button role="tab">. Its children are its label.
Step | Takes | What it does |
|---|---|---|
.Name(…) | string | The 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 names | An icon before the label. |
.IconTrailing(…) | Ui.IconName — one of 317 names | An icon after the label. |
.IconVariant(…) | Ui.IconVariantOutline 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() | bool | Selects it until one is chosen. The row's own value decides instead, when it has one. |
.Action() | bool | A plain button in the row that selects nothing and the arrows pass over: "Add tab". |
.Accent() | bool | Whether it is painted in the accent when selected. On unless false. |
.Size(…) | Ui.TabsSizeBase 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() | bool | Dims it and takes it out of reach of the pointer and the arrow keys. |
.OnClick(…) | a handler, with no argument | Runs when it is pressed: after it is selected, or instead, for an Action. |
Ui.TabPanel
What the tab of the same name shows.
Step | Takes | What it does |
|---|---|---|
.Name(…) | string | The name of the tab it belongs to. |
.Selected() | bool | Shows it when no row says which tab is selected. |