Rask
Rask
v0.23.1-alpha.0.440

Components

Table

Columns, rows and cells, part for part as Flux's. The table sorts nothing and pages nothing: the page keeps the sorted column, its direction and the page number, and the table draws them.

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

Table

A heading row and a row per record. A cell holding a badge drops its own vertical padding.

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

public sealed partial class TableExample : Component
{
    private static readonly (string Customer, string Date, string Status, Ui.Color? Hue, string Amount)[] Orders =
    [
        ("Lindsey Aminoff", "Jul 29", "Paid", Ui.Color.Green, "$49.00"),
        ("Hanna Lubin", "Jul 28", "Paid", Ui.Color.Green, "$312.00"),
        ("Kianna Bushevi", "Jul 30", "Refunded", null, "$132.00"),
        ("Gustavo Geidt", "Jul 27", "Paid", Ui.Color.Green, "$31.00"),
    ];

    protected override Component? Render() =>
        Ui.Table[
            Ui.TableColumns[
                Ui.TableColumn["Customer"],
                Ui.TableColumn["Date"],
                Ui.TableColumn["Status"],
                Ui.TableColumn.End["Amount"]
            ],
            Ui.TableRows[
                Orders.Select(order => Ui.TableRow.Key(order.Customer)[
                    Ui.TableCell[order.Customer],
                    Ui.TableCell[order.Date],
                    // A cell holding a badge drops its own vertical padding, so the row keeps its height.
                    Ui.TableCell.Class("py-0")[Ui.Badge.Sm.Color(order.Hue)[order.Status]],
                    Ui.TableCell.Variant(Ui.TableCellVariant.Strong).End[order.Amount]
                ])
            ]
        ];
}
Live result
Customer
Date
Status
Amount
Lindsey AminoffJul 29
Paid
$49.00
Hanna LubinJul 28
Paid
$312.00
Kianna BusheviJul 30
Refunded
$132.00
Gustavo GeidtJul 27
Paid
$31.00

Sortable and paged

Sortable() headings ask for a sort with OnSort; Paginate puts a Ui.Pagination under the rows. The page holds the column, the direction and the page.

TableSortableExample.cs
using System.Globalization;

namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class TableSortableExample : Component
{
    private const int PageSize = 4;

    private static readonly Order[] Orders =
    [
        new(428, "Lindsey Aminoff", new(2026, 7, 29), "Paid", 49m),
        new(427, "Hanna Lubin", new(2026, 7, 28), "Paid", 312m),
        new(426, "Kianna Bushevi", new(2026, 7, 30), "Refunded", 132m),
        new(424, "Gustavo Geidt", new(2026, 7, 27), "Paid", 31m),
        new(423, "Nolan George", new(2026, 7, 26), "Paid", 313m),
        new(421, "Desirae George", new(2026, 7, 31), "Paid", 32m),
        new(420, "Jackson Bothman", new(2026, 7, 30), "Refunded", 94m),
        new(419, "Hanna Lipshutz", new(2026, 7, 29), "Paid", 22m),
        new(418, "Alfredo Levin", new(2026, 7, 25), "Failed", 12m),
        new(417, "Zain Lubin", new(2026, 7, 28), "Paid", 91m),
    ];

    private string _sortBy = "date";
    private Ui.TableColumnDirection _direction = Ui.TableColumnDirection.Desc;
    private int _page = 1;

    protected override Component? Render() =>
        Ui.Table
            .Id("ui-orders")
            .Paginate(Ui.Pagination
                .Paginator(new UiPaginator { Page = _page, PerPage = PageSize, Total = Orders.Length })
                .OnPage(page => { _page = page; }))[
            Ui.TableColumns[
                Ui.TableColumn["Customer"],
                Column("date", "Date"),
                Column("status", "Status"),
                Column("amount", "Amount", Ui.Align.End)
            ],
            Ui.TableRows[
                Sorted().Skip((_page - 1) * PageSize).Take(PageSize).Select(order => Ui.TableRow.Key(order.Id)[
                    Ui.TableCell[order.Customer],
                    Ui.TableCell[order.Placed.ToString("MMM d", CultureInfo.InvariantCulture)],
                    Ui.TableCell.Class("py-0")[Ui.Badge.Sm.Color(Hue(order.Status))[order.Status]],
                    Ui.TableCell.Variant(Ui.TableCellVariant.Strong).End[order.Amount.ToString("C2", CultureInfo.GetCultureInfo("en-US"))]
                ])
            ]
        ];

    private Component Column(string column, string label, Ui.Align? align = null) =>
        Ui.TableColumn
            .Sortable()
            .Sorted(IsSortedBy(column))
            .Direction(_direction)
            .Align(align)
            .OnSort(() => SortBy(column))[label];

    private bool IsSortedBy(string column) => string.Equals(_sortBy, column, StringComparison.Ordinal);

    // A second click on the sorted column turns it round; a first click on another starts it ascending.
    private void SortBy(string column)
    {
        _direction = IsSortedBy(column) && _direction == Ui.TableColumnDirection.Asc
            ? Ui.TableColumnDirection.Desc
            : Ui.TableColumnDirection.Asc;
        _sortBy = column;
        _page = 1;
    }

    private IEnumerable<Order> Sorted()
    {
        Func<Order, IComparable> key = _sortBy switch
        {
            "status" => order => order.Status,
            "amount" => order => order.Amount,
            _ => order => order.Placed,
        };

        return _direction == Ui.TableColumnDirection.Asc ? Orders.OrderBy(key) : Orders.OrderByDescending(key);
    }

    private static Ui.Color? Hue(string status) => status switch
    {
        "Paid" => Ui.Color.Green,
        "Failed" => Ui.Color.Red,
        _ => null,
    };

    private sealed record Order(int Id, string Customer, DateOnly Placed, string Status, decimal Amount);
}
Live result
Customer
Desirae GeorgeJul 31
Paid
$32.00
Kianna BusheviJul 30
Refunded
$132.00
Jackson BothmanJul 30
Refunded
$94.00
Lindsey AminoffJul 29
Paid
$49.00
Showing 1 to 4 of 10 results

