shadimage
niko-table
componentdata-table-column-date-filtersource: ok · hit

DataTable Column Date Filter

Column header date filter with calendar picker in a dropdown.

npx shadcn add "https://niko-table.com/r/data-table-column-date-filter.json"
free

Where it goes

navbardata-tablecalendar

Interaction

dropdown

Use cases

dashboard

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
iconslucide-react

Registry dependencies

https://niko-table.com/r/data-table-core.jsonbuttondropdown-menutooltippopovercalendar

Files2 · source cached 10/9/2026, 1:26:49 PM

src/components/niko-table/components/data-table-column-date-filter-options.tsx
"use client"

/**
 * niko-table — created by Semir N. (Semkoo, https://github.com/Semkoo) with AI assistance.
 *
 * Before reporting anything: please check the changelog first.
 *  - In-repo: ./CHANGELOG.md
 *  - Docs site: https://niko-table.com/changelog
 *
 * Found a bug or have a fix? Open an issue or PR on GitHub so other
 * users (and future LLMs reading this code) benefit:
 * https://github.com/Semkoo/niko-table-registry
 */
import React from "react"

import {
  TableColumnDateFilterOptions,
  TableColumnDateFilterMenu,
} from "../filters/table-column-date-filter"
import { useColumnHeaderContext } from "./data-table-column-header"

import type { DataTableColumn } from "../types"
import type { RowData } from "@tanstack/react-table"
/**
 * Date filter options for composing inside DataTableColumnActions using context.
 */
export function DataTableColumnDateFilterOptions<TData extends RowData, TValue>(
  props: Omit<
    React.ComponentProps<typeof TableColumnDateFilterOptions>,
    "column"
  > & {
    column?: DataTableColumn<TData, TValue>
  },
) {
  const context = useColumnHeaderContext<TData, TValue>(false)
  const column = props.column || context?.column

  if (!column) {
    console.warn(
      "DataTableColumnDateFilterOptions must be used within DataTableColumnHeaderRoot or provided with a column prop",
    )
    return null
  }

  return <TableColumnDateFilterOptions column={column} {...props} />
}

DataTableColumnDateFilterOptions.displayName =
  "DataTableColumnDateFilterOptions"

/**
 * Standalone date filter menu for column header using context.
 */
export function DataTableColumnDateFilterMenu<TData extends RowData, TValue>(
  props: Omit<
    React.ComponentProps<typeof TableColumnDateFilterMenu>,
    "column"
  > & {
    column?: DataTableColumn<TData, TValue>
  },
) {
  const context = useColumnHeaderContext<TData, TValue>(false)
  const column = props.column || context?.column

  if (!column) {
    console.warn(
      "DataTableColumnDateFilterMenu must be used within DataTableColumnHeaderRoot or provided with a column prop",
    )
    return null
  }

  return <TableColumnDateFilterMenu column={column} {...props} />
}

DataTableColumnDateFilterMenu.displayName = "DataTableColumnDateFilterMenu"
src/components/niko-table/filters/table-column-date-filter.tsx
"use client"

/**
 * niko-table — created by Semir N. (Semkoo, https://github.com/Semkoo) with AI assistance.
 *
 * Before reporting anything: please check the changelog first.
 *  - In-repo: ./CHANGELOG.md
 *  - Docs site: https://niko-table.com/changelog
 *
 * Found a bug or have a fix? Open an issue or PR on GitHub so other
 * users (and future LLMs reading this code) benefit:
 * https://github.com/Semkoo/niko-table-registry
 */
import React from "react"
import type { DateRange } from "react-day-picker"
import { CircleHelp, CalendarIcon, CalendarX2 } from "lucide-react"

import {
  DropdownMenuSeparator,
  DropdownMenuLabel,
} from "@/components/ui/dropdown-menu"
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"
import { Calendar } from "@/components/ui/calendar"
import { Button } from "@/components/ui/button"
import { useDerivedColumnTitle } from "../hooks/use-derived-column-title"
import { formatDate } from "../lib/format"
import { cn } from "@/lib/utils"

import type { DataTableColumn } from "../types"
import type { RowData } from "@tanstack/react-table"
type DateSelection = Date[] | DateRange

function parseAsDate(timestamp: number | string | undefined): Date | undefined {
  if (!timestamp) return undefined
  const numericTimestamp =
    typeof timestamp === "string" ? Number(timestamp) : timestamp
  const date = new Date(numericTimestamp)
  return !Number.isNaN(date.getTime()) ? date : undefined
}

function parseColumnFilterValue(value: unknown) {
  if (value === null || value === undefined) {
    return []
  }

  if (Array.isArray(value)) {
    return value.map(item => {
      if (typeof item === "number" || typeof item === "string") {
        return item
      }
      return undefined
    })
  }

  if (typeof value === "string" || typeof value === "number") {
    return [value]
  }

  return []
}

/**
 * Date filter options for composing inside TableColumnActions.
 * Renders as button that opens a popover with calendar picker - matches FilterDatePicker from table-filter-menu.
 *
 * @example
 * ```tsx
 * // Inside TableColumnActions
 * <TableColumnActions column={column}>
 *   <TableColumnDateFilterOptions
 *     column={column}
 *     multiple
 *   />
 * </TableColumnActions>
 * ```
 */
