npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@astroapps/datagrid-fluent-ui

v1.1.0

Published

Style @astroapps/datagrid to match the FluentUI v9 DataGrid

Readme

@astroapps/datagrid-fluent-ui

Styles @astroapps/datagrid to look like the FluentUI v9 DataGrid, and wires up the row-level behaviour Fluent gets from its row elements.

The searching — sort, filter, paging, filter options, client vs server — belongs to @astroapps/datagrid-search, as does anything else a second renderer would need: row selection, and the rules for which row clicks count and when a pager is worth showing. This package renders what that gives it and adds no logic of its own. Read its README for how the state works; this one covers the Fluent side.

Every metric here was measured against a live Fluent v9 DataGrid rather than copied from docs — see src/app/fluentgrid/page.tsx in the formServer site for the harness that diffs the two grids' computed styles, and src/app/fluentgrid/features/page.tsx for a tour of the search features.

npm i @astroapps/datagrid-search @astroapps/datagrid-fluent-ui

A whole grid

const state = useControl<SearchRequest>({
  ...defaultSearchOptions,
  length: 25,
});
const data = useClientData(state, { rows, columns }); // or useServerData
const search = useGridSearch(state, { columns, data });

<FluentDataGrid search={search} size="medium" rowKey={(r) => r.id} />;

That's a sortable, filterable, paged grid. Everything it renders follows from the search rather than a flag:

| Affordance | Appears when | | ---------------- | --------------------------------------------------------------- | | Sort arrow | the column has a sortField | | Multi-sort badge | sort mode isn't "single" and more than one column is sorted | | Filter funnel | the column's filter options resolve (see datagrid-search) | | Pager | there's a page before or after this one, or pageSizes is set | | Selection column | a selection is passed |

So a grid whose columns carry none of that renders as a plain table. Grid-wide off switches exist for the blunt cases: sortable={false}, filterable={false}, pager={false}.

Must be rendered inside a FluentProvider.

Props beyond the search

<FluentDataGrid
  search={search}
  size="medium" // "medium" | "small" | "extra-small"
  rowKey={(r) => r.id}
  selection={selection}
  noData="Nothing here"
  pager // false to suppress, or a node to replace
  pageSizes={[10, 25, 50]}
  renderFilterPopup={(props) => <MyBody {...props} />}
  renderFilterControl={(column, search) => <MyInlineFilter />}
  renderHeaderExtra={(column) => <ColumnInfo column={column} />}
/>

DataGridClasses props (className, cellClass, …) pass through and win over the Fluent defaults.

Composing it yourself

When you need to own the <DataGrid> call — extra header rows, a custom body wrapper — use the hook and spread its parts:

const fluent = useFluentDataGrid(search, { size, selection, rowKey });

<DataGrid
  {...fluent.gridProps}
  columns={fluent.columns} // selection column already prepended
  bodyRows={search.data.rowProps.bodyRows}
  getBodyRow={search.data.rowProps.getBodyRow}
/>;

gridProps carries the classes, renderHeaderContent, the row wrapper and the per-cell RenderControl wrapper. fluent.parts exposes the part classes, for building custom cells that still look like Fluent's.

The pieces are also exported individually — useFluentDataGridStyles, fluentHeaderContent, fluentRowWrapper, fluentSelectionColumn, FluentFilterPopover, FilterOptionList, FluentPager — for grids that need to compose them differently.

Selection

Page-scoped: the header checkbox reflects and acts on the rows currently rendered, and never disturbs a selection made on another page.

const selectedIds = useControl<string[]>([]);
const selection = makeGridSelection({
  selected: selectedIds,
  rows: data.rows, // the current page
  getId: (r) => r.id,
});

arraySelection is the same thing over plain state. Neither is a hook, despite taking a control — they read .value when called, so call them during render.

Cross-page "select all N matching" is deliberately not supported: it needs the filtered total, the live search, and a way to fetch every matching id, at which point selection stops being a renderer concern.

Adding to a header

renderHeaderExtra renders after the filter funnel, for anything a column wants in its header that sorting and filtering don't cover:

renderHeaderExtra={(column) =>
  column.id === "margin" ? (
    <InfoTip className="shrink-0" text="Gross margin, ex-GST" />
  ) : undefined
}

It is a sibling of the sort button, not a child, so it can hold interactive content — nesting a button inside the sort button would be invalid HTML. The sort button fills the cell, so an extra that must stay visible needs flexShrink: 0.