Bleed

The dividers run through the padding of the box the table sits in.

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

public sealed partial class TableBleedExample : Component
{
    private static readonly (string Customer, string Status, string Amount)[] Customers =
    [
        ("Lindsey Aminoff", "Paid", "$49.00"),
        ("Hanna Lubin", "Paid", "$312.00"),
        ("Kianna Bushevi", "Refunded", "$132.00"),
    ];

    // The box states how far the table may run through its padding; a card states its own.
    protected override Component? Render() =>
        Div.Class("max-w-md rounded-lg border border-zinc-200 p-4 [--ui-bleed:1rem] dark:border-zinc-700")[
            Ui.Heading["Recent customers"],
            Ui.Table.Bleed().ContainerClass("mt-4")[
                Ui.TableColumns[
                    Ui.TableColumn["Customer"],
                    Ui.TableColumn["Status"],
                    Ui.TableColumn.End["Amount"]
                ],
                Ui.TableRows[
                    Customers.Select(customer => Ui.TableRow.Key(customer.Customer)[
                        Ui.TableCell.Variant(Ui.TableCellVariant.Strong)[customer.Customer],
                        Ui.TableCell.Class("py-0")[Ui.Badge.Sm[customer.Status]],
                        Ui.TableCell.Variant(Ui.TableCellVariant.Strong).End[customer.Amount]
                    ])
                ]
            ]
        ];
}
Live result
Recent customers
Customer
Status
Amount
Lindsey Aminoff
Paid
$49.00
Hanna Lubin
Paid
$312.00
Kianna Bushevi
Refunded
$132.00

Sticky header and column

Scroll it both ways: the headings and the first column stay put, and the column casts a shadow once the others are under it.

TableStickyExample.cs
using System.Globalization;

namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class TableStickyExample : Component
{
    // Sticky parts need a surface of their own, or the rows show through them.
    private const string Surface = "bg-white dark:bg-zinc-900";

    private static readonly (int Id, string Customer, string Date, string Status, string Amount)[] Orders =
    [
        (428, "Lindsey Aminoff", "Jul 29", "Paid", "$49.00"), (427, "Hanna Lubin", "Jul 28", "Paid", "$312.00"),
        (426, "Kianna Bushevi", "Jul 30", "Refunded", "$132.00"), (424, "Gustavo Geidt", "Jul 27", "Paid", "$31.00"),
        (423, "Nolan George", "Jul 26", "Paid", "$313.00"), (421, "Desirae George", "Jul 31", "Paid", "$32.00"),
        (420, "Jackson Bothman", "Jul 30", "Refunded", "$94.00"), (419, "Hanna Lipshutz", "Jul 29", "Paid", "$22.00"),
        (418, "Alfredo Levin", "Jul 25", "Failed", "$12.00"), (417, "Zain Lubin", "Jul 28", "Paid", "$91.00"),
    ];

    protected override Component? Render() =>
        Ui.Table.ContainerClass("max-h-64 max-w-md")[
            Ui.TableColumns.Sticky().Class(Surface)[
                Ui.TableColumn.Sticky().Class(Surface)["ID"],
                Ui.TableColumn["Customer"],
                Ui.TableColumn["Email"],
                Ui.TableColumn["Date"],
                Ui.TableColumn["Status"],
                Ui.TableColumn.End["Amount"]
            ],
            Ui.TableRows[
                Orders.Select(order => Ui.TableRow.Key(order.Id)[
                    Ui.TableCell.Sticky().Class(Surface)[order.Id.ToString(CultureInfo.InvariantCulture)],
                    Ui.TableCell[order.Customer],
                    Ui.TableCell[order.Customer.Split(' ')[0].ToLowerInvariant() + "@example.com"],
                    Ui.TableCell[order.Date],
                    Ui.TableCell.Class("py-0")[Ui.Badge.Sm[order.Status]],
                    Ui.TableCell.Variant(Ui.TableCellVariant.Strong).End[order.Amount]
                ])
            ]
        ];
}
Live result
ID
Customer
Email
Date
Status
Amount
428Lindsey Aminofflindsey@example.comJul 29
Paid
$49.00
427Hanna Lubinhanna@example.comJul 28
Paid
$312.00
426Kianna Bushevikianna@example.comJul 30
Refunded
$132.00
424Gustavo Geidtgustavo@example.comJul 27
Paid
$31.00
423Nolan Georgenolan@example.comJul 26
Paid
$313.00
421Desirae Georgedesirae@example.comJul 31
Paid
$32.00
420Jackson Bothmanjackson@example.comJul 30
Refunded
$94.00
419Hanna Lipshutzhanna@example.comJul 29
Paid
$22.00
418Alfredo Levinalfredo@example.comJul 25
Failed
$12.00
417Zain Lubinzain@example.comJul 28
Paid
$91.00

Tables that hold still

Columns that stay put

A width on the table and on every column but one: paging, sorting and an empty list move nothing, and a long value ends in an ellipsis with all of it in its tooltip.

TableSteadyColumnsExample.cs
using System.Globalization;

namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class TableSteadyColumnsExample : Component
{
    private const int PageSize = 4;

    private static readonly Job[] Jobs =
    [
        new("ABC-123", "Tyre change", new(2026, 3, 2)),
        new("KLM-456", "Oil", new(2026, 3, 9)),
        new("RST-789", "MOT", new(2026, 3, 11)),
        new("XYZ-012", "Wipers", new(2026, 3, 12)),
        new("AA-BB-001", "Full service with brake pads, brake discs, a coolant flush, both wiper arms, the cabin filter and a wash, as agreed with the driver on the phone", new(2026, 4, 1)),
        new("AA-BB-002", "Windscreen replaced after the stone chip on the motorway spread across the driver's side overnight in the frost", new(2026, 4, 3)),
        new("AA-BB-003", "Gearbox", new(2026, 4, 7)),
        new("AA-BB-004", "Insurance claim: rear bumper, tailgate, both rear lights and the parking sensors, waiting on the assessor's second visit", new(2026, 4, 8)),
        new("ZZ-9", "Bulb", new(2026, 5, 20)),
        new("ZZ-10", "Battery", new(2026, 5, 21)),
    ];

    private string _sortBy = "date";
    private Ui.TableColumnDirection _direction = Ui.TableColumnDirection.Asc;
    private int _page = 1;
    private bool _hidden;

    protected override Component? Render()
    {
        var jobs = _hidden ? [] : Sorted();

        return Div.Class("grid gap-3")[
            Div[
                Ui.Button.Sm.Data("testid", "ui-fleet-toggle").OnClick(Toggle)[_hidden ? "Show the jobs" : "Hide the jobs"]
            ],
            // A width on the table, a width on every column but Job, and a floor for the phone.
            Ui.Table
                .Id("ui-fleet")
                .Class("w-full min-w-160")
                // A grid item grows to its content unless told it may not.
                .ContainerClass("min-w-0")
                .Paginate(Ui.Pagination
                    .Paginator(new UiPaginator { Page = _page, PerPage = PageSize, Total = jobs.Count })
                    .OnPage(page => { _page = page; }))[
                Ui.TableColumns[
                    Column("plate", "w-40")["Plate"],
                    Column("job", null)["Job"],
                    Column("date", "w-36")["Date"],
                    Ui.TableColumn.Class("w-24")[""]
                ],
                Ui.TableRows[
                    jobs.Skip((_page - 1) * PageSize).Take(PageSize).Select(job => Ui.TableRow.Key(job.Plate)[
                        Ui.TableCell.Variant(Ui.TableCellVariant.Strong).Class("truncate")[job.Plate],
                        Ui.TableCell.Class("truncate").Title(job.Name)[job.Name],
                        Ui.TableCell[job.Booked.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)],
                        Ui.TableCell.Class("py-0")[Ui.Button.Sm.Ghost["Open"]]
                    ])
                ]
            ]
        ];
    }

    private UiTableColumn Column(string column, string? width) =>
        Ui.TableColumn
            .Class(width)
            .Sortable()
            .Sorted(IsSortedBy(column))
            .Direction(_direction)
            .OnSort(() => SortBy(column));

    private bool IsSortedBy(string column) => string.Equals(_sortBy, column, StringComparison.Ordinal);

    private void Toggle()
    {
        _hidden = !_hidden;
        _page = 1;
    }

    private void SortBy(string column)
    {
        _direction = IsSortedBy(column) && _direction == Ui.TableColumnDirection.Asc
            ? Ui.TableColumnDirection.Desc
            : Ui.TableColumnDirection.Asc;
        _sortBy = column;
        _page = 1;
    }

    private List<Job> Sorted()
    {
        Func<Job, IComparable> key = _sortBy switch
        {
            "plate" => job => job.Plate,
            "job" => job => job.Name,
            _ => job => job.Booked,
        };

        return [.. _direction == Ui.TableColumnDirection.Asc ? Jobs.OrderBy(key) : Jobs.OrderByDescending(key)];
    }

    private sealed record Job(string Plate, string Name, DateOnly Booked);
}
Live result
ABC-123Tyre change2026-03-02
KLM-456Oil2026-03-09
RST-789MOT2026-03-11
XYZ-012Wipers2026-03-12
Showing 1 to 4 of 10 results

Paging that waits

Nothing changes until the next page has its rows. Make the read slow and the pager moves first, the rows you have dim, and the new ones take their place when they arrive.

TablePagingThatWaitsExample.cs
using System.Globalization;

namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class TablePagingThatWaitsExample : Component
{
    private const int PageSize = 5;
    private const int Count = 40;

    private static readonly string[] Customers =
    [
        "Lindsey Aminoff", "Hanna Lubin", "Kianna Bushevi", "Gustavo Geidt", "Nolan George",
        "Desirae George", "Jackson Bothman", "Hanna Lipshutz", "Alfredo Levin", "Zain Lubin",
    ];

    private int _page = 1;
    private bool _newestFirst;
    private bool _slow;
    private Invoice[] _invoices = Read(1, newestFirst: false);

    protected override Component? Render() =>
        Div.Class("grid gap-3")[
            Div[
                Ui.Button.Sm.Data("testid", "ui-invoices-slow").Loading(false).OnClick(() => { _slow = !_slow; })[
                    _slow ? "The read takes 1.5 s" : "The read is instant"
                ]
            ],
            Ui.Table
                .Id("ui-invoices")
                .Class("w-full")
                .ContainerClass("min-w-0")
                .Paginate(Ui.Pagination
                    .Paginator(new UiPaginator { Page = _page, PerPage = PageSize, Total = Count })
                    .OnPage(Show))[
                Ui.TableColumns[
                    Ui.TableColumn.Sortable().Sorted(true).Direction(Direction).OnSort(TurnRound)["Invoice"],
                    Ui.TableColumn["Customer"],
                    Ui.TableColumn.End["Amount"]
                ],
                Ui.TableRows[
                    _invoices.Select(invoice => Ui.TableRow.Key(invoice.Number)[
                        Ui.TableCell.Variant(Ui.TableCellVariant.Strong)[$"INV-{invoice.Number:0000}"],
                        Ui.TableCell[invoice.Customer],
                        Ui.TableCell.End[invoice.Amount.ToString("C", CultureInfo.GetCultureInfo("en-US"))]
                    ])
                ]
            ]
        ];

    private Ui.TableColumnDirection Direction =>
        _newestFirst ? Ui.TableColumnDirection.Desc : Ui.TableColumnDirection.Asc;

    // The pager moves now; the rows are replaced when the read is back.
    private async Task Show(int page)
    {
        _page = page;
        _invoices = await Invoices();
    }

    private async Task TurnRound()
    {
        _newestFirst = !_newestFirst;
        _page = 1;
        _invoices = await Invoices();
    }

    private async Task<Invoice[]> Invoices()
    {
        if (_slow)
        {
            await Task.Delay(TimeSpan.FromMilliseconds(1500), CancellationToken);
        }

        return Read(_page, _newestFirst);
    }

    private static Invoice[] Read(int page, bool newestFirst) =>
    [
        .. Enumerable.Range(((page - 1) * PageSize) + 1, PageSize)
            .Select(row => newestFirst ? Count + 1 - row : row)
            .Select(number => new Invoice(number, Customers[number % Customers.Length], 20m + (number * 7.5m)))
    ];

    private sealed record Invoice(int Number, string Customer, decimal Amount);
}
Live result
Customer
Amount
INV-0001Hanna Lubin$27.50
INV-0002Kianna Bushevi$35.00
INV-0003Gustavo Geidt$42.50
INV-0004Nolan George$50.00
INV-0005Desirae George$57.50
Showing 1 to 5 of 40 results