export function TableColumnDateFilterOptions<TData extends RowData, TValue>({
  column,
  title,
  multiple = true,
  withSeparator = true,
}: {
  column: DataTableColumn<TData, TValue>
  title?: string
  /** Whether to allow date range selection. Defaults to true. */
  multiple?: boolean
  /** Whether to render a separator before the options. Defaults to true. */
  withSeparator?: boolean
}) {
  const [showValueSelector, setShowValueSelector] = React.useState(false)
  const columnFilterValue = column.getFilterValue()

  const selectedDates = React.useMemo<DateSelection>(() => {
    if (!columnFilterValue) {
      return multiple ? { from: undefined, to: undefined } : []
    }

    if (multiple) {
      const timestamps = parseColumnFilterValue(columnFilterValue)
      return {
        from: parseAsDate(timestamps[0]),
        to: parseAsDate(timestamps[1]),
      }
    }

    const timestamps = parseColumnFilterValue(columnFilterValue)
    const date = parseAsDate(timestamps[0])
    return date ? [date] : []
  }, [columnFilterValue, multiple])

  const derivedTitle = useDerivedColumnTitle(column, column.id, title)
  const labelText = multiple ? "Date Range Filter" : "Date Filter"
  const tooltipText = multiple
    ? "Select a date range to filter"
    : "Select a date to filter"

  const dateValue = Array.isArray(selectedDates)
    ? selectedDates.filter(Boolean)
    : [selectedDates.from, selectedDates.to].filter(Boolean)

  const displayValue =
    multiple && dateValue.length === 2
      ? `${formatDate(dateValue[0] as Date)} - ${formatDate(dateValue[1] as Date)}`
      : dateValue[0]
        ? formatDate(dateValue[0] as Date)
        : "Pick a date"

  const onSelect = React.useCallback(
    (date: Date | DateRange | undefined) => {
      if (!date) {
        column.setFilterValue(undefined)
        return
      }

      if (multiple && !("getTime" in date)) {
        const from = date.from?.getTime()
        const to = date.to?.getTime()

        if (from && to) {
          column.setFilterValue([from, to])
        } else if (from) {
          column.setFilterValue([from])
        } else {
          column.setFilterValue(undefined)
        }
      } else if (!multiple && "getTime" in date) {
        column.setFilterValue([date.getTime()])
      }
    },
    [column, multiple],
  )

  const onReset = React.useCallback(() => {
    column.setFilterValue(undefined)
  }, [column])

  return (
    <>
      {withSeparator && <DropdownMenuSeparator />}
      <DropdownMenuLabel className="flex items-center justify-between text-xs font-normal text-muted-foreground">
        <span>{labelText}</span>
        <Tooltip>
          <TooltipTrigger asChild>
            <CircleHelp className="size-3.5 cursor-help" />
          </TooltipTrigger>
          <TooltipContent side="right">
            {tooltipText}
            {derivedTitle && ` - ${derivedTitle}`}
          </TooltipContent>
        </Tooltip>
      </DropdownMenuLabel>
      <div className="px-2 py-2">
        <Popover open={showValueSelector} onOpenChange={setShowValueSelector}>
          <PopoverTrigger asChild>
            <Button
              variant="outline"
              size="sm"
              className={cn(
                "w-full justify-start rounded text-left font-normal",
                !columnFilterValue && "text-muted-foreground",
              )}
            >
              <CalendarIcon />
              <span className="truncate">{displayValue}</span>
            </Button>
          </PopoverTrigger>
          <PopoverContent align="start" className="w-auto p-0">
            {multiple ? (
              <Calendar
                mode="range"
                captionLayout="dropdown"
                selected={selectedDates as DateRange}
                onSelect={onSelect as (date: DateRange | undefined) => void}
              />
            ) : (
              <Calendar
                mode="single"
                captionLayout="dropdown"
                selected={(selectedDates as Date[])[0]}
                onSelect={onSelect as (date: Date | undefined) => void}
              />
            )}
          </PopoverContent>
        </Popover>
        <Button
          variant="outline"
          size="sm"
          onClick={onReset}
          className="mt-2 w-full"
        >
          Clear
        </Button>
      </div>
    </>
  )
}

TableColumnDateFilterOptions.displayName = "TableColumnDateFilterOptions"

/**
 * Standalone date filter menu for column headers.
 * Shows a filter button that opens a popover with a calendar picker.
 *
 * @example
 * ```tsx
 * // Standalone usage
 * <TableColumnDateFilterMenu
 *   column={column}
 *   multiple
 * />
 * ```
 */
export function TableColumnDateFilterMenu<TData extends RowData, TValue>({
  column,
  title,
  className,
  ...props
}: Omit<
  React.ComponentProps<typeof TableColumnDateFilterOptions>,
  "withSeparator" | "column"
> & {
  column: DataTableColumn<TData, TValue>
  className?: string
}) {
  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button
          variant="ghost"
          size="icon"
          className={cn(
            "size-7 transition-opacity dark:text-muted-foreground",
            column.getIsFiltered() && "text-primary",
            className,
          )}
        >
          {column.getIsFiltered() ? (
            <CalendarX2 className="size-3.5" />
          ) : (
            <CalendarIcon className="size-3.5" />
          )}
          <span className="sr-only">Filter by date</span>
        </Button>
      </PopoverTrigger>
      <PopoverContent align="end" className="w-auto p-0">
        <TableColumnDateFilterOptions
          column={column}
          title={title}
          withSeparator={false}
          {...props}
        />
      </PopoverContent>
    </Popover>
  )
}

TableColumnDateFilterMenu.displayName = "TableColumnDateFilterMenu"

More from niko-table

Browse all components in this library →