DataTable
An unstyled behavioral primitive for client-side data tables. Provides text search, sortable columns, pagination, and configurable page size — all styling is the consumer's responsibility via explicit class parameters.
Overview
MDataTable<TItem> is an unstyled behavioral primitive. It renders the table structure —
controls, wrapper, <table>, header, body, footer, and pagination — with no built-in Tailwind
classes or visual defaults. Every rendered element receives only the class string you supply via the corresponding
class parameter. Pass any IEnumerable<TItem> to Items and define columns using
MDataTableColumn inside the Columns named slot.
The table handles text filtering across all columns, multi-cycle column sorting (ascending → descending → none),
page size selection, and pagination entirely on the client — no extra wiring required.
Usage
Add the namespace import to use the component.
@using Marai.UI.Components.MDataTableBasic Usage
Supply a class parameter for every rendered element. Columns are defined via MDataTableColumn
inside the Columns slot:
| Name | Role | Status | |
|---|---|---|---|
| Alice Martin | alice@example.com | Admin | Active |
| Bob Chen | bob@example.com | Editor | Active |
| Carol Davis | carol@example.com | Viewer | Inactive |
| David Kim | david@example.com | Editor | Active |
| Eva Rossi | eva@example.com | Admin | Active |
<MDataTable Items="_users" PageSize="5"
Class="w-full"
ControlsClass="flex items-center justify-between gap-4 mb-3"
SearchInputClass="border border-gray-300 rounded px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
PageSizeLabelClass="flex items-center gap-2 text-sm text-gray-600"
PageSizeSelectClass="border border-gray-300 rounded px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
TableWrapperClass="overflow-auto rounded border border-gray-200"
TableClass="w-full text-sm"
TheadClass="bg-gray-50"
HeaderCellClass="px-4 py-2 text-left font-medium text-gray-700 border-b border-gray-200"
TbodyClass=""
RowClass="border-t border-gray-100 hover:bg-gray-50"
CellClass="px-4 py-2 text-gray-800"
EmptyCellClass="px-4 py-6 text-center text-gray-500"
FooterClass="flex items-center justify-between mt-3 text-sm text-gray-600"
PaginationClass="flex items-center gap-1"
PageButtonClass="px-2 py-1 rounded border border-gray-200 hover:bg-gray-100 disabled:opacity-40"
ActivePageButtonClass="px-2 py-1 rounded bg-blue-600 text-white">
<Columns>
<MDataTableColumn Header="Name" Field="FullName" />
<MDataTableColumn Header="Email" Field="Email" />
<MDataTableColumn Header="Role" Field="Role" />
<MDataTableColumn Header="Status" Field="Status" />
</Columns>
</MDataTable>
@code {
private sealed record UserRow(string FullName, string Email, string Role, string Status);
private readonly List<UserRow> _users =
[
new("Alice Martin", "alice@example.com", "Admin", "Active"),
new("Bob Chen", "bob@example.com", "Editor", "Active"),
new("Carol Davis", "carol@example.com", "Viewer", "Inactive"),
new("David Kim", "david@example.com", "Editor", "Active"),
new("Eva Rossi", "eva@example.com", "Admin", "Active"),
new("Frank Müller", "frank@example.com", "Viewer", "Active"),
new("Grace O'Brien", "grace@example.com", "Editor", "Inactive"),
new("Hiro Tanaka", "hiro@example.com", "Viewer", "Active"),
];
}
Examples
Page Size and Pagination
Set PageSize to control the initial rows per page. Use PaginationClass,
PageButtonClass, and ActivePageButtonClass to style the pagination controls.
The user can change the page size at runtime via the page size selector.
| Name | Role |
|---|---|
| Alice Martin | Admin |
| Bob Chen | Editor |
| Carol Davis | Viewer |
<MDataTable Items="_users" PageSize="3"
Class="w-full"
ControlsClass="flex items-center justify-between gap-4 mb-3"
SearchInputClass="border border-gray-300 rounded px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
PageSizeLabelClass="flex items-center gap-2 text-sm text-gray-600"
PageSizeSelectClass="border border-gray-300 rounded px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
TableWrapperClass="overflow-auto rounded border border-gray-200"
TableClass="w-full text-sm"
TheadClass="bg-gray-50"
HeaderCellClass="px-4 py-2 text-left font-medium text-gray-700 border-b border-gray-200"
RowClass="border-t border-gray-100 hover:bg-gray-50"
CellClass="px-4 py-2 text-gray-800"
EmptyCellClass="px-4 py-6 text-center text-gray-500"
FooterClass="flex items-center justify-between mt-3 text-sm text-gray-600"
PaginationClass="flex items-center gap-1"
PageButtonClass="px-2 py-1 rounded border border-gray-200 hover:bg-gray-100 disabled:opacity-40"
ActivePageButtonClass="px-2 py-1 rounded bg-blue-600 text-white">
<Columns>
<MDataTableColumn Header="Name" Field="FullName" />
<MDataTableColumn Header="Role" Field="Role" />
</Columns>
</MDataTable>
@code {
private sealed record UserRow(string FullName, string Email, string Role, string Status);
private readonly List<UserRow> _users =
[
new("Alice Martin", "alice@example.com", "Admin", "Active"),
new("Bob Chen", "bob@example.com", "Editor", "Active"),
new("Carol Davis", "carol@example.com", "Viewer", "Inactive"),
new("David Kim", "david@example.com", "Editor", "Active"),
new("Eva Rossi", "eva@example.com", "Admin", "Active"),
new("Frank Müller", "frank@example.com", "Viewer", "Active"),
new("Grace O'Brien", "grace@example.com", "Editor", "Inactive"),
new("Hiro Tanaka", "hiro@example.com", "Viewer", "Active"),
];
}
Sortable Columns
Add Sortable="true" to any MDataTableColumn to make it clickable.
Use SortButtonClass and SortButtonActiveClass to style the sort button in each state,
and SortIndicatorClass to style the ↑ / ↓ indicator.
Clicking once sorts ascending (↑), clicking again sorts descending (↓), a third click clears the sort.
| Alice Martin | alice@example.com | Admin | Active |
| Bob Chen | bob@example.com | Editor | Active |
| Carol Davis | carol@example.com | Viewer | Inactive |
| David Kim | david@example.com | Editor | Active |
| Eva Rossi | eva@example.com | Admin | Active |
<MDataTable Items="_users" PageSize="5"
Class="w-full"
ControlsClass="flex items-center justify-between gap-4 mb-3"
SearchInputClass="border border-gray-300 rounded px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
PageSizeLabelClass="flex items-center gap-2 text-sm text-gray-600"
PageSizeSelectClass="border border-gray-300 rounded px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
TableWrapperClass="overflow-auto rounded border border-gray-200"
TableClass="w-full text-sm"
TheadClass="bg-gray-50"
HeaderCellClass="px-4 py-2 text-left font-medium text-gray-700 border-b border-gray-200"
SortButtonClass="flex items-center gap-1 w-full text-left hover:text-blue-600"
SortButtonActiveClass="flex items-center gap-1 w-full text-left text-blue-600 font-semibold"
SortIndicatorClass="text-xs"
RowClass="border-t border-gray-100 hover:bg-gray-50"
CellClass="px-4 py-2 text-gray-800"
EmptyCellClass="px-4 py-6 text-center text-gray-500"
FooterClass="flex items-center justify-between mt-3 text-sm text-gray-600"
PaginationClass="flex items-center gap-1"
PageButtonClass="px-2 py-1 rounded border border-gray-200 hover:bg-gray-100 disabled:opacity-40"
ActivePageButtonClass="px-2 py-1 rounded bg-blue-600 text-white">
<Columns>
<MDataTableColumn Header="Name" Field="FullName" Sortable="true" />
<MDataTableColumn Header="Email" Field="Email" />
<MDataTableColumn Header="Role" Field="Role" Sortable="true" />
<MDataTableColumn Header="Status" Field="Status" Sortable="true" />
</Columns>
</MDataTable>
@code {
private sealed record UserRow(string FullName, string Email, string Role, string Status);
private readonly List<UserRow> _users =
[
new("Alice Martin", "alice@example.com", "Admin", "Active"),
new("Bob Chen", "bob@example.com", "Editor", "Active"),
new("Carol Davis", "carol@example.com", "Viewer", "Inactive"),
new("David Kim", "david@example.com", "Editor", "Active"),
new("Eva Rossi", "eva@example.com", "Admin", "Active"),
new("Frank Müller", "frank@example.com", "Viewer", "Active"),
new("Grace O'Brien", "grace@example.com", "Editor", "Inactive"),
new("Hiro Tanaka", "hiro@example.com", "Viewer", "Active"),
];
}
Column-Level Styling
Use HeaderClass and CellClass on MDataTableColumn to override the
table-level HeaderCellClass and CellClass for individual columns.
Columns without their own class fall back to the table-level defaults.
| Name | Role | Status |
|---|---|---|
| Alice Martin | Admin | Active |
| Bob Chen | Editor | Active |
| Carol Davis | Viewer | Inactive |
| David Kim | Editor | Active |
| Eva Rossi | Admin | Active |
@* Column-level HeaderClass/CellClass override table-level HeaderCellClass/CellClass when set *@
<MDataTable Items="_users" PageSize="5"
Class="w-full"
ControlsClass="flex items-center justify-between gap-4 mb-3"
SearchInputClass="border border-gray-300 rounded px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
PageSizeLabelClass="flex items-center gap-2 text-sm text-gray-600"
PageSizeSelectClass="border border-gray-300 rounded px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
TableWrapperClass="overflow-auto rounded border border-gray-200"
TableClass="w-full text-sm"
TheadClass="bg-gray-50"
HeaderCellClass="px-4 py-2 text-left font-medium text-gray-700 border-b border-gray-200"
RowClass="border-t border-gray-100 hover:bg-gray-50"
CellClass="px-4 py-2 text-gray-800"
EmptyCellClass="px-4 py-6 text-center text-gray-500"
FooterClass="flex items-center justify-between mt-3 text-sm text-gray-600"
PaginationClass="flex items-center gap-1"
PageButtonClass="px-2 py-1 rounded border border-gray-200 hover:bg-gray-100 disabled:opacity-40"
ActivePageButtonClass="px-2 py-1 rounded bg-blue-600 text-white">
<Columns>
<MDataTableColumn Header="Name" Field="FullName"
HeaderClass="px-4 py-2 text-left font-bold text-blue-700 border-b border-gray-200"
CellClass="px-4 py-2 font-medium text-blue-900" />
<MDataTableColumn Header="Role" Field="Role"
HeaderClass="px-4 py-2 text-center font-medium text-gray-700 border-b border-gray-200"
CellClass="px-4 py-2 text-center text-gray-600" />
<MDataTableColumn Header="Status" Field="Status" />
</Columns>
</MDataTable>
@code {
private sealed record UserRow(string FullName, string Email, string Role, string Status);
private readonly List<UserRow> _users =
[
new("Alice Martin", "alice@example.com", "Admin", "Active"),
new("Bob Chen", "bob@example.com", "Editor", "Active"),
new("Carol Davis", "carol@example.com", "Viewer", "Inactive"),
new("David Kim", "david@example.com", "Editor", "Active"),
new("Eva Rossi", "eva@example.com", "Admin", "Active"),
new("Frank Müller", "frank@example.com", "Viewer", "Active"),
new("Grace O'Brien", "grace@example.com", "Editor", "Inactive"),
new("Hiro Tanaka", "hiro@example.com", "Viewer", "Active"),
];
}
HTML Attribute Passthrough
Any unrecognized HTML attribute (e.g. id, aria-label, data-testid)
is passed through to the outer wrapper element via AdditionalAttributes.
| Name | |
|---|---|
| Alice Martin | alice@example.com |
| Bob Chen | bob@example.com |
| Carol Davis | carol@example.com |
| David Kim | david@example.com |
| Eva Rossi | eva@example.com |
@* id, aria-label, and other HTML attributes pass through to the outer wrapper *@
<MDataTable Items="_users" PageSize="5"
id="users-table"
aria-label="Users list"
Class="w-full"
ControlsClass="flex items-center justify-between gap-4 mb-3"
SearchInputClass="border border-gray-300 rounded px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
PageSizeLabelClass="flex items-center gap-2 text-sm text-gray-600"
PageSizeSelectClass="border border-gray-300 rounded px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
TableWrapperClass="overflow-auto rounded border border-gray-200"
TableClass="w-full text-sm"
TheadClass="bg-gray-50"
HeaderCellClass="px-4 py-2 text-left font-medium text-gray-700 border-b border-gray-200"
RowClass="border-t border-gray-100 hover:bg-gray-50"
CellClass="px-4 py-2 text-gray-800"
EmptyCellClass="px-4 py-6 text-center text-gray-500"
FooterClass="flex items-center justify-between mt-3 text-sm text-gray-600"
PaginationClass="flex items-center gap-1"
PageButtonClass="px-2 py-1 rounded border border-gray-200 hover:bg-gray-100 disabled:opacity-40"
ActivePageButtonClass="px-2 py-1 rounded bg-blue-600 text-white">
<Columns>
<MDataTableColumn Header="Name" Field="FullName" />
<MDataTableColumn Header="Email" Field="Email" />
</Columns>
</MDataTable>
@code {
private sealed record UserRow(string FullName, string Email, string Role, string Status);
private readonly List<UserRow> _users =
[
new("Alice Martin", "alice@example.com", "Admin", "Active"),
new("Bob Chen", "bob@example.com", "Editor", "Active"),
new("Carol Davis", "carol@example.com", "Viewer", "Inactive"),
new("David Kim", "david@example.com", "Editor", "Active"),
new("Eva Rossi", "eva@example.com", "Admin", "Active"),
new("Frank Müller", "frank@example.com", "Viewer", "Active"),
new("Grace O'Brien", "grace@example.com", "Editor", "Inactive"),
new("Hiro Tanaka", "hiro@example.com", "Viewer", "Active"),
];
}
Data Loading
MDataTable works with any already-deserialized collection. Loading data from an API or other source
is the consumer's responsibility. A typical pattern:
// Load from an API and deserialize
protected override async Task OnInitializedAsync()
{
var json = await Http.GetStringAsync("/api/users");
_users = JsonSerializer.Deserialize<List<UserRow>>(json) ?? [];
}Accessibility
- The table uses semantic
<table>,<thead>,<tbody>,<th>, and<td>elements - Column headers use
scope="col"for proper screen reader association with cells - Sortable column headers render as
<button>elements witharia-label="Sort by ..." - The search input includes
aria-label="Search table" - The page size selector includes
aria-label="Rows per page" - The pagination container uses
role="navigation"witharia-label="Pagination" - The active page button has
aria-current="page" - All pagination buttons have descriptive
aria-labelattributes (e.g. "First page", "Next page") - The outer wrapper accepts arbitrary ARIA attributes via
AdditionalAttributes
API Reference
MDataTable<TItem>
| Parameter | Type | Default | Description |
|---|---|---|---|
| Items | IEnumerable<TItem>? | null |
The data source. Accepts any typed collection (e.g. List<MyClass>). |
| Columns | RenderFragment? | null |
Named slot containing one or more MDataTableColumn definitions. |
| PageSize | int | 10 |
Initial number of rows per page. Users can change this via the page size selector at runtime. |
| Class | string? | null |
CSS classes applied to the outer wrapper <div>. |
| ControlsClass | string? | null |
CSS classes for the controls row containing the search input and page size selector. |
| SearchInputClass | string? | null |
CSS classes for the search <input>. |
| PageSizeLabelClass | string? | null |
CSS classes for the <label> wrapping the page size selector. |
| PageSizeSelectClass | string? | null |
CSS classes for the page size <select>. |
| SelectClass | string? | null |
Compatibility alias for PageSizeSelectClass. Used when PageSizeSelectClass is not set. |
| TableWrapperClass | string? | null |
CSS classes for the <div> wrapping the <table> (useful for overflow scroll). |
| TableClass | string? | null |
CSS classes for the <table> element. |
| TheadClass | string? | null |
CSS classes for the <thead> element. |
| HeaderRowClass | string? | null |
CSS classes for the <tr> inside <thead>. |
| HeaderCellClass | string? | null |
Default CSS classes for each <th>. Overridden per column by MDataTableColumn.HeaderClass. |
| SortButtonClass | string? | null |
CSS classes for the sort <button> in its default (inactive) state. |
| SortButtonActiveClass | string? | null |
CSS classes for the sort <button> when that column is the active sort column. |
| SortIndicatorClass | string? | null |
CSS classes for the ↑ / ↓ sort direction indicator <span>. |
| TbodyClass | string? | null |
CSS classes for the <tbody> element. |
| RowClass | string? | null |
CSS classes for each data <tr>. |
| CellClass | string? | null |
Default CSS classes for each <td>. Overridden per column by MDataTableColumn.CellClass. |
| EmptyRowClass | string? | null |
CSS classes for the empty-state <tr> shown when no results are found. |
| EmptyCellClass | string? | null |
CSS classes for the empty-state <td>. |
| FooterClass | string? | null |
CSS classes for the footer row containing the summary and pagination. |
| SummaryClass | string? | null |
CSS classes for the summary text <span> (e.g. "Showing 1 to 5 of 8 entries"). |
| PaginationClass | string? | null |
CSS classes for the pagination <div> container. |
| PageButtonClass | string? | null |
CSS classes for inactive pagination buttons (first, previous, page numbers, next, last). |
| ActivePageButtonClass | string? | null |
CSS classes for the currently active page number button. |
| AdditionalAttributes | Dictionary<string, object>? | null |
Arbitrary HTML attributes passed through to the outer wrapper (e.g. id, aria-label). |
MDataTableColumn
| Parameter | Type | Default | Description |
|---|---|---|---|
| Header | string | "" |
The visible column heading text rendered in <th>. |
| Field | string | "" |
The property name on each item used to read the cell value via reflection. |
| Sortable | bool | false |
When true, the column header becomes a button. Clicking cycles through ascending ↑, descending ↓, and unsorted. |
| HeaderClass | string? | null |
CSS classes for this column's <th>. Takes precedence over the table-level HeaderCellClass when set. |
| CellClass | string? | null |
CSS classes for this column's <td> cells. Takes precedence over the table-level CellClass when set. |