Filtering Dates
Create custom date filters in LyteNyte Grid. This guide covers common filters for date cell values.
Note
Applying a date filter varies depending on the row source. Refer to the guides below for each supported row data source:
While this guide focuses on using client row filtering, these filter concepts apply to all row sources.
Date Filters
LyteNyte Grid provides flexible filter capabilities that let you define custom date filters. You can check whether two dates are equal, whether a date falls within a specific range, or whether a date falls within a specific time period, such as the first quarter of the year.
Filtering dates is more complex than filtering other data types, since dates can have many representations, formats, and timezones to contend with. In this guide, dates are represented as ISO 8601 date strings.
To create a date filter, define a function that receives a row node and
returns true to keep the row or false to remove it.
For example, the following function filters a list of orders and keeps only orders with a sale date in 2025.
1import { getYear } from "data-fns";2
3const filter2025: Grid.T.FilterFn<GridSpec["data"]> = (row) => {4 return getYear(row.data.saleDate) === 2025;5};Click the Sales in 2025 switch to toggle the filter state.
Date Filter
1import "@1771technologies/lytenyte-pro/light-dark.css";2import { Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";3import { data, type DataItem } from "./data.js";4import { useId, useState, type CSSProperties } from "react";5import { Switch } from "radix-ui";6import { getYear } from "date-fns";7
8export interface GridSpec {9 readonly data: DataItem;10}11
12const columns: Grid.Column<GridSpec>[] = [13 { id: "id", name: "Product ID", width: 130 },14 { id: "saleDate", name: "Sale Date", width: 120, widthFlex: 1, cellRenderer: DateCell },15 { id: "category", name: "Category", widthFlex: 1 },16 { id: "name", name: "Name", widthFlex: 1, width: 160 },17 { id: "quantity", name: "Quantity", widthFlex: 1, type: "number", cellRenderer: NumberCell },18 { id: "price", name: "Price", widthFlex: 1, type: "number", cellRenderer: DollarCell },19 { id: "status", name: "Status", widthFlex: 1 },20 { id: "total", name: "Total", widthFlex: 1, type: "number", cellRenderer: DollarCell },21];22
23const base: Grid.ColumnBase<GridSpec> = { width: 120 };24
25const filter2025: Grid.T.FilterFn<GridSpec["data"]> = (row) => {26 return getYear(row.data.saleDate) === 2025;27};28
29export default function FilterDemo() {30 const [filterValues, setFilterValues] = useState(true);31 const ds = useClientDataSource<GridSpec>({32 data: data,33 filter: filterValues ? filter2025 : null,34 });35
36 return (37 <>38 <div className="border-ln-border flex w-full border-b px-2 py-2">39 <SwitchToggle40 label="Sales in 2025"41 checked={filterValues}42 onChange={() => {43 setFilterValues((prev) => !prev);44 }}45 />46 </div>47 <div className="ln-grid" style={{ height: 500 }}>48 <Grid rowSource={ds} columns={columns} columnBase={base} />49 </div>50 </>51 );52}53
54const formatter = new Intl.NumberFormat("en-US", {55 maximumFractionDigits: 2,56 minimumFractionDigits: 0,57});58export function DollarCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {59 const field = api.columnField(column, row);60
61 if (typeof field === "number") {62 if (field < 0) return `-$${formatter.format(Math.abs(field))}`;63
64 return "$" + formatter.format(field);65 }66
67 return `${field ?? "-"}`;68}69
70export function NumberCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {71 const field = api.columnField(column, row);72
73 return typeof field === "number" ? formatter.format(field) : `${field ?? "-"}`;74}75
76export function DateCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {77 const field = api.columnField(column, row);78
79 return <div className="flex h-full w-full items-center tabular-nums">{String(field)}</div>;80}81export function SwitchToggle(props: { label: string; checked: boolean; onChange: (b: boolean) => void }) {82 const id = useId();83 return (84 <div className="flex items-center gap-2">85 <label className="text-ln-text-dark text-sm leading-none" htmlFor={id}>86 {props.label}87 </label>88 <Switch.Root89 className="bg-ln-gray-10 data-[state=checked]:bg-ln-primary-50 h-5.5 w-9.5 border-ln-border-strong relative cursor-pointer rounded-full border outline-none"90 id={id}91 checked={props.checked}92 onCheckedChange={(c) => props.onChange(c)}93 style={{ "-webkit-tap-highlight-color": "rgba(0, 0, 0, 0)" } as CSSProperties}94 >95 <Switch.Thumb className="size-4.5 block translate-x-0.5 rounded-full bg-white/95 shadow transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-4 data-[state=checked]:bg-white" />96 </Switch.Root>97 </div>98 );99}Date Filter Model
Define date filters dynamically rather than relying on predefined logic. LyteNyte Grid lets you define a filter model and a set of operations to build custom filters. This section presents one approach. Design the filter model that best fits your application’s requirements.
Begin by defining the type representation for your date filter. The code below defines a basic filter model you can use to create a filter function for the client row data source:
1export type FilterDateOperator = "equals" | "before" | "after" | "quarter" | "month";2
3export interface FilterDate {4 readonly operator: FilterDateOperator;5 readonly value: string | number;6}7
8export interface GridFilter {9 readonly left: FilterDate;10 readonly right: FilterDate | null;11 readonly operator: "AND" | "OR";12}13
14export interface GridSpec {15 readonly data: DataItem;16 readonly api: {17 readonly filterModel: PieceWritable<Record<string, GridFilter>>;18 };19}Open the filter popover by clicking the funnel icon on the Sale Date column.
Date Filter User Interface
1import "@1771technologies/lytenyte-pro/light-dark.css";2import {3 Grid,4 useClientDataSource,5 usePiece,6 type PieceWritable,7} from "@1771technologies/lytenyte-pro";8import { data, type DataItem } from "./data.js";9import { useId, useMemo, useState, type CSSProperties } from "react";10import { Switch } from "radix-ui";11import { Header } from "./filter.jsx";12import { isAfter, isBefore, isEqual, isTomorrow, isToday, isYesterday } from "date-fns";13import {14 isInPeriod,15 isLastMonth,16 isLastQuarter,17 isLastWeek,18 isLastYear,19 isNextMonth,20 isNextQuarter,21 isNextWeek,22 isNextYear,23 isThisMonth,24 isThisQuarter,25 isThisWeek,26 isThisYear,27 isYtd,28 type Period,29} from "./date-utils.js";30
31export interface GridSpec {32 readonly data: DataItem;33 readonly api: {34 readonly filterModel: PieceWritable<Record<string, GridFilter>>;35 };36}37
38const columns: Grid.Column<GridSpec>[] = [39 { id: "id", name: "Product ID", width: 130 },40 {41 id: "saleDate",42 name: "Sale Date",43 width: 120,44 widthFlex: 1,45 cellRenderer: DateCell,46 headerRenderer: Header,47 },48 { id: "category", name: "Category", widthFlex: 1 },49 { id: "name", name: "Name", widthFlex: 1, width: 160 },50 { id: "quantity", name: "Quantity", widthFlex: 1, type: "number", cellRenderer: NumberCell },51 { id: "price", name: "Price", widthFlex: 1, type: "number", cellRenderer: DollarCell },52 { id: "status", name: "Status", widthFlex: 1 },53 { id: "total", name: "Total", widthFlex: 1, type: "number", cellRenderer: DollarCell },54];55
56export type FilterDateOperator =57 | "equals"58 | "before"59 | "after"60 | "tomorrow"61 | "today"62 | "yesterday"63 | "next_week"64 | "this_week"65 | "last_week"66 | "next_month"67 | "this_month"68 | "last_month"69 | "next_month"70 | "this_month"71 | "last_month"72 | "next_quarter"73 | "this_quarter"74 | "last_quarter"75 | "next_year"76 | "this_year"77 | "last_year"78 | "year_to_date"79 | "all_dates_in_the_period";80
81export interface FilterDate {82 readonly operator: FilterDateOperator;83 readonly value: string;84}85
86export interface GridFilter {87 readonly left: FilterDate;88 readonly right: FilterDate | null;89 readonly operator: "AND" | "OR";90}91
92const base: Grid.ColumnBase<GridSpec> = { width: 120 };93
94export default function FilterDemo() {95 const [filter, setFilter] = useState<Record<string, GridFilter>>({});96 const filterModel = usePiece(filter, setFilter);97
98 const filterFn = useMemo(() => {99 const entries = Object.entries(filter);100
101 const evaluateDateFilter = (operator: FilterDateOperator, compare: string, value: string) => {102 if (operator === "equals") return isEqual(compare, value);103 if (operator === "after") return isAfter(compare, value);104 if (operator === "before") return isBefore(compare, value);105
106 if (operator === "tomorrow") return isTomorrow(compare);107 if (operator === "today") return isToday(compare);108 if (operator === "yesterday") return isYesterday(compare);109
110 if (operator === "next_week") return isNextWeek(compare);111 if (operator === "next_month") return isNextMonth(compare);112 if (operator === "next_quarter") return isNextQuarter(compare);113 if (operator === "next_year") return isNextYear(compare);114
115 if (operator === "this_week") return isThisWeek(compare);116 if (operator === "this_month") return isThisMonth(compare);117 if (operator === "this_quarter") return isThisQuarter(compare);118 if (operator === "this_year") return isThisYear(compare);119
120 if (operator === "last_week") return isLastWeek(compare);121 if (operator === "last_month") return isLastMonth(compare);122 if (operator === "last_quarter") return isLastQuarter(compare);123 if (operator === "last_year") return isLastYear(compare);124
125 if (operator === "year_to_date") return isYtd(compare);126
127 if (operator === "all_dates_in_the_period") {128 return isInPeriod(compare, value as Period);129 }130
131 return false;132 };133
134 return entries.map<Grid.T.FilterFn<GridSpec["data"]>>(([column, filter]) => {135 return (row) => {136 const value = row.data[column as keyof GridSpec["data"]];137
138 // We are only working with number filters, so lets filter out none number139 if (typeof value !== "string") return false;140
141 const compareValue = value;142
143 const leftResult = evaluateDateFilter(filter.left.operator, compareValue, filter.left.value);144 if (!filter.right) return leftResult;145
146 if (filter.operator === "OR")147 return leftResult || evaluateDateFilter(filter.right.operator, compareValue, filter.right.value);148
149 return leftResult && evaluateDateFilter(filter.right.operator, compareValue, filter.right.value);150 };151 });152 }, [filter]);153
154 const ds = useClientDataSource<GridSpec>({155 data: data,156 filter: filterFn,157 });158
159 return (160 <div className="ln-grid" style={{ height: 500 }}>161 <Grid162 apiExtension={useMemo(() => ({ filterModel }), [filterModel])}163 rowSource={ds}164 columns={columns}165 columnBase={base}166 />167 </div>168 );169}170
171const formatter = new Intl.NumberFormat("en-US", {172 maximumFractionDigits: 2,173 minimumFractionDigits: 0,174});175export function DollarCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {176 const field = api.columnField(column, row);177
178 if (typeof field === "number") {179 if (field < 0) return `-$${formatter.format(Math.abs(field))}`;180
181 return "$" + formatter.format(field);182 }183
184 return `${field ?? "-"}`;185}186
187export function NumberCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {188 const field = api.columnField(column, row);189
190 return typeof field === "number" ? formatter.format(field) : `${field ?? "-"}`;191}192
193export function DateCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {194 const field = api.columnField(column, row);195
196 return <div className="flex h-full w-full items-center tabular-nums">{String(field)}</div>;197}198export function SwitchToggle(props: { label: string; checked: boolean; onChange: (b: boolean) => void }) {199 const id = useId();200 return (201 <div className="flex items-center gap-2">202 <label className="text-ln-text-dark text-sm leading-none" htmlFor={id}>203 {props.label}204 </label>205 <Switch.Root206 className="bg-ln-gray-10 data-[state=checked]:bg-ln-primary-50 h-5.5 w-9.5 border-ln-border-strong relative cursor-pointer rounded-full border outline-none"207 id={id}208 checked={props.checked}209 onCheckedChange={(c) => props.onChange(c)}210 style={{ "-webkit-tap-highlight-color": "rgba(0, 0, 0, 0)" } as CSSProperties}211 >212 <Switch.Thumb className="size-4.5 block translate-x-0.5 rounded-full bg-white/95 shadow transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-4 data-[state=checked]:bg-white" />213 </Switch.Root>214 </div>215 );216}1import "@1771technologies/lytenyte-pro/components.css";2import { type Grid } from "@1771technologies/lytenyte-pro";3import { CaretRightIcon, ChevronDownIcon } from "@radix-ui/react-icons";4import { useState } from "react";5import { twMerge } from "tailwind-merge";6import clsx, { type ClassValue } from "clsx";7import type { GridFilter, GridSpec } from "./demo";8import { Menu, Popover } from "@1771technologies/lytenyte-pro/components";9
10function tw(...c: ClassValue[]) {11 return twMerge(clsx(...c));12}13
14export function Header({ api, column }: Grid.T.HeaderParams<GridSpec>) {15 const label = column.name ?? column.id;16
17 const model = api.filterModel.useValue();18 const hasFilter = !!model[column.id];19 return (20 <div className="flex h-full w-full items-center justify-between">21 <div>{label}</div>22
23 <Popover>24 <Popover.Trigger data-ln-button="secondary" data-ln-icon data-ln-size="sm" className="relative">25 <div className="sr-only">Filter the {label}</div>26 <svg27 xmlns="http://www.w3.org/2000/svg"28 width="16"29 height="16"30 fill="currentcolor"31 viewBox="0 0 256 256"32 >33 <path d="M230.6,49.53A15.81,15.81,0,0,0,216,40H40A16,16,0,0,0,28.19,66.76l.08.09L96,139.17V216a16,16,0,0,0,24.87,13.32l32-21.34A16,16,0,0,0,160,194.66V139.17l67.74-72.32.08-.09A15.8,15.8,0,0,0,230.6,49.53ZM40,56h0Zm106.18,74.58A8,8,0,0,0,144,136v58.66L112,216V136a8,8,0,0,0-2.16-5.47L40,56H216Z"></path>34 </svg>35 {hasFilter && <div className="bg-ln-primary-50 absolute right-px top-px size-2 rounded-full" />}36 </Popover.Trigger>37 <Popover.Container>38 <Popover.Arrow />39 <Popover.Title className="sr-only">Filter {label}</Popover.Title>40 <Popover.Description className="sr-only">Filter the numbers in the{label}</Popover.Description>41 <TextFilterControl column={column} filter={model[column.id] ?? null} api={api}></TextFilterControl>42 </Popover.Container>43 </Popover>44 </div>45 );46}47
48const selectOptions = [49 { id: "equals", label: "Equals" },50 { id: "separator-1", selectable: false },51 { id: "before", label: "Before" },52 { id: "after", label: "After" },53 { id: "between", label: "After" },54 { id: "separator-2", selectable: false },55 { id: "tomorrow", label: "Tomorrow" },56 { id: "today", label: "Today" },57 { id: "yesterday", label: "Yesterday" },58 { id: "separator-3", selectable: false },59 { id: "next_week", label: "Next Week" },60 { id: "this_week", label: "This Week" },61 { id: "last_week", label: "Last Week" },62 { id: "separator-4", selectable: false },63 { id: "next_month", label: "Next Month" },64 { id: "this_month", label: "This Month" },65 { id: "last_month", label: "Last Month" },66 { id: "separator-5", selectable: false },67 { id: "next_quarter", label: "Next Quarter" },68 { id: "this_quarter", label: "This Quarter" },69 { id: "last_quarter", label: "Last Quarter" },70 { id: "separator-6", selectable: false },71 { id: "next_year", label: "Next Year" },72 { id: "this_year", label: "This Year" },73 { id: "last_year", label: "Last Year" },74 { id: "separator-7", selectable: false },75 { id: "year_to_date", label: "Year to Date" },76 { id: "separator-8", selectable: false },77 { id: "all_dates_in_the_period", label: "All Dates in the Period" },78];79
80const periodOptions: Record<string, string> = {81 q1: "Quarter 1",82 q2: "Quarter 2",83 q3: "Quarter 3",84 q4: "Quarter 4",85 jan: "January",86 feb: "February",87 mar: "March",88 apr: "April",89 may: "May",90 jun: "June",91 jul: "July",92 aug: "August",93 sep: "September",94 oct: "October",95 nov: "November",96 dec: "December",97};98
99type DeepPartial<T> = T extends object100 ? {101 -readonly [P in keyof T]?: DeepPartial<T[P]>;102 }103 : T;104
105function TextFilterControl({106 api,107 filter: initialFilter,108 column,109}: {110 api: Grid.API<GridSpec>;111 filter: GridFilter | null;112 column: Grid.Column<GridSpec>;113}) {114 const [filter, setFilter] = useState<DeepPartial<GridFilter> | null>(initialFilter);115
116 const leftOperator = filter?.left?.operator;117 const rightOperator = filter?.right?.operator;118
119 const showInput = leftOperator === "equals" || leftOperator === "after" || leftOperator === "before";120
121 const leftValue =122 selectOptions.find((x) => typeof x !== "string" && x.id === filter?.left?.operator) ?? null;123 const rightValue =124 selectOptions.find((x) => typeof x !== "string" && x.id === filter?.right?.operator) ?? null;125
126 const combineOperator = filter?.operator ?? "AND";127 const canShowSecond = leftOperator === "before" && rightOperator === "after";128
129 const canSubmit =130 (filter?.left?.value != null && filter.left?.operator) ||131 (filter?.right?.value != null && filter.right.operator);132
133 const popoverControls = Popover.useControls();134 return (135 <form136 className="grid grid-cols-1 gap-2 md:grid-cols-2"137 onSubmit={(e) => {138 if (!canSubmit) return;139 e.preventDefault();140
141 const finalFilter: DeepPartial<GridFilter> = {};142
143 if (filter?.left?.value != null && filter?.left.operator) finalFilter.left = filter.left;144 if (filter?.right?.value != null && filter?.right.operator) {145 // If the left filter is incomplete then we use the right filter value as the left filter.146 if (!finalFilter.left) {147 finalFilter.left = filter.right;148 finalFilter.right = null;149 } else {150 finalFilter.right = filter.right;151 }152 }153
154 finalFilter.operator = combineOperator;155
156 api.filterModel.set((prev) => ({ ...prev, [column.id]: finalFilter as GridFilter }));157 popoverControls.openChange(false);158 }}159 >160 <div className="text-ln-text hidden ps-2 text-sm md:block">Operator</div>161 <div className="text-ln-text hidden ps-2 text-sm md:block">{showInput && "Values"}</div>162
163 <Menu>164 <Menu.Trigger165 data-ln-input166 className={tw(167 "flex min-w-40 cursor-pointer items-center justify-between",168 !showInput && "col-span-2",169 )}170 type="button"171 >172 <div>173 {leftOperator === "all_dates_in_the_period"174 ? `All dates in ${periodOptions[filter?.left?.value ?? ""]}`175 : (leftValue?.label ?? "Select...")}176 </div>177 <div>178 <ChevronDownIcon />179 </div>180 </Menu.Trigger>181 <Menu.Popover>182 <Menu.Container className="min-w-(--ln-anchor-width)">183 <Menu.Item184 onAction={() => {185 if (leftOperator === "equals") return;186 setFilter({ left: { operator: "equals" } });187 }}188 >189 Equals190 </Menu.Item>191 <Menu.Divider />192 <Menu.Item193 onAction={() => {194 if (leftOperator === "before") return;195 setFilter({ left: { operator: "before" } });196 }}197 >198 Before199 </Menu.Item>200 <Menu.Item201 onAction={() => {202 if (leftOperator === "after") return;203 setFilter({ left: { operator: "after" } });204 }}205 >206 After207 </Menu.Item>208 <Menu.Item209 onAction={() => {210 if (leftOperator === "before" && rightOperator === "after") return;211 setFilter({ left: { operator: "before" }, right: { operator: "after" } });212 }}213 >214 Between215 </Menu.Item>216 <Menu.Divider />217 <Menu.Item218 onAction={() => {219 setFilter({ left: { operator: "tomorrow", value: "" } });220 }}221 >222 Tomorrow223 </Menu.Item>224 <Menu.Item225 onAction={() => {226 setFilter({ left: { operator: "today", value: "" } });227 }}228 >229 Today230 </Menu.Item>231 <Menu.Item232 onAction={() => {233 setFilter({ left: { operator: "yesterday", value: "" } });234 }}235 >236 Yesterday237 </Menu.Item>238 <Menu.Divider />239 <Menu.Item240 onAction={() => {241 setFilter({ left: { operator: "next_week", value: "" } });242 }}243 >244 Next Week245 </Menu.Item>246 <Menu.Item247 onAction={() => {248 setFilter({ left: { operator: "this_week", value: "" } });249 }}250 >251 This Week252 </Menu.Item>253 <Menu.Item254 onAction={() => {255 setFilter({ left: { operator: "last_week", value: "" } });256 }}257 >258 Last Week259 </Menu.Item>260 <Menu.Divider />261 <Menu.Item262 onAction={() => {263 setFilter({ left: { operator: "next_month", value: "" } });264 }}265 >266 Next Month267 </Menu.Item>268 <Menu.Item269 onAction={() => {270 setFilter({ left: { operator: "this_month", value: "" } });271 }}272 >273 This Month274 </Menu.Item>275 <Menu.Item276 onAction={() => {277 setFilter({ left: { operator: "last_month", value: "" } });278 }}279 >280 Last Month281 </Menu.Item>282 <Menu.Divider />283 <Menu.Item284 onAction={() => {285 setFilter({ left: { operator: "next_quarter", value: "" } });286 }}287 >288 Next Quarter289 </Menu.Item>290 <Menu.Item291 onAction={() => {292 setFilter({ left: { operator: "this_quarter", value: "" } });293 }}294 >295 This Quarter296 </Menu.Item>297 <Menu.Item298 onAction={() => {299 setFilter({ left: { operator: "last_quarter", value: "" } });300 }}301 >302 Last Quarter303 </Menu.Item>304 <Menu.Divider />305 <Menu.Item306 onAction={() => {307 setFilter({ left: { operator: "next_year", value: "" } });308 }}309 >310 Next Year311 </Menu.Item>312 <Menu.Item313 onAction={() => {314 setFilter({ left: { operator: "this_year", value: "" } });315 }}316 >317 This Year318 </Menu.Item>319 <Menu.Item320 onAction={() => {321 setFilter({ left: { operator: "last_year", value: "" } });322 }}323 >324 Last Year325 </Menu.Item>326 <Menu.Divider />327 <Menu.Item328 onAction={() => {329 setFilter({ left: { operator: "year_to_date", value: "" } });330 }}331 >332 Year to Date333 </Menu.Item>334 <Menu.Divider />335 <Menu.Submenu>336 <Menu.SubmenuTrigger className="flex items-center justify-between gap-2">337 <div>All Dates in the Period</div>338 <CaretRightIcon />339 </Menu.SubmenuTrigger>340 <Menu.SubmenuContainer>341 <Menu.Item342 onAction={() => setFilter({ left: { operator: "all_dates_in_the_period", value: "q1" } })}343 >344 Quarter 1345 </Menu.Item>346 <Menu.Item347 onAction={() => setFilter({ left: { operator: "all_dates_in_the_period", value: "q2" } })}348 >349 Quarter 2350 </Menu.Item>351 <Menu.Item352 onAction={() => setFilter({ left: { operator: "all_dates_in_the_period", value: "q3" } })}353 >354 Quarter 3355 </Menu.Item>356 <Menu.Item357 onAction={() => setFilter({ left: { operator: "all_dates_in_the_period", value: "q4" } })}358 >359 Quarter 4360 </Menu.Item>361 <Menu.Item362 onAction={() => setFilter({ left: { operator: "all_dates_in_the_period", value: "jan" } })}363 >364 January365 </Menu.Item>366 <Menu.Item367 onAction={() => setFilter({ left: { operator: "all_dates_in_the_period", value: "feb" } })}368 >369 February370 </Menu.Item>371 <Menu.Item372 onAction={() => setFilter({ left: { operator: "all_dates_in_the_period", value: "mar" } })}373 >374 March375 </Menu.Item>376 <Menu.Item377 onAction={() => setFilter({ left: { operator: "all_dates_in_the_period", value: "apr" } })}378 >379 April380 </Menu.Item>381 <Menu.Item382 onAction={() => setFilter({ left: { operator: "all_dates_in_the_period", value: "may" } })}383 >384 May385 </Menu.Item>386 <Menu.Item387 onAction={() => setFilter({ left: { operator: "all_dates_in_the_period", value: "jun" } })}388 >389 June390 </Menu.Item>391 <Menu.Item392 onAction={() => setFilter({ left: { operator: "all_dates_in_the_period", value: "jul" } })}393 >394 July395 </Menu.Item>396 <Menu.Item397 onAction={() => setFilter({ left: { operator: "all_dates_in_the_period", value: "aug" } })}398 >399 August400 </Menu.Item>401 <Menu.Item402 onAction={() => setFilter({ left: { operator: "all_dates_in_the_period", value: "sep" } })}403 >404 September405 </Menu.Item>406 <Menu.Item407 onAction={() => setFilter({ left: { operator: "all_dates_in_the_period", value: "oct" } })}408 >409 October410 </Menu.Item>411 <Menu.Item412 onAction={() => setFilter({ left: { operator: "all_dates_in_the_period", value: "nov" } })}413 >414 November415 </Menu.Item>416 <Menu.Item417 onAction={() => setFilter({ left: { operator: "all_dates_in_the_period", value: "dev" } })}418 >419 December420 </Menu.Item>421 </Menu.SubmenuContainer>422 </Menu.Submenu>423 </Menu.Container>424 </Menu.Popover>425 </Menu>426
427 {showInput && (428 <div>429 <label>430 <span className="sr-only">Value for the first filter</span>431 <input432 disabled={!filter?.left}433 data-ln-input434 value={filter?.left?.value ?? ""}435 className="w-full text-xs"436 type="date"437 onChange={(e) => {438 setFilter((prev) => {439 if (!prev) return { left: { value: e.target.value } };440
441 return { ...prev, left: { ...prev.left, value: e.target.value } };442 });443 }}444 />445 </label>446 </div>447 )}448
449 {canShowSecond && (450 <>451 <div className="grid grid-cols-2 py-1 md:col-span-2 md:grid-cols-subgrid">452 <label className="flex justify-end gap-2 pe-2">453 <input454 type="radio"455 value="AND"456 name="operator"457 className={tw(458 "border-ln-gray-40 checked:border-ln-primary-50 h-4 w-4 cursor-pointer select-none appearance-none rounded-full border checked:border-[5px]",459 "focus-visible:outline-ln-primary-50 focus-visible:outline focus-visible:outline-offset-1",460 )}461 checked={combineOperator === "AND"}462 onChange={() => {463 setFilter((prev) => {464 if (!prev) return { operator: "AND" };465 return { ...prev, operator: "AND" };466 });467 }}468 />469 <span>And</span>470 </label>471 <label className="flex gap-2 ps-2">472 <input473 type="radio"474 value="OR"475 name="operator"476 className={tw(477 "border-ln-gray-40 checked:border-ln-primary-50 h-4 w-4 cursor-pointer select-none appearance-none rounded-full border checked:border-[5px]",478 "focus-visible:outline-ln-primary-50 focus-visible:outline focus-visible:outline-offset-1",479 )}480 checked={combineOperator === "OR"}481 onChange={() => {482 setFilter((prev) => {483 if (!prev) return { operator: "OR" };484 return { ...prev, operator: "OR" };485 });486 }}487 />488 <span>Or</span>489 </label>490 </div>491
492 <button disabled type="button" data-ln-input className="flex min-w-40 items-center justify-between">493 <div>{rightValue?.label ?? "Select..."}</div>494 </button>495
496 <div>497 <label>498 <span className="sr-only">Value for the second filter</span>499 <input500 data-ln-input501 value={filter?.right?.value ?? ""}502 className="w-full text-xs"503 type="date"504 onChange={(e) => {505 setFilter((prev) => {506 if (!prev) return { right: { value: e.target.value } };507
508 return { ...prev, right: { ...prev.right, value: e.target.value } };509 });510 }}511 />512 </label>513 </div>514 </>515 )}516
517 <div className="flex items-center justify-between gap-4 md:col-span-2 md:grid md:grid-cols-subgrid">518 <div className="pt-2">519 <button520 data-ln-button="tertiary"521 data-ln-size="sm"522 type="button"523 className="hover:bg-ln-gray-30"524 onClick={() => popoverControls.openChange(false)}525 >526 Cancel527 </button>528 </div>529 <div className="flex justify-end gap-2 pt-2">530 <button531 data-ln-button="secondary"532 data-ln-size="sm"533 type="button"534 className="hover:bg-ln-bg-button-light"535 onClick={() => {536 api.filterModel.set((prev) => {537 const next = { ...prev };538 delete next[column.id];539
540 return next;541 });542 popoverControls.openChange(false);543 }}544 >545 Clear546 </button>547 <button data-ln-button="primary" data-ln-size="sm" disabled={!canSubmit}>548 Apply Filters549 </button>550 </div>551 </div>552 </form>553 );554}1// date-buckets.ts2import {3 addMonths,4 addQuarters,5 addWeeks,6 addYears,7 endOfMonth,8 endOfQuarter,9 endOfWeek,10 endOfYear,11 isWithinInterval,12 parseISO,13 startOfMonth,14 startOfQuarter,15 startOfWeek,16 startOfYear,17} from "date-fns";18
19/**20 * Excel-like behavior:21 * - Uses TODAY as the base date.22 * - Weeks start on Sunday.23 * - Calendar-based week/month/quarter/year buckets.24 */25
26type DateInput = Date | string;27
28const toDate = (input: DateInput): Date => (input instanceof Date ? input : parseISO(input));29
30const inInterval = (date: Date, start: Date, end: Date) => isWithinInterval(date, { start, end });31
32const today = () => new Date();33
34// -------------------- WEEK --------------------35const weekBounds = (base: Date) => {36 const start = startOfWeek(base, { weekStartsOn: 0 });37 const end = endOfWeek(base, { weekStartsOn: 0 });38 return { start, end };39};40
41export function isThisWeek(input: DateInput): boolean {42 const date = toDate(input);43 const { start, end } = weekBounds(today());44 return inInterval(date, start, end);45}46
47export function isNextWeek(input: DateInput): boolean {48 const date = toDate(input);49 const next = addWeeks(today(), 1);50 const { start, end } = weekBounds(next);51 return inInterval(date, start, end);52}53
54export function isLastWeek(input: DateInput): boolean {55 const date = toDate(input);56 const last = addWeeks(today(), -1);57 const { start, end } = weekBounds(last);58 return inInterval(date, start, end);59}60
61// -------------------- MONTH --------------------62export function isThisMonth(input: DateInput): boolean {63 const date = toDate(input);64 const base = today();65 return inInterval(date, startOfMonth(base), endOfMonth(base));66}67
68export function isNextMonth(input: DateInput): boolean {69 const date = toDate(input);70 const base = addMonths(today(), 1);71 return inInterval(date, startOfMonth(base), endOfMonth(base));72}73
74export function isLastMonth(input: DateInput): boolean {75 const date = toDate(input);76 const base = addMonths(today(), -1);77 return inInterval(date, startOfMonth(base), endOfMonth(base));78}79
80// -------------------- QUARTER --------------------81export function isThisQuarter(input: DateInput): boolean {82 const date = toDate(input);83 const base = today();84 return inInterval(date, startOfQuarter(base), endOfQuarter(base));85}86
87export function isNextQuarter(input: DateInput): boolean {88 const date = toDate(input);89 const base = addQuarters(today(), 1);90 return inInterval(date, startOfQuarter(base), endOfQuarter(base));91}92
93export function isLastQuarter(input: DateInput): boolean {94 const date = toDate(input);95 const base = addQuarters(today(), -1);96 return inInterval(date, startOfQuarter(base), endOfQuarter(base));97}98
99// -------------------- YEAR --------------------100export function isThisYear(input: DateInput): boolean {101 const date = toDate(input);102 const base = today();103 return inInterval(date, startOfYear(base), endOfYear(base));104}105
106export function isNextYear(input: DateInput): boolean {107 const date = toDate(input);108 const base = addYears(today(), 1);109 return inInterval(date, startOfYear(base), endOfYear(base));110}111
112export function isLastYear(input: DateInput): boolean {113 const date = toDate(input);114 const base = addYears(today(), -1);115 return inInterval(date, startOfYear(base), endOfYear(base));116}117
118// -------------------- YTD --------------------119export function isYtd(input: DateInput): boolean {120 const date = toDate(input);121 const start = startOfYear(today());122 const end = today();123 return inInterval(date, start, end);124}125
126// -------------------- IS IN PERIOD --------------------127
128type MonthPeriod =129 | "jan"130 | "feb"131 | "mar"132 | "apr"133 | "may"134 | "jun"135 | "jul"136 | "aug"137 | "sep"138 | "oct"139 | "nov"140 | "dec";141
142type QuarterPeriod = "q1" | "q2" | "q3" | "q4";143
144export type Period = MonthPeriod | QuarterPeriod;145
146const MONTH_INDEX: Record<MonthPeriod, number> = {147 jan: 0,148 feb: 1,149 mar: 2,150 apr: 3,151 may: 4,152 jun: 5,153 jul: 6,154 aug: 7,155 sep: 8,156 oct: 9,157 nov: 10,158 dec: 11,159};160
161const QUARTER_START_MONTH: Record<QuarterPeriod, number> = {162 q1: 0, // Jan163 q2: 3, // Apr164 q3: 6, // Jul165 q4: 9, // Oct166};167
168/**169 * Returns true if `date` falls within the given `period`.170 *171 * Excel-like interpretation:172 * - Month period ("jan".."dec"): checks month match (in the date's own year).173 * - Quarter period ("q1".."q4"): checks quarter match (in the date's own year).174 *175 * Examples:176 * isInPeriod("2026-02-10", "feb") => true177 * isInPeriod("2026-02-10", "q1") => true178 * isInPeriod("2026-10-01", "q4") => true179 */180export function isInPeriod(dateInput: DateInput, period: Period): boolean {181 const date = toDate(dateInput);182 const p = period.toLowerCase() as Period;183
184 if (p.startsWith("q")) {185 const startMonth = QUARTER_START_MONTH[p as QuarterPeriod];186 const year = date.getFullYear();187 const start = startOfQuarter(new Date(year, startMonth, 1));188 const end = endOfQuarter(new Date(year, startMonth, 1));189 return inInterval(date, start, end);190 }191
192 const monthIndex = MONTH_INDEX[p as MonthPeriod];193 const year = date.getFullYear();194 const start = startOfMonth(new Date(year, monthIndex, 1));195 const end = endOfMonth(new Date(year, monthIndex, 1));196 return inInterval(date, start, end);197}Note
The demo dynamically generates date values for each row based on the current date. It demonstrates the full range of supported date filters. Since the demo generates dates at runtime, the values change depending on the current date.
The code below defines the filter model. It creates filter model state and passes it to the grid API as an extension. The demo also includes a filter UI for applying filters interactively.
When a user applies a filter, useMemo creates a new filterFn, which you then pass to the
client data source. See the filter.tsx file in the demo’s expanded code for the logic that
builds the filter UI.
1const [filter, setFilter] = useState<Record<string, GridFilter>>({});2const filterModel = usePiece(filter, setFilter);3
4const filterFn = useMemo(() => {5 const entries = Object.entries(filter);6
7 const evaluateDateFilter = (operator: FilterDateOperator, compare: string, value: string | number) => {8 if (operator === "equals") return isEqual(compare, value);9 if (operator === "after") return isAfter(compare, value);10 if (operator === "before") return isBefore(compare, value);11 if (operator === "month") return getMonth(compare) === value;12 if (operator === "quarter") return getQuarter(compare) === value;13 return false;14 };15
16 return entries.map<Grid.T.FilterFn<GridSpec["data"]>>(([column, filter]) => {17 return (row) => {16 collapsed lines
18 const value = row.data[column as keyof GridSpec["data"]];19
20 // This filter operates on date strings, so return early for non-string values21 if (typeof value !== "string") return false;22
23 const compareValue = value;24
25 const leftResult = evaluateDateFilter(filter.left.operator, compareValue, filter.left.value);26
27 if (!filter.right) return leftResult;28
29 if (filter.operator === "OR") {30 return leftResult || evaluateDateFilter(filter.right.operator, compareValue, filter.right.value);31 }32
33 return leftResult && evaluateDateFilter(filter.right.operator, compareValue, filter.right.value);34 };35 });36}, [filter]);Next Steps
- Filtering Numbers: Filter row data based on numerical cell values.
- Quick Search Filtering: Quickly find rows based on simple text searches.
- Set Filtering: Learn how to create custom set filters and filter rows.