Behaviour

The page sorts and pages

The table sorts nothing and pages nothing. The page keeps the sorted column, its direction and the page number. A Sortable() heading draws its label as a button with a chevron, and OnSort fires when the heading is clicked or its button is pressed from the keyboard; Sorted and Direction are what the page sets in answer.

Strong and Desc are also HTML tags, and a tag's entry hides a step of the same name: those two values are passed as Variant(Ui.TableCellVariant.Strong) and Direction(Ui.TableColumnDirection.Desc).

A slow page change dims the rows

Nothing on the table changes until the next page has its rows: a page change, a sort or a filter that is back within 150ms is one paint, pager and rows together. When the read is slower the rows the reader has stay and dim to half strength until the new ones take their place, for at most two seconds, after which they are at full strength again and the new ones arrive when they do. There is nothing to write: Paginate(Ui.Pagination.Paginator(orders).OnPage(async page => { _page = page; _orders = await Orders.Page(page); })).

The pager and the headings stay at full strength (they are what says something is happening), the rows stay readable and clickable, and a button inside a row, a Delete with a slow handler, marks itself and leaves the table alone. No Loading prop: the runtime marks what was pressed once it has waited 200ms, and the kit's sheet dims the rows by that mark.

What waits
What happens
The pager: a page number, Previous, Next, or anything inside itdata-loading on the pager's root, written by Ui.Pagination (data-rask-loading="within"), under the table or right after it.
A sortable headingdata-loading and aria-busy on its <th>, written by Ui.TableColumn.Sortable() (data-rask-loading).
Anything else: a filter above the tabledata-loading on the table, written by you: .Data("loading", "") on the Ui.Table while its rows are stale.

Flux documents no loading treatment for a table. The model is Livewire's own: data-loading on what was pressed and has-data-loading:opacity-50 on what it replaces, so a table of your own markup can do the same with has-[[data-loading]]:opacity-50 around anything carrying data-rask-loading="within".

Keep the rows: the dim is for rows that are still there. A handler that awaits the read keeps them by itself (_orders = await … replaces them only when it is back); a page that empties them first (_orders = null) has nothing to dim, and shows its placeholder when the read is slow. A Rask.Query page keeps them with KeepPreviousData.

Columns that stay put

A table sizes its columns to what is in them, Flux's too, so they shift when a page, a sort or a filter brings other values. Give the table a width and each column but one a width, and nothing moves.

w-full is what turns the widths on. The table is table-fixed already, but a browser lays a table out by its content until the table itself has a width, and min-w-full is not one. With w-full the columns come from the heading row alone: the same to the pixel on every page, in every sort order, and with no rows.

One column has no width and takes what is left; leave out more than one and they share it equally. truncate on a cell cuts a long value with an ellipsis where its column ends, and .Title(text) puts the whole of it in the browser's tooltip. A heading's chevron changes nothing: a sorted column is as wide as an unsorted one.

