Grid / TreeGrid
An interactive two-dimensional widget with roving cell focus, row selection, sorting and expandable rows.
Sortable data grid
WAI-ARIA Data Grid on a real <table> via
As. One tab stop; arrows move between cells, Home / End,
Ctrl + Home / End, PageUp / PageDown. Click a header or press Enter on it: OnActivate
toggles the sort, Sort emits aria-sort,
styled with group-aria-[sort=descending]:rotate-180.
| Planet | Type | Diameter | Moons |
|---|---|---|---|
| Earth | Terrestrial | 12,756 km | 1 |
| Jupiter | Gas giant | 142,984 km | 95 |
| Mars | Terrestrial | 6,792 km | 2 |
| Mercury | Terrestrial | 4,879 km | 0 |
| Neptune | Ice giant | 49,528 km | 16 |
| Saturn | Gas giant | 120,536 km | 146 |
| Uranus | Ice giant | 51,118 km | 28 |
| Venus | Terrestrial | 12,104 km | 0 |
<div class="w-full overflow-x-auto rounded-lg border border-zinc-200 bg-white">
<BitBhGrid As="table" aria-label="Planets" class="w-full border-collapse text-sm text-zinc-900">
<BitBhGridRowGroup As="thead">
<BitBhGridRow As="tr" IsHeaderRow="true">
<BitBhGridCell As="th" Header="true" Sort="@(SortOf("name"))" OnActivate="@(() => SortBy("name"))"
class="group cursor-pointer select-none border-b border-zinc-200 bg-zinc-50 px-3 py-2 text-start text-xs font-semibold uppercase tracking-wide whitespace-nowrap text-zinc-500 outline-none hover:text-zinc-900 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600 aria-[sort=ascending]:text-zinc-900 aria-[sort=descending]:text-zinc-900">
Planet<span aria-hidden="true" class="ml-1 inline-block text-blue-600 opacity-0 transition group-hover:opacity-40 group-aria-[sort=ascending]:opacity-100 group-aria-[sort=descending]:rotate-180 group-aria-[sort=descending]:opacity-100">▲</span>
</BitBhGridCell>
<BitBhGridCell As="th" Header="true" Sort="@(SortOf("type"))" OnActivate="@(() => SortBy("type"))"
class="group cursor-pointer select-none border-b border-zinc-200 bg-zinc-50 px-3 py-2 text-start text-xs font-semibold uppercase tracking-wide whitespace-nowrap text-zinc-500 outline-none hover:text-zinc-900 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600 aria-[sort=ascending]:text-zinc-900 aria-[sort=descending]:text-zinc-900">
Type<span aria-hidden="true" class="ml-1 inline-block text-blue-600 opacity-0 transition group-hover:opacity-40 group-aria-[sort=ascending]:opacity-100 group-aria-[sort=descending]:rotate-180 group-aria-[sort=descending]:opacity-100">▲</span>
</BitBhGridCell>
<BitBhGridCell As="th" Header="true" Sort="@(SortOf("diameter"))" OnActivate="@(() => SortBy("diameter"))"
class="group cursor-pointer select-none border-b border-zinc-200 bg-zinc-50 px-3 py-2 text-end text-xs font-semibold uppercase tracking-wide whitespace-nowrap text-zinc-500 outline-none hover:text-zinc-900 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600 aria-[sort=ascending]:text-zinc-900 aria-[sort=descending]:text-zinc-900">
Diameter<span aria-hidden="true" class="ml-1 inline-block text-blue-600 opacity-0 transition group-hover:opacity-40 group-aria-[sort=ascending]:opacity-100 group-aria-[sort=descending]:rotate-180 group-aria-[sort=descending]:opacity-100">▲</span>
</BitBhGridCell>
<BitBhGridCell As="th" Header="true" Sort="@(SortOf("moons"))" OnActivate="@(() => SortBy("moons"))"
class="group cursor-pointer select-none border-b border-zinc-200 bg-zinc-50 px-3 py-2 text-end text-xs font-semibold uppercase tracking-wide whitespace-nowrap text-zinc-500 outline-none hover:text-zinc-900 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600 aria-[sort=ascending]:text-zinc-900 aria-[sort=descending]:text-zinc-900">
Moons<span aria-hidden="true" class="ml-1 inline-block text-blue-600 opacity-0 transition group-hover:opacity-40 group-aria-[sort=ascending]:opacity-100 group-aria-[sort=descending]:rotate-180 group-aria-[sort=descending]:opacity-100">▲</span>
</BitBhGridCell>
</BitBhGridRow>
</BitBhGridRowGroup>
<BitBhGridRowGroup As="tbody" class="[&>tr:last-child>*]:border-b-0">
@foreach (var planet in SortedPlanets)
{
<BitBhGridRow As="tr" @key="planet.Name" Key="@planet.Name">
<BitBhGridCell As="th" RowHeader="true" class="border-b border-zinc-100 px-3 py-2 text-start font-semibold whitespace-nowrap outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600">@planet.Name</BitBhGridCell>
<BitBhGridCell As="td" class="border-b border-zinc-100 px-3 py-2 whitespace-nowrap outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600">@planet.Type</BitBhGridCell>
<BitBhGridCell As="td" class="border-b border-zinc-100 px-3 py-2 text-end tabular-nums whitespace-nowrap outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600">@planet.DiameterKm.ToString("N0") km</BitBhGridCell>
<BitBhGridCell As="td" class="border-b border-zinc-100 px-3 py-2 text-end tabular-nums whitespace-nowrap outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600">@planet.Moons</BitBhGridCell>
</BitBhGridRow>
}
</BitBhGridRowGroup>
</BitBhGrid>
</div>
@code {
private sealed record Planet(string Name, string Type, int DiameterKm, int Moons);
private readonly Planet[] planets =
[
new("Mercury", "Terrestrial", 4_879, 0),
new("Venus", "Terrestrial", 12_104, 0),
new("Earth", "Terrestrial", 12_756, 1),
new("Mars", "Terrestrial", 6_792, 2),
new("Jupiter", "Gas giant", 142_984, 95),
new("Saturn", "Gas giant", 120_536, 146),
new("Uranus", "Ice giant", 51_118, 28),
new("Neptune", "Ice giant", 49_528, 16),
];
private string sortColumn = "name";
private BitBhSortDirection sortDirection = BitBhSortDirection.Ascending;
private BitBhSortDirection SortOf(string column) => sortColumn == column ? sortDirection : BitBhSortDirection.None;
private void SortBy(string column)
{
sortDirection = sortColumn == column && sortDirection == BitBhSortDirection.Ascending
? BitBhSortDirection.Descending
: BitBhSortDirection.Ascending;
sortColumn = column;
}
private IEnumerable<Planet> SortedPlanets
{
get
{
Func<Planet, object> key = sortColumn switch
{
"type" => p => p.Type,
"diameter" => p => p.DiameterKm,
"moons" => p => p.Moons,
_ => p => p.Name,
};
return sortDirection == BitBhSortDirection.Descending ? planets.OrderByDescending(key) : planets.OrderBy(key);
}
}
}Multi-row selection
SelectionMode="BitBhGridSelectionMode.Multiple" +
@bind-SelectedKeys. Click / Shift + click, Space,
Shift + Space or Shift + Up / Down, Ctrl + A. Rows emit aria-selected:
the boxes use group-aria-selected:; the header box toggles everything via
OnActivate.
| Invoice | Customer | Status | Amount | |
|---|---|---|---|---|
| INV-1042 | Acme Corp | Paid | $1,250.00 | |
| INV-1043 | Globex | Pending | $830.50 | |
| INV-1044 | Initech | Overdue | $4,200.00 | |
| INV-1045 | Umbrella | Paid | $615.75 | |
| INV-1046 | Hooli | Pending | $2,980.00 | |
| INV-1047 | Stark Industries | Paid | $12,400.00 |
<div class="flex min-h-8 items-center justify-between gap-3 text-sm text-zinc-500">
<span><strong class="font-semibold text-zinc-900">@selectedInvoices.Count</strong> selected · total <strong class="font-semibold text-zinc-900">@FormatMoney(SelectedTotal)</strong></span>
<button type="button" disabled="@(selectedInvoices.Count == 0)" @onclick="ClearInvoices"
class="cursor-pointer rounded-md border border-zinc-200 bg-white px-3 py-1.5 text-xs font-medium text-zinc-900 hover:bg-zinc-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 disabled:cursor-not-allowed disabled:opacity-50">Clear selection</button>
</div>
<div class="w-full overflow-x-auto rounded-lg border border-zinc-200 bg-white">
<BitBhGrid As="table" aria-label="Invoices" class="w-full border-collapse text-sm text-zinc-900"
SelectionMode="BitBhGridSelectionMode.Multiple"
@bind-SelectedKeys="selectedInvoices">
<BitBhGridRowGroup As="thead">
<BitBhGridRow As="tr" IsHeaderRow="true">
<BitBhGridCell As="th" Header="true" OnActivate="ToggleAllInvoices"
class="w-10 cursor-pointer border-b border-zinc-200 bg-zinc-50 px-3 py-2 text-start text-xs font-semibold uppercase tracking-wide whitespace-nowrap text-zinc-500 outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600">
<BitBhVisuallyHidden>Select all</BitBhVisuallyHidden>
<span data-checked="@AllInvoicesState" aria-hidden="true"
class="inline-flex size-4 items-center justify-center rounded border-[1.5px] border-zinc-400 bg-white align-middle text-[0.7rem] font-bold leading-none text-transparent data-[checked=true]:border-blue-600 data-[checked=true]:bg-blue-600 data-[checked=true]:text-white data-[checked=mixed]:border-blue-600 data-[checked=mixed]:bg-blue-600 data-[checked=mixed]:text-white">@(AllInvoicesState == "mixed" ? "–" : "✓")</span>
</BitBhGridCell>
<BitBhGridCell As="th" Header="true" class="border-b border-zinc-200 bg-zinc-50 px-3 py-2 text-start text-xs font-semibold uppercase tracking-wide whitespace-nowrap text-zinc-500 outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600">Invoice</BitBhGridCell>
<BitBhGridCell As="th" Header="true" class="border-b border-zinc-200 bg-zinc-50 px-3 py-2 text-start text-xs font-semibold uppercase tracking-wide whitespace-nowrap text-zinc-500 outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600">Customer</BitBhGridCell>
<BitBhGridCell As="th" Header="true" class="border-b border-zinc-200 bg-zinc-50 px-3 py-2 text-start text-xs font-semibold uppercase tracking-wide whitespace-nowrap text-zinc-500 outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600">Status</BitBhGridCell>
<BitBhGridCell As="th" Header="true" class="border-b border-zinc-200 bg-zinc-50 px-3 py-2 text-end text-xs font-semibold uppercase tracking-wide whitespace-nowrap text-zinc-500 outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600">Amount</BitBhGridCell>
</BitBhGridRow>
</BitBhGridRowGroup>
<BitBhGridRowGroup As="tbody" class="[&>tr:last-child>*]:border-b-0">
@foreach (var invoice in invoices)
{
<BitBhGridRow As="tr" @key="invoice.Id" Key="@invoice.Id"
class="group cursor-pointer select-none hover:bg-zinc-50 aria-selected:bg-blue-50 aria-selected:hover:bg-blue-100">
<BitBhGridCell As="td" class="w-10 border-b border-zinc-100 px-3 py-2 whitespace-nowrap outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600">
<span aria-hidden="true"
class="inline-flex size-4 items-center justify-center rounded border-[1.5px] border-zinc-400 bg-white align-middle text-[0.7rem] font-bold leading-none text-transparent group-aria-selected:border-blue-600 group-aria-selected:bg-blue-600 group-aria-selected:text-white">✓</span>
</BitBhGridCell>
<BitBhGridCell As="td" class="border-b border-zinc-100 px-3 py-2 whitespace-nowrap outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600">@invoice.Id</BitBhGridCell>
<BitBhGridCell As="td" class="border-b border-zinc-100 px-3 py-2 whitespace-nowrap outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600">@invoice.Customer</BitBhGridCell>
<BitBhGridCell As="td" class="border-b border-zinc-100 px-3 py-2 whitespace-nowrap outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600">
<span data-status="@invoice.Status.ToLowerInvariant()"
class="inline-block rounded-full bg-zinc-100 px-2 py-0.5 text-xs font-medium text-zinc-600 data-[status=paid]:bg-green-100 data-[status=paid]:text-green-800 data-[status=pending]:bg-yellow-100 data-[status=pending]:text-yellow-800 data-[status=overdue]:bg-red-100 data-[status=overdue]:text-red-800">@invoice.Status</span>
</BitBhGridCell>
<BitBhGridCell As="td" class="border-b border-zinc-100 px-3 py-2 text-end tabular-nums whitespace-nowrap outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600">@FormatMoney(invoice.Amount)</BitBhGridCell>
</BitBhGridRow>
}
</BitBhGridRowGroup>
</BitBhGrid>
</div>
@code {
private sealed record Invoice(string Id, string Customer, string Status, decimal Amount);
private readonly Invoice[] invoices =
[
new("INV-1042", "Acme Corp", "Paid", 1_250.00m),
new("INV-1043", "Globex", "Pending", 830.50m),
new("INV-1044", "Initech", "Overdue", 4_200.00m),
new("INV-1045", "Umbrella", "Paid", 615.75m),
new("INV-1046", "Hooli", "Pending", 2_980.00m),
new("INV-1047", "Stark Industries", "Paid", 12_400.00m),
];
private IReadOnlyCollection<string> selectedInvoices = [];
private decimal SelectedTotal => invoices.Where(i => selectedInvoices.Contains(i.Id)).Sum(i => i.Amount);
private string AllInvoicesState =>
selectedInvoices.Count == 0 ? "false" : selectedInvoices.Count == invoices.Length ? "true" : "mixed";
private void ToggleAllInvoices() =>
selectedInvoices = selectedInvoices.Count == invoices.Length ? [] : invoices.Select(i => i.Id).ToArray();
private void ClearInvoices() => selectedInvoices = [];
private static string FormatMoney(decimal amount) =>
"$" + amount.ToString("N2", System.Globalization.CultureInfo.InvariantCulture);
}Row activation
OnRowActivate fires with the row's
Key on Enter or double click — open a message, navigate to a
record. Unread rows are styled with data-[unread]:font-semibold.
| From | Subject | Received |
|---|---|---|
| Ada Lovelace | Notes on the Analytical Engine | 09:24 |
| Grace Hopper | Found a bug (an actual moth) | 08:51 |
| Alan Turing | Can machines think? | Yesterday |
| Margaret Hamilton | Priority displays | Mon |
Press Enter on a row, or double-click it, to open the message.
<div class="w-full overflow-x-auto rounded-lg border border-zinc-200 bg-white">
<BitBhGrid As="table" aria-label="Inbox" class="w-full border-collapse text-sm text-zinc-900"
SelectionMode="BitBhGridSelectionMode.Single"
@bind-SelectedKeys="selectedMessage"
OnRowActivate="OpenMessage">
<BitBhGridRowGroup As="thead">
<BitBhGridRow As="tr" IsHeaderRow="true">
<BitBhGridCell As="th" Header="true" class="border-b border-zinc-200 bg-zinc-50 px-3 py-2 text-start text-xs font-semibold uppercase tracking-wide whitespace-nowrap text-zinc-500 outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600">From</BitBhGridCell>
<BitBhGridCell As="th" Header="true" class="border-b border-zinc-200 bg-zinc-50 px-3 py-2 text-start text-xs font-semibold uppercase tracking-wide whitespace-nowrap text-zinc-500 outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600">Subject</BitBhGridCell>
<BitBhGridCell As="th" Header="true" class="border-b border-zinc-200 bg-zinc-50 px-3 py-2 text-end text-xs font-semibold uppercase tracking-wide whitespace-nowrap text-zinc-500 outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600">Received</BitBhGridCell>
</BitBhGridRow>
</BitBhGridRowGroup>
<BitBhGridRowGroup As="tbody" class="[&>tr:last-child>*]:border-b-0">
@foreach (var message in messages)
{
<BitBhGridRow As="tr" @key="message.Id" Key="@message.Id"
data-unread="@(!readMessages.Contains(message.Id))"
class="group cursor-pointer select-none hover:bg-zinc-50 aria-selected:bg-blue-50 aria-selected:hover:bg-blue-100 data-[unread]:font-semibold">
<BitBhGridCell As="td" class="border-b border-zinc-100 px-3 py-2 whitespace-nowrap outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600"><span aria-hidden="true" class="invisible mr-2 inline-block size-2 rounded-full bg-blue-600 align-middle group-data-[unread]:visible"></span>@message.From</BitBhGridCell>
<BitBhGridCell As="td" class="border-b border-zinc-100 px-3 py-2 whitespace-nowrap outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600">@message.Subject</BitBhGridCell>
<BitBhGridCell As="td" class="border-b border-zinc-100 px-3 py-2 text-end tabular-nums whitespace-nowrap outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600">@message.Received</BitBhGridCell>
</BitBhGridRow>
}
</BitBhGridRowGroup>
</BitBhGrid>
</div>
@if (OpenedMessage is { } opened)
{
<div class="rounded-lg border border-zinc-200 bg-zinc-50 px-4 py-3 text-sm text-zinc-600">
<strong class="mb-1 block text-zinc-900">@opened.Subject</strong>
From @opened.From — @opened.Body
</div>
}
else
{
<p class="text-sm text-zinc-500">Press Enter on a row, or double-click it, to open the message.</p>
}
@code {
private sealed record Message(string Id, string From, string Subject, string Received, string Body);
private readonly Message[] messages =
[
new("m1", "Ada Lovelace", "Notes on the Analytical Engine", "09:24", "I have attached my notes — see note G."),
new("m2", "Grace Hopper", "Found a bug (an actual moth)", "08:51", "Taped it into the logbook for posterity."),
new("m3", "Alan Turing", "Can machines think?", "Yesterday", "Let us play the imitation game this Friday."),
new("m4", "Margaret Hamilton", "Priority displays", "Mon", "The guidance computer recovered beautifully."),
];
private IReadOnlyCollection<string> selectedMessage = [];
private readonly HashSet<string> readMessages = ["m3"];
private string? openedMessageId;
private Message? OpenedMessage => messages.FirstOrDefault(m => m.Id == openedMessageId);
// Enter on any cell of the row, or a double click, passes the row's Key.
private void OpenMessage(string id)
{
openedMessageId = id;
readMessages.Add(id);
}
}TreeGrid
TreeGrid="true" + per-row
Level / HasChildren /
ParentKey: collapsed descendants are hidden.
Right / Left on the first cell expand / collapse; the chevron edits @bind-ExpandedKeys
and rotates with group-data-[state=open]:rotate-90.
| Name | Kind | Size |
|---|---|---|
| 📂project | Folder | 502.1 KB |
| 📂src | Folder | 23.6 KB |
| 📄app.ts | TypeScript | 13.9 KB |
| 📄router.ts | TypeScript | 6.7 KB |
| 📄styles.css | Stylesheet | 3.1 KB |
| 📁assets | Folder | 472.8 KB |
| 📄logo.svg | SVG image | 2 KB |
| 📄hero.png | PNG image | 470.8 KB |
| 📄package.json | JSON | 1.2 KB |
| 📄README.md | Markdown | 4.4 KB |
| 📄notes.txt | Text | 1 KB |
Selected: (none)
<div class="w-full overflow-x-auto rounded-lg border border-zinc-200 bg-white">
<BitBhGrid As="table" TreeGrid="true" aria-label="Project files" class="w-full border-collapse text-sm text-zinc-900"
SelectionMode="BitBhGridSelectionMode.Single"
@bind-SelectedKeys="selectedEntry"
@bind-ExpandedKeys="expandedFolders">
<BitBhGridRowGroup As="thead">
<BitBhGridRow As="tr" IsHeaderRow="true">
<BitBhGridCell As="th" Header="true" class="border-b border-zinc-200 bg-zinc-50 px-3 py-2 text-start text-xs font-semibold uppercase tracking-wide whitespace-nowrap text-zinc-500 outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600">Name</BitBhGridCell>
<BitBhGridCell As="th" Header="true" class="border-b border-zinc-200 bg-zinc-50 px-3 py-2 text-start text-xs font-semibold uppercase tracking-wide whitespace-nowrap text-zinc-500 outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600">Kind</BitBhGridCell>
<BitBhGridCell As="th" Header="true" class="border-b border-zinc-200 bg-zinc-50 px-3 py-2 text-end text-xs font-semibold uppercase tracking-wide whitespace-nowrap text-zinc-500 outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600">Size</BitBhGridCell>
</BitBhGridRow>
</BitBhGridRowGroup>
<BitBhGridRowGroup As="tbody" class="[&>tr:last-child>*]:border-b-0">
@foreach (var entry in fileSystem)
{
<BitBhGridRow As="tr" @key="entry.Path" Key="@entry.Path"
Level="entry.Level" ParentKey="@entry.Parent" HasChildren="entry.IsFolder"
class="group cursor-pointer select-none hover:bg-zinc-50 aria-selected:bg-blue-50 aria-selected:hover:bg-blue-100">
<BitBhGridCell As="td" class="border-b border-zinc-100 px-3 py-2 whitespace-nowrap outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600">
<span class="flex items-center gap-1.5 pl-[calc((var(--bit-bh-grid-level,1)_-_1)_*_1.25rem)]">
@if (entry.IsFolder)
{
<BitBhGridRowToggle class="inline-flex size-5 flex-none cursor-pointer items-center justify-center rounded text-xs text-zinc-400 transition-transform duration-150 hover:bg-zinc-200 hover:text-zinc-900 group-data-[state=open]:rotate-90">▸</BitBhGridRowToggle>
<span>@(expandedFolders.Contains(entry.Path) ? "📂" : "📁")</span>
}
else
{
<span class="w-5 flex-none"></span>
<span>📄</span>
}
@entry.Name
</span>
</BitBhGridCell>
<BitBhGridCell As="td" class="border-b border-zinc-100 px-3 py-2 whitespace-nowrap outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600">@entry.Kind</BitBhGridCell>
<BitBhGridCell As="td" class="border-b border-zinc-100 px-3 py-2 text-end tabular-nums whitespace-nowrap outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600">@FormatSize(SizeOf(entry))</BitBhGridCell>
</BitBhGridRow>
}
</BitBhGridRowGroup>
</BitBhGrid>
</div>
<p class="text-sm text-zinc-500">Selected: <strong class="font-semibold text-zinc-900">@(selectedEntry.FirstOrDefault() ?? "(none)")</strong></p>
@code {
private sealed record FsEntry(string Path, string Name, string Kind, long Bytes, int Level, string? Parent, bool IsFolder);
// Flattened in document (depth-first) order.
private readonly FsEntry[] fileSystem =
[
new("project", "project", "Folder", 0, 1, null, true),
new("project/src", "src", "Folder", 0, 2, "project", true),
new("project/src/app.ts", "app.ts", "TypeScript", 14_236, 3, "project/src", false),
new("project/src/router.ts", "router.ts", "TypeScript", 6_812, 3, "project/src", false),
new("project/src/styles.css", "styles.css", "Stylesheet", 3_154, 3, "project/src", false),
new("project/assets", "assets", "Folder", 0, 2, "project", true),
new("project/assets/logo.svg", "logo.svg", "SVG image", 2_048, 3, "project/assets", false),
new("project/assets/hero.png", "hero.png", "PNG image", 482_113, 3, "project/assets", false),
new("project/package.json", "package.json", "JSON", 1_240, 2, "project", false),
new("project/README.md", "README.md", "Markdown", 4_502, 2, "project", false),
new("notes.txt", "notes.txt", "Text", 1_024, 1, null, false),
];
private IReadOnlyCollection<string> selectedEntry = [];
private IReadOnlyCollection<string> expandedFolders = ["project", "project/src"];
private long SizeOf(FsEntry entry) =>
entry.IsFolder ? fileSystem.Where(e => e.Parent == entry.Path).Sum(e => SizeOf(e)) : entry.Bytes;
private static string FormatSize(long bytes) => bytes switch
{
< 1024 => $"{bytes} B",
< 1024 * 1024 => $"{bytes / 1024.0:0.#} KB",
_ => $"{bytes / (1024.0 * 1024):0.#} MB",
};
}Div-based grid (calendar)
Without As the grid renders
<div role="grid">, rows and cells — lay them out
with grid-cols-[repeat(7,2.25rem)]. Arrows move by day / week;
Enter or click calls the cell's OnActivate.
Picked: (none)
<div class="inline-flex flex-col gap-2 self-start rounded-lg border border-zinc-200 bg-white p-3 text-sm text-zinc-900">
<div id="tw-grid-calendar-title" class="font-semibold">@MonthStart.ToString("MMMM yyyy", System.Globalization.CultureInfo.InvariantCulture)</div>
<BitBhGrid aria-labelledby="tw-grid-calendar-title" class="flex flex-col gap-1">
<BitBhGridRowGroup>
<BitBhGridRow IsHeaderRow="true" class="grid grid-cols-[repeat(7,2.25rem)] gap-1">
@foreach (var (shortName, longName) in weekdays)
{
<BitBhGridCell Header="true"
class="flex h-8 items-center justify-center rounded-md text-xs font-semibold text-zinc-500 outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600">
<abbr title="@longName" class="no-underline">@shortName</abbr>
</BitBhGridCell>
}
</BitBhGridRow>
</BitBhGridRowGroup>
<BitBhGridRowGroup class="flex flex-col gap-1">
@foreach (var week in Weeks)
{
<BitBhGridRow class="grid grid-cols-[repeat(7,2.25rem)] gap-1">
@foreach (var day in week)
{
<BitBhGridCell aria-selected="@(day == pickedDate ? "true" : null)"
aria-current="@(day == today ? "date" : null)"
aria-disabled="@(day.Month != MonthStart.Month ? "true" : null)"
OnActivate="() => PickDate(day)"
class="flex h-9 cursor-pointer select-none items-center justify-center rounded-md outline-none hover:bg-zinc-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 aria-[current=date]:font-bold aria-[current=date]:ring-1 aria-[current=date]:ring-inset aria-[current=date]:ring-blue-300 aria-selected:bg-blue-600 aria-selected:text-white aria-disabled:cursor-default aria-disabled:text-zinc-300 aria-disabled:hover:bg-transparent">@day.Day</BitBhGridCell>
}
</BitBhGridRow>
}
</BitBhGridRowGroup>
</BitBhGrid>
</div>
<p class="text-sm text-zinc-500">
Picked: <strong class="font-semibold text-zinc-900">@(pickedDate?.ToString("dddd, MMMM d", System.Globalization.CultureInfo.InvariantCulture) ?? "(none)")</strong>
</p>
@code {
private readonly DateOnly today = DateOnly.FromDateTime(DateTime.Today);
private DateOnly? pickedDate;
private readonly (string Short, string Long)[] weekdays =
[
("Su", "Sunday"), ("Mo", "Monday"), ("Tu", "Tuesday"), ("We", "Wednesday"),
("Th", "Thursday"), ("Fr", "Friday"), ("Sa", "Saturday"),
];
private DateOnly MonthStart => new(today.Year, today.Month, 1);
// Whole weeks (Sunday → Saturday) covering the current month.
private IEnumerable<DateOnly[]> Weeks
{
get
{
var monthEnd = MonthStart.AddMonths(1).AddDays(-1);
for (var start = MonthStart.AddDays(-(int)MonthStart.DayOfWeek); start <= monthEnd; start = start.AddDays(7))
{
var weekStart = start;
yield return Enumerable.Range(0, 7).Select(i => weekStart.AddDays(i)).ToArray();
}
}
}
// Days of the neighbouring months are aria-disabled and can't be picked.
private void PickDate(DateOnly day)
{
if (day.Month == MonthStart.Month) pickedDate = day;
}
}