Table
JavaScript off: what the HTML and CSS do on their own.
Usage
<Table Items="orders" PageSize="10" Caption="Orders">
<PropertyColumn Property="o => o.Customer" Sortable Filterable />
<PropertyColumn Property="o => o.Total" Format="C" Sortable />
<TemplateColumn Title="Status">
<Badge>context.Status</Badge>
</TemplateColumn>
</Table>Sorting and paging
Use Sortable on a column and PageSize on the table. Headers and page numbers are links that set ?sort= and ?page=, so the table works with no script, survives a reload and can be shared.
Preview
Code
@* Sorting and paging through the URL: headers and page numbers are links, so the table works with
JavaScript off, survives a reload and can be shared. *@
<Table Items="Orders.All" PageSize="5" PageSizeOptions="[5, 10]" InitialSort="-date" Caption="Orders" Grow>
<PropertyColumn Property="o => o.Number" Title="Order" />
<PropertyColumn Property="o => o.Customer" Sortable />
<PropertyColumn Property="o => o.Date" Format="MMM d" Sortable />
<TemplateColumn Title="Status" SortBy="o => o.Status" SortKey="status" Sortable>
<Badge Status="Orders.StatusOf(context.Status)" Dot>context.Status</Badge>
</TemplateColumn>
<PropertyColumn Property="o => o.Total" Format="C" Sortable />
</Table>Off: Headers and page numbers are links: each click loads the page again.
InitialSort sets the order before anyone picks one ("-date" for newest first), and PageSizeOptions adds a "Rows per page" menu.
Column filters
Use Filterable on a column for a filter in its header. Text matches anywhere, numbers exactly, a date its day, and enums and booleans get a menu.
Code
@* Column filters: a funnel in each Filterable header opens the filter; active filters show as tags
above the table. Text matches anywhere, enums get a menu, numbers match exactly. *@
<Table Items="Orders.All" PageSize="5" InitialSort="-date" Caption="Filtered orders" QueryPrefix="orders-" Grow>
<PropertyColumn Property="o => o.Number" Title="Order" />
<PropertyColumn Property="o => o.Customer" Sortable Filterable />
<PropertyColumn Property="o => o.Status" Sortable Filterable />
<PropertyColumn Property="o => o.Total" Format="C" Sortable Filterable />
</Table>Off: Press Enter or Apply in the filter to submit it.
Filters go in the URL as ?f-customer=ada (here ?orders-f-customer=ada: this table has a QueryPrefix, so it sorts and pages apart from the one above, as under Several tables on a page). FilterMultiple lets an enum column match several values, and FilterPlacement.Row puts the filters in a row under the headers. For filters of your own, use a FilterForm.
Selection
Use Selectable with an ItemKey for a checkbox per row. In a form, the checked rows post their keys, so this works with JavaScript off.
Preview
Code
@* Selection in a form: each checked row posts its ItemKey, so Archive works with JavaScript off. A
footer row shows the total. *@
<form method="post" @onsubmit="Archive" @formname="archive" style="width: 100%">
<AntiforgeryToken />
<Stack Gap="3">
<Table Items="Recent" Caption="Recent orders" Selectable ItemKey="o => o.Number.ToString()"
RowLabel='o => $"order {o.Number}"' @bind-SelectedKeys="_selected">
<PropertyColumn Property="o => o.Number" Title="Order" FooterText="Total" />
<PropertyColumn Property="o => o.Customer" />
<PropertyColumn Property="o => o.Total" Format="C" FooterText="Recent.Sum(o => o.Total).ToString("C")" />
</Table>
<Row Gap="3" Align="Align.Center" Wrap>
<Button Submit Variant="ButtonVariant.Outline" Accent="Accent.Neutral" StartIcon="Lucide.Archive">Archive selected</Button>
if (_archived is not null)
{
<TextBlock Size="TextSize.Sm" Color="Color.Muted">
Archived: (_archived.Count == 0 ? "nothing selected" : string.Join(", ", _archived.Select(k => "#" + k)))
</TextBlock>
}
</Row>
</Stack>
</form>
{
private static readonly IReadOnlyList<Order> Recent = Orders.All.Take(5).ToList();
// Static pages: the checked rows post as "selected".
[SupplyParameterFromForm(FormName = "archive", Name = "selected")]
private string[]? Posted { get; set; }
// Interactive pages: bound to the checkboxes.
private IReadOnlyCollection<string>? _selected;
private IReadOnlyCollection<string>? _archived;
protected override void OnInitialized() => _selected = Posted;
private void Archive() => _archived = (IReadOnlyCollection<string>?)Posted ?? _selected ?? [];
}Off: Checked rows post their keys with the form.
Read the keys with [SupplyParameterFromForm(Name = "selected")] on a static page, or @bind-SelectedKeys on an interactive one. Give columns FooterText for a totals row.
State in the component
Use @bind-Sort, @bind-Page and @bind-Filters on an interactive page to keep them in C# instead of the URL, like a client-side grid.
Always Live
Preview
This example runs C# over a Blazor Server connection, which an Off page doesn't have. Show this page Live
Code
@* State in the component, on an interactive page: headers and page numbers become buttons, filters
apply after a pause in typing, and the URL doesn't change. *@
<Stack Gap="3" Grow>
<Table Items="Orders.All" PageSize="5" InitialSort="-date" Caption="Order history" QueryPrefix="bound-" FilterPlacement="FilterPlacement.Row"
@bind-Sort="_sort" @bind-Page="_page" @bind-Filters="_filters">
<PropertyColumn Property="o => o.Number" Title="Order" />
<PropertyColumn Property="o => o.Customer" Sortable Filterable />
<PropertyColumn Property="o => o.Total" Format="C" Sortable />
</Table>
<TextBlock Size="TextSize.Sm" Color="Color.Muted">
C# has sort <Code>(_sort ?? "-date")</Code>, page <Code>_page</Code>, filters
<Code>(_filters is { Count: > 0 } f ? string.Join(", ", f.Select(kv => $"{kv.Key}={kv.Value}")) : "none")</Code>
</TextBlock>
</Stack>
{
private string? _sort;
private int _page = 1;
private IReadOnlyDictionary<string, string>? _filters;
}The table starts from the URL if it has values, and a new sort or filter goes back to page 1. Set the page back to 1 yourself when your own filters change.
Data from a database
Pass an EF Core IQueryable as Items, and the database sorts, filters and pages. For more control, use ItemsProvider to load each page yourself:
<Table TItem="Order" ItemsProvider="LoadOrders" PageSize="10">...</Table>
{
async ValueTask<TableResult<Order>> LoadOrders(TableRequest<Order> request)
{
var query = db.Orders.Where(o => o.Active);
var page = await request.Apply(query).ToListAsync(); // filters, sorts, pages
return new(page, await request.ApplyFiltering(query).CountAsync());
}
}Row links
Use RowHref to make each row a link, and RowLabel to name it. Links, buttons and checkboxes inside the row still work on their own.
<Table Items="orders" RowHref='o => $"orders/{o.Id}"' RowLabel='o => $"order {o.Number}"'>...</Table>Several tables on a page
Use QueryPrefix to give each table its own keys in the URL, so they sort and page on their own.
<Table Items="orders" QueryPrefix="o-" PageSize="10">...</Table> @* ?o-sort=, ?o-page= *@
<Table Items="refunds" QueryPrefix="r-" PageSize="10">...</Table>Lines, size and scrolling
Use Size, Striped, ColumnLines, RowLines and Bordered for the look; turn Bordered off for a table in a card. MaxHeight scrolls the rows under a sticky header, and EmptyText shows when there are none.
<Table Items="orders" Size="ControlSize.Sm" Striped ColumnLines>...</Table>
<Table Items="orders" MaxHeight="Length.Rem(20)" EmptyText="No orders yet">...</Table>Mode support
| Feature | Off | Static | Live |
|---|---|---|---|
| Sort, filter and page through the URL | Yes | Yes | Yes |
| Selected rows post with a form | Yes | Yes | Yes |
| Filters apply as you type | No | Yes | Yes |
| Select-all checkbox | No | Yes | Yes |
| Sort, page, filters and selection bound to C# | No | No | Yes |
Off: JavaScript disabled. Static: a static SSR page with IzzyBlazor's script. Live: an interactive page (Server, WebAssembly or Auto).
Accessibility
Captionnames the table for screen readers; it isn't shown. It also names the scrolling area, which can be scrolled from the keyboard.- Sortable headers are links, and the sorted column has
aria-sort. - Each filter button is named for its column ("Filter Customer"), and the active filters are a group named "Filters".
- Row checkboxes are named "Select" and the row's
RowLabel("Select order 1001"); without one, theItemKey. - The range under the table ("1–5 of 23") is read when it changes.
Parameters
| Prop | Type | Default |
|---|---|---|
ItemsThe items. | IEnumerable<TItem>? | |
ItemsProviderLoads one page of items yourself, in place of Items. | Func<TableRequest<TItem>, ValueTask<TableResult<TItem>>>? | |
ChildContentThe columns: PropertyColumn<T1, T2> and TemplateColumn<T>. | RenderFragment? | |
@bind-PageSizeItems per page. | int? | |
PageSizeOptionsSizes to choose from in a "Rows per page" menu in the footer, such as new[] { 10, 25, 50 }. | IReadOnlyList<int>? | |
InitialSortThe sort when the URL has none, such as "name" or "-date" (descending). | string? | |
@bind-SortThe sort, such as "name" or "-date" (descending); null for InitialSort. | string? | |
@bind-PageThe page, from 1. | int? | |
FilterPlacementWhere the column filters go: a filter button in each header that opens the filter (FilterPlacement.Header, the default, with the active filters as removable tags above the table), or a row of fields under the headers (FilterPlacement.Row). | FilterPlacement | FilterPlacement.Header |
@bind-FiltersThe filter values of Filterable columns, by column key ("customer" → "ada"). | IReadOnlyDictionary<string, string>? | |
QueryPrefixPrefix for this table's query-string keys, so several tables can share a page: QueryPrefix="orders-" uses ?orders-sort= and ?orders-page=. | string | "" |
CaptionThe table's name for screen readers (a visually hidden caption). | string? | |
EmptyContentShown when there are no items. | RenderFragment? | computed |
EmptyTextThe text when there are no items. | string? | "No items" |
RowClassA class for each row, such as one that highlights overdue orders. | Func<TItem, string?>? | |
StripedAlternating row backgrounds. | bool | false |
RowLinesA line between rows. | bool | true |
ColumnLinesA line between columns. | bool | false |
BorderedA rounded border around the table. | bool | true |
SizeRow height and text size. | ControlSize | ControlSize.Md |
MaxHeightA height limit: the rows scroll and the header stays in view. | Length? | |
ItemKeyA unique, stable key for each item, such as its id. | Func<TItem, string>? | computed |
SelectableAdds a checkbox to each row. | bool | false |
SelectionNameThe form field name the checked rows post under. | string? | "{QueryPrefix}selected" |
@bind-SelectedKeysThe keys of the selected rows (on any page). | IReadOnlyCollection<string>? | |
RowHrefMakes each row a link, such as to the item's details page. | Func<TItem, string?>? | |
RowLabelNames a row for screen readers, in its link and its checkbox. | Func<TItem, string>? | computed |
LoadingShows the table as loading: rows dim, or placeholder rows show while there are none. | bool | false |
SelectAllLabelThe header checkbox's name for screen readers. | string? | "Select all rows" |
SelectRowLabelThe start of each row checkbox's name for screen readers, before RowLabel. | string? | "Select" |
Plus 16 item props and 4 common props.
Full APISee also
- FilterForm: search and filters of your own, in the URL.
- Pagination: page links for a list that isn't a table.
- Render modes: state in the URL on static pages.