M
Marai.UI
v1.0.0-alpha.7

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.

razor
@using Marai.UI.Components.MDataTable

Basic Usage

Supply a class parameter for every rendered element. Columns are defined via MDataTableColumn inside the Columns slot:

Preview
NameEmailRoleStatus
Alice Martinalice@example.comAdminActive
Bob Chenbob@example.comEditorActive
Carol Daviscarol@example.comViewerInactive
David Kimdavid@example.comEditorActive
Eva Rossieva@example.comAdminActive
Showing 1 to 5 of 8 entries
razor
<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.

Preview
NameRole
Alice MartinAdmin
Bob ChenEditor
Carol DavisViewer
Showing 1 to 3 of 8 entries
razor
<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.

Preview
Email
Alice Martinalice@example.comAdminActive
Bob Chenbob@example.comEditorActive
Carol Daviscarol@example.comViewerInactive
David Kimdavid@example.comEditorActive
Eva Rossieva@example.comAdminActive
Showing 1 to 5 of 8 entries
razor
<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.

Preview
NameRoleStatus
Alice MartinAdminActive
Bob ChenEditorActive
Carol DavisViewerInactive
David KimEditorActive
Eva RossiAdminActive
Showing 1 to 5 of 8 entries
razor
@* 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.

Preview
NameEmail
Alice Martinalice@example.com
Bob Chenbob@example.com
Carol Daviscarol@example.com
David Kimdavid@example.com
Eva Rossieva@example.com
Showing 1 to 5 of 8 entries
razor
@* 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:

csharp
// 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 with aria-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" with aria-label="Pagination"
  • The active page button has aria-current="page"
  • All pagination buttons have descriptive aria-label attributes (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.