Facets
Faceted value counts and range facets with useFacets, FacetedFilterSidebar, and the adapter facet contract.
Facets show live counts next to filter options ("Open (34)") and min/max ranges for numeric columns, computed by the adapter with the current filters applied. Self-exclusion is built in: a column's own filter never narrows its own facet, so picking "Open" doesn't collapse the status facet to one entry.
Client wiring
useFacets fetches counts and ranges; FacetedFilterSidebar renders them (both copied by init):
'use client';
import { FacetedFilterSidebar } from '@/components/better-tables-ui/filters/faceted-filter-sidebar';
import { useFacets } from '@/hooks/use-facets';
export function TicketsWithFacets({ adapter, columns }) {
const [filters, setFilters] = useState<FilterState[]>([]);
const { facets, ranges, loading, error, refetch } = useFacets({
adapter,
columnIds: ['status', 'priority'], // checkbox facets via adapter.getFacetedValues
rangeColumnIds: ['reopenCount'], // [min, max] via adapter.getMinMaxValues
filters, // pass the FULL filter state, unmodified
});
return (
<div className="flex gap-4">
<FacetedFilterSidebar
columns={columns}
columnIds={['status', 'priority']}
rangeColumnIds={['reopenCount']}
facets={facets}
ranges={ranges}
filters={filters}
onFiltersChange={setFilters}
/>
{/* table filtered by the same `filters` state */}
</div>
);
}UseFacetsOptions: adapter, columnIds, rangeColumnIds, filters, enabled. Results: facets (columnId → [{ value, count }]), ranges (columnId → [min, max]), loading, error, refetch(). Array literals for columnIds are fine on every render — the hook interns them with JSON.stringify so IDs that contain spaces cannot collide.
Do not pre-filter the filters you pass per column — self-exclusion is the adapter's job, and pre-filtering fights it.
Adapter contract
Facets ride on two adapter methods, both implemented by the Drizzle adapter and proxied by the HTTP adapter:
adapter.getFacetedValues(columnId, { filters, limit, signal }); // Map<string, number>
adapter.getMinMaxValues(columnId, { filters, signal }); // [min, max]FacetQueryParams: filters (flat array or FilterGroupNode tree), signal (abort), and limit — cap on distinct values, ordered by count descending (default 100; null disables the cap).
fetchData can also return counts inline via its faceted result field (Record<columnId, Map<value, count>>) — over HTTP, Maps travel as [value, count][] and are rebuilt client-side (see HTTP adapter).
Live example
Source: apps/marketing/src/components/sections/facets-sidebar.tsx.