To wrap the whole cell instead — a tooltip covering the title, arrow and funnel together — use the column's own renderHeader from @astroapps/datagrid, going through defaultRenderCell so the cell keeps its grid placement.

Custom filter popups

Three levels, most specific first:

  1. getColumnFilter(column).render — replaces one column's popup body, keeping the standard funnel and the loading/error/empty shell.
  2. renderFilterPopup on the grid — the same, as the default for every column.
  3. renderFilterControl on the grid — replaces the funnel and popup, for a column that wants an inline control instead.

The body receives FilterPopupProps, whose whole contract is a Control<string[] | undefined> for that column. FilterOptionList is exported so a custom body can keep the standard checkbox list and just add a header.

The body is a separate component mounted only while the popover is open, which is what makes an async option source lazy.

Finding rows and cells in tests

The grid is a single flat CSS grid: there's no <table>, and the row wrapper is display: contents, which takes it out of the accessibility tree — so neither getByRole("row") nor DOM walking gets you a cell. Address them by data attribute instead:

| Attribute | Where | Value | | ---------------- | -------------------- | ------------------------------------------ | | data-column | every cell | the column's id (its title by default) | | data-row-index | body cells, rows | 0-based index within the page | | data-row-key | row wrapper | your rowKey, else the index |

// By identity — survives sorting and paging, given a `rowKey`.
page.locator('[data-row-key="42"] [data-column="status"]');
// By position.
page.locator('[data-row-index="2"] [data-column="status"]');
// A whole column, header included.
page.locator('[data-column="status"]');
// The header cell alone.
page.locator('[data-column="status"]:not([data-row-index])');

data-row-key needs a rowKey, which is worth passing anyway — React reorders rather than rebuilds rows on sort:

<FluentDataGrid search={search} rowKey={(r) => r.id} />

Sort buttons and filter funnels are reachable through the same attributes, and each funnel is named after its column — Filter (Kind), or Filter (Kind, filtered) once something is applied:

page.locator('[data-column="status"] button').first(); // the sort button
page.getByLabel("Filter (Status)");

Row checkboxes are labelled "Select row" by default, which makes every row's checkbox identical to getByLabelText. Name the row instead:

<FluentDataGrid
  search={search}
  selection={selection}
  selectionColumn={{ rowAriaLabel: (r) => `Select ${r.name}` }}
/>

Excel-mode filtering

filterMode is a useGridSearch option, not a prop on this component — the funnels read it off the search. Set it to "excel" and every filter popup opens with each value ticked when the column is unfiltered, above a (Select All) row:

const search = useGridSearch(state, { columns, data, filterMode: "excel" });

Applying with everything ticked stores no filter at all, so the funnel goes back to idle; applying with nothing ticked is refused, and the Apply button is disabled to say so. Clear re-ticks everything, which is the way back out of an empty selection. The rules, and why the empty selection can't be stored, are in the datagrid-search README.

FilterOptionList takes the row as a selectAll prop, so a custom popup body can keep it:

<FilterOptionList
  options={options}
  selected={values}
  onToggle={draft.toggle}
  selectAll={{ checked, indeterminate, onToggle }}
/>

Styling

useFluentDataGridStyles returns the classes; the grid contributes no CSS of its own beyond them. Stable class names are on fluentDataGridClassNames, for CSS overrides and for tests that measure rendered cells.

Sizes match Fluent's: body rows are 44/34/32px of content, the header row is 32px at every size, and extra-small drops the row divider and uses 12px text.

Two structural differences from Fluent worth knowing:

  • No row box. The row wrapper is display: contents, so cells stay direct grid items and keep their explicit placement. Hover and selection are painted on the cells rather than the row — :hover still resolves up the ancestor chain.
  • Column widths. Fluent's resizable column sizing reserves a constant ~48px of the container and gives the slack to the last column, which a CSS grid template won't reproduce. Set the widths you want explicitly rather than letting either side distribute slack.

Pager

Fluent v9 ships no pagination component, so FluentPager is built from its primitives rather than matching a reference. It renders prev/next over offset/length with an optional page-size selector, and hides itself when everything fits on one page — unless pageSizes is set, since that selector is the only way back from a size that fits every row. Then it stays, with prev/next disabled.

It copes with a source that doesn't count: without a total it shows 1-10 instead of 1-10 of 42, and enables Next while the page comes back full. See pageInfo in datagrid-search for why counting is optional.