min-w-160 is for the phone. When the widths add up to more than the screen, the column without one would get nothing; with a minimum (40rem: the three widths plus 15rem for the rest) the table keeps its columns and scrolls sideways inside its own box, and the page does not. ContainerClass (Flux's container:class) is for a height, max-h-80; sideways needs nothing, unless the table is an item of a grid or a flex row, which grows to its content: there the box itself becomes 40rem wide and the page scrolls instead. Say .ContainerClass("min-w-0") and it stays in.

Sticky parts and bleed

Ui.TableColumns.Sticky() holds the headings at the top of the box, and a sticky Ui.TableColumn with sticky Ui.TableCells under it holds a column at the left. Give each .Class("bg-white dark:bg-zinc-900"), or the rows show through. A sticky column casts a shadow once the others have scrolled under it; that is a CSS scroll timeline, so a browser without one shows no shadow. A sticky column takes its width like any other.

Bleed() reads --ui-bleed, 1.5rem unless the box says otherwise: Div.Class("p-4 [--ui-bleed:1rem]")[Ui.Table.Bleed()[…]]. A card states it, and its own top and bottom bleed, so a table that opens or closes a card reaches that edge too.

A class of yours wins

What the table writes by default (a cell's padding, text size, colour and alignment, the table's min-w-full, table-fixed and whitespace-nowrap) weighs nothing in the cascade, so py-0, ps-10 on a first cell, text-xs, whitespace-normal or min-w-160 does what it says wherever Tailwind sorts it. colspan and scope go through .Attributes(("colspan", "3")).

The scroll frame holds what is placed in it

The box the table scrolls in is relative, which Flux's is not: a recorded departure from Flux (owner, 2026-10-14). Flux's frame is static, so an absolutely placed child of a cell, a checkbox's hidden input or a hidden heading, escapes it and the whole page scrolls sideways on a phone. Here it stays in the frame.

Reference

Ui.Table

flux:table

A <table> in a box that scrolls, with its pager under the rows. It sorts and pages nothing itself.

Step
Takes
What it does
.Bleed()boolRuns the row dividers through the padding of the box it sits in, by --ui-bleed (1.5rem unless the box says).
.Paginate(…)a Ui.PaginationThe pager under the rows, outside the scroll area: a Ui.Pagination.
.ContainerClass(…)stringClasses for the box around the table: a height to scroll within, max-h-80, or min-w-0 in a grid.

It is one HTML element, so Id, Class, Style, Data, Aria and the events are its steps too.

Ui.TableColumns

flux:table.columns

The heading row: the <thead>.

Step
Takes
What it does
.Sticky()boolKeeps the headings in view while the rows scroll under them. Give it a background too.

It is one HTML element, so Id, Class, Style, Data, Aria and the events are its steps too.

Ui.TableColumn

flux:table.column

One heading: the <th>. Its children are its label.

Step
Takes
What it does
.Align(…)
Ui.Align
Default
Start
Center
End

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

Where the heading sits in its column. Start when unset.
.Sortable()boolMakes the heading a button with a chevron that asks for this column to be sorted.
.Sorted()boolWhether the rows are sorted by this column now: its chevron stays visible.
.Direction(…)
Ui.TableColumnDirection
Asc
Desc

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

Which way the sorted column runs. Ascending when unset.
.Sticky()boolKeeps the column in view while the others scroll sideways. Its cells say Sticky too, and all of it a background.
.OnSort(…)a handler, with no argumentRuns when a sortable heading is clicked, or its button pressed from the keyboard.

It is one HTML element, so Id, Class, Style, Data, Aria and the events are its steps too.

Ui.TableRows

flux:table.rows

The body: the <tbody>, holding a row per record.

No steps of its own: what goes inside it is its content.

It is one HTML element, so Id, Class, Style, Data, Aria and the events are its steps too.

Ui.TableRow

flux:table.row

One record: the <tr>. Key it with the record's id.

Step
Takes
What it does
.Sticky()boolKeeps the row in view while the others scroll under it.

It is one HTML element, so Id, Class, Style, Data, Aria and the events are its steps too.

Ui.TableCell

flux:table.cell

One value: the <td>.

Step
Takes
What it does
.Align(…)
Ui.Align
Default
Start
Center
End

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

Where the value sits in its column. Start when unset.
.Variant(…)
Ui.TableCellVariant
Default
Strong

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

How much the cell stands out. Muted when unset.
.Sticky()boolKeeps the cell in view while the others scroll sideways. Give it a background too.

It is one HTML element, so Id, Class, Style, Data, Aria and the events are its steps too.