21st

ReUI Filters

Component 3

About

ReUI Filters with the first 10 official ReUI examples, preserved from the MIT-licensed source registry.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds ReUI Filters to your project and fits it to your theme.

Code

How ReUI Filters is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

import { CalendarIcon, ClockIcon, SlidersVerticalIcon, ListFilterIcon, FunnelXIcon } from "lucide-react"
"use client"

import { useCallback, useEffect, useMemo, useState } from "react"
import {
  DateSelector,
  formatDateValue,
  type DateSelectorValue,
} from "@/components/ui/reui-filters-utils/date-selector"
import {
  createFilter,
  Filters,
  type Filter,
  type FilterFieldConfig,
} from "@/components/ui/reui-filters"
import {
  endOfMonth,
  endOfYear,
  format,
  isEqual,
  startOfDay,
  startOfMonth,
  startOfYear,
  subDays,
  subMonths,
  subYears,
} from "date-fns"
import { DateRange } from "react-day-picker"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/reui-filters-utils/button"
import { Calendar } from "@/components/ui/reui-filters-utils/calendar"
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/reui-filters-utils/dialog"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/reui-filters-utils/popover"
import { ScrollArea } from "@/components/ui/reui-filters-utils/scroll-area"
import { Slider } from "@/components/ui/reui-filters-utils/slider"

// Type for custom renderer props
type CustomRendererProps = {
  values: unknown[]
  onChange: (values: unknown[]) => void
  autoFocus?: boolean
}

// Modal-based Date Selector Component
function CustomModalDateSelector({
  values,
  onChange,
  autoFocus,
}: CustomRendererProps) {
  const value = values?.[0] as DateSelectorValue | undefined
  const [open, setOpen] = useState(false)
  const [internalValue, setInternalValue] = useState<
    DateSelectorValue | undefined
  >(value)

  const formattedValue = value ? formatDateValue(value) : ""
  const displayText = formattedValue || "Select a date"

  useEffect(() => {
    if (autoFocus) {
      const timer = setTimeout(() => setOpen(true), 400)
      return () => clearTimeout(timer)
    }
  }, [autoFocus])

  useEffect(() => {
    if (open) {
      setInternalValue(value)
    }
  }, [open, value])

  const handleApply = () => {
    onChange([internalValue])
    setOpen(false)
  }

  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger>{displayText}</DialogTrigger>
      <DialogContent className="sm:max-w-lg" showCloseButton={false}>
        <DialogHeader>
          <DialogTitle>Select Date</DialogTitle>
        </DialogHeader>

        <DateSelector
          value={internalValue}
          onChange={setInternalValue}
          showInput={true}
        />

        <DialogFooter>
          <DialogClose render={<Button variant="outline">Cancel</Button>} />
          <Button onClick={handleApply}>Apply</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}

// Custom Date Range Input Component
function CustomDateRangeInput({
  values,
  onChange,
  autoFocus,
}: CustomRendererProps) {
  const [date, setDate] = useState<DateRange | undefined>(
    values?.[0] && typeof values[0] === "string"
      ? {
          from: new Date(values[0] as string),
          to:
            values[1] && typeof values[1] === "string"
              ? new Date(values[1] as string)
              : undefined,
        }
      : undefined
  )
  const [isOpen, setIsOpen] = useState(false)

  useEffect(() => {
    if (autoFocus) {
      const timer = setTimeout(() => setIsOpen(true), 400)
      return () => clearTimeout(timer)
    }
  }, [autoFocus])

  const handleApply = () => {
    if (date?.from) {
      const fromStr = date.from.toISOString().split("T")[0]
      const toStr = date.to ? date.to.toISOString().split("T")[0] : fromStr
      onChange([fromStr, toStr])
    }
    setIsOpen(false)
  }

  const handleCancel = () => {
    setIsOpen(false)
  }

  const handleSelect = (selected: DateRange | undefined) => {
    setDate(selected)
  }

  return (
    <Popover open={isOpen} onOpenChange={setIsOpen}>
      <PopoverTrigger>
        {date?.from ? (
          date.to ? (
            <>
              {format(date.from, "LLL dd, y")} - {format(date.to, "LLL dd, y")}
            </>
          ) : (
            format(date.from, "LLL dd, y")
          )
        ) : (
          <span>Pick a date range</span>
        )}
      </PopoverTrigger>
      <PopoverContent className="w-auto p-0" align="start" sideOffset={8}>
        <Calendar
          autoFocus
          mode="range"
          defaultMonth={date?.from}
          showOutsideDays={false}
          selected={date}
          onSelect={handleSelect}
          numberOfMonths={2}
        />
        <div className="border-border flex items-center justify-end gap-1.5 border-t p-3">
          <Button variant="outline" onClick={handleCancel}>
            Cancel
          </Button>
          <Button onClick={handleApply}>Apply</Button>
        </div>
      </PopoverContent>
    </Popover>
  )
}

// Custom Date Range with Presets Input Component
function CustomDateRangeWithPresetsInput({
  values,
  onChange,
  autoFocus,
}: CustomRendererProps) {
  const today = useMemo(() => new Date(), [])

  const presets = useMemo(
    () => [
      { label: "Today", range: { from: today, to: today } },
      {
        label: "Yesterday",
        range: { from: subDays(today, 1), to: subDays(today, 1) },
      },
      { label: "Last 7 days", range: { from: subDays(today, 6), to: today } },
      { label: "Last 30 days", range: { from: subDays(today, 29), to: today } },
      {
        label: "Month to date",
        range: { from: startOfMonth(today), to: today },
      },
      {
        label: "Last month",
        range: {
          from: startOfMonth(subMonths(today, 1)),
          to: endOfMonth(subMonths(today, 1)),
        },
      },
      { label: "Year to date", range: { from: startOfYear(today), to: today } },
      {
        label: "Last year",
        range: {
          from: startOfYear(subYears(today, 1)),
          to: endOfYear(subYears(today, 1)),
        },
      },
    ],
    [today]
  )

  const [month, setMonth] = useState(today)
  const [date, setDate] = useState<DateRange | undefined>(
    values?.[0] && typeof values[0] === "string"
      ? {
          from: new Date(values[0] as string),
          to:
            values[1] && typeof values[1] === "string"
              ? new Date(values[1] as string)
              : undefined,
        }
      : undefined
  )
  const [selectedPreset, setSelectedPreset] = useState<string | null>(null)
  const [isOpen, setIsOpen] = useState(false)

  useEffect(() => {
    if (autoFocus) {
      const timer = setTimeout(() => setIsOpen(true), 400)
      return () => clearTimeout(timer)
    }
  }, [autoFocus])

  useEffect(() => {
    const matchedPreset = presets.find(
      (preset) =>
        isEqual(
          startOfDay(preset.range.from),
          startOfDay(date?.from || new Date(0))
        ) &&
        isEqual(
          startOfDay(preset.range.to),
          startOfDay(date?.to || new Date(0))
        )
    )
    setSelectedPreset(matchedPreset?.label || null)
  }, [date, presets])

  const handleApply = () => {
    if (date?.from) {
      const fromStr = date.from.toISOString().split("T")[0]
      const toStr = date.to ? date.to.toISOString().split("T")[0] : fromStr
      onChange([fromStr, toStr])
    }
    setIsOpen(false)
  }

  const handleCancel = () => {
    setIsOpen(false)
  }

  const handleSelect = (selected: DateRange | undefined) => {
    setDate({
      from: selected?.from || undefined,
      to: selected?.to || undefined,
    })
    setSelectedPreset(null)
  }

  const handlePresetSelect = (preset: (typeof presets)[0]) => {
    setDate(preset.range)
    setMonth(preset.range.from || today)
    setSelectedPreset(preset.label)
  }

  return (
    <Popover open={isOpen} onOpenChange={setIsOpen}>
      <PopoverTrigger>
        {date?.from ? (
          format(date.from, "LLL dd, y") +
          (date.to ? ` - ${format(date.to, "LLL dd, y")}` : "")
        ) : (
          <span>Pick a date range with presets</span>
        )}
      </PopoverTrigger>
      <PopoverContent className="w-auto p-0" align="center" sideOffset={8}>
        <div className="flex max-sm:flex-col">
          <div className="border-border relative max-sm:order-1 max-sm:border-t sm:w-32">
            <div className="border-border h-full py-2 sm:border-e">
              <div className="flex flex-col gap-[2px] px-2">
                {presets.map((preset, index) => (
                  <Button
                    key={index}
                    type="button"
                    variant="ghost"
                    className={cn(
                      "h-8 w-full justify-start",
                      selectedPreset === preset.label && "bg-accent"
                    )}
                    onClick={() => handlePresetSelect(preset)}
                  >
                    {preset.label}
                  </Button>
                ))}
              </div>
            </div>
          </div>
          <Calendar
            autoFocus
            mode="range"
            month={month}
            onMonthChange={setMonth}
            showOutsideDays={false}
            selected={date}
            onSelect={handleSelect}
            numberOfMonths={2}
          />
        </div>
        <div className="border-border flex items-center justify-end gap-1.5 border-t p-3">
          <Button variant="outline" onClick={handleCancel}>
            Cancel
          </Button>
          <Button onClick={handleApply}>Apply</Button>
        </div>
      </PopoverContent>
    </Popover>
  )
}

// Custom DateTime Input Component
function CustomDateTimeInput({
  values,
  onChange,
  autoFocus,
}: CustomRendererProps) {
  const today = new Date()
  const [date, setDate] = useState<Date | undefined>(
    values?.[0] && typeof values[0] === "string"
      ? new Date(values[0] as string)
      : undefined
  )
  const [time, setTime] = useState<string | undefined>(
    values?.[0] && typeof values[0] === "string"
      ? new Date(values[0] as string).toTimeString().slice(0, 5)
      : "10:00"
  )
  const [isOpen, setIsOpen] = useState(false)

  useEffect(() => {
    if (autoFocus) {
      const timer = setTimeout(() => setIsOpen(true), 400)
      return () => clearTimeout(timer)
    }
  }, [autoFocus])

  const timeSlots = [
    { time: "09:00", available: false },
    { time: "09:30", available: false },
    { time: "10:00", available: true },
    { time: "10:30", available: true },
    { time: "11:00", available: true },
    { time: "11:30", available: true },
    { time: "12:00", available: false },
    { time: "12:30", available: true },
    { time: "13:00", available: true },
    { time: "13:30", available: true },
    { time: "14:00", available: true },
    { time: "14:30", available: false },
    { time: "15:00", available: false },
    { time: "15:30", available: true },
    { time: "16:00", available: true },
    { time: "16:30", available: true },
    { time: "17:00", available: true },
    { time: "17:30", available: true },
    { time: "18:00", available: true },
    { time: "18:30", available: true },
    { time: "19:00", available: true },
    { time: "19:30", available: true },
    { time: "20:00", available: true },
    { time: "20:30", available: true },
    { time: "21:00", available: true },
    { time: "21:30", available: true },
    { time: "22:00", available: true },
    { time: "22:30", available: true },
    { time: "23:00", available: true },
    { time: "23:30", available: true },
  ]

  const handleApply = () => {
    if (date && time) {
      const dateTime = new Date(date)
      const [hours, minutes] = time.split(":").map(Number)
      dateTime.setHours(hours, minutes, 0, 0)
      onChange([dateTime.toISOString()])
    }
    setIsOpen(false)
  }

  const handleCancel = () => {
    setIsOpen(false)
  }

  return (
    <Popover open={isOpen} onOpenChange={setIsOpen}>
      <PopoverTrigger>
        {date ? (
          format(date, "PPP") + (time ? ` - ${time}` : "")
        ) : (
          <span>Pick a date and time</span>
        )}
      </PopoverTrigger>
      <PopoverContent className="w-auto gap-0 p-0 pt-1" align="start">
        <div className="flex max-sm:flex-col">
          <Calendar
            mode="single"
            selected={date}
            onSelect={(newDate: Date | undefined) => {
              if (newDate) {
                setDate(newDate)
                setTime(undefined)
              }
            }}
            className="p-2 sm:pe-5"
            disabled={[{ before: today }]}
          />
          <div className="relative w-full max-sm:h-46 sm:w-40">
            <div className="absolute inset-0 py-4 max-sm:border-t">
              <ScrollArea className="h-full sm:border-s">
                <div className="space-y-3">
                  <div className="flex h-5 shrink-0 items-center px-5">
                    <p className="text-sm font-medium">
                      {date ? format(date, "EEEE, d") : "Pick a date"}
                    </p>
                  </div>
                  <div className="grid gap-1.5 px-5 max-sm:grid-cols-2">
                    {timeSlots.map(({ time: timeSlot, available }) => (
                      <Button
                        key={timeSlot}
                        variant={time === timeSlot ? "default" : "outline"}
                        size="sm"
                        className="w-full"
                        onClick={() => setTime(timeSlot)}
                        disabled={!available}
                      >
                        {timeSlot}
                      </Button>
                    ))}
                  </div>
                </div>
              </ScrollArea>
            </div>
          </div>
        </div>
        <div className="border-border flex items-center justify-end gap-1.5 border-t p-3">
          <Button variant="outline" onClick={handleCancel}>
            Cancel
          </Button>
          <Button onClick={handleApply}>Apply</Button>
        </div>
      </PopoverContent>
    </Popover>
  )
}

// Custom Slider Range Input Component
function CustomSliderRangeInput({
  values,
  onChange,
  autoFocus,
}: CustomRendererProps) {
  const [range, setRange] = useState<number[]>(
    values?.[0] &&
      typeof values[0] === "object" &&
      values[0] !== null &&
      "min" in values[0] &&
      "max" in values[0]
      ? [
          (values[0] as { min: number; max: number }).min,
          (values[0] as { min: number; max: number }).max,
        ]
      : [0, 100]
  )
  const [isOpen, setIsOpen] = useState(false)

  useEffect(() => {
    if (autoFocus) {
      const timer = setTimeout(() => setIsOpen(true), 400)
      return () => clearTimeout(timer)
    }
  }, [autoFocus])

  const handleApply = () => {
    onChange([{ min: range[0], max: range[1] }])
    setIsOpen(false)
  }

  const handleCancel = () => {
    setIsOpen(false)
  }

  return (
    <Popover open={isOpen} onOpenChange={setIsOpen}>
      <PopoverTrigger render={<span />}>
        {range[0]} - {range[1]}
      </PopoverTrigger>
      <PopoverContent
        className="w-auto p-4"
        align="start"
        sideOffset={8}
        alignOffset={-8}
      >
        <div className="space-y-2.5">
          <div className="space-y-4 pt-2.5">
            <Slider
              value={range}
              onValueChange={(value) => setRange(value as number[])}
              max={100}
              min={0}
              step={1}
              className="w-[200px]"
            />
            <div className="text-muted-foreground flex justify-between ps-1.5 text-xs">
              <span>0</span>
              <span>100</span>
            </div>
          </div>
          <div className="flex items-center justify-end gap-1.5">
            <Button variant="ghost" size="sm" onClick={handleCancel}>
              Cancel
            </Button>
            <Button size="sm" variant="outline" onClick={handleApply}>
              Apply
            </Button>
          </div>
        </div>
      </PopoverContent>
    </Popover>
  )
}

export default function Pattern() {
  const [filters, setFilters] = useState<Filter[]>([
    createFilter("customDateRange", "between", []),
  ])
  const [lastAddedValues, setLastAddedValues] = useState<unknown[] | null>(null)

  const fields: FilterFieldConfig[] = [
    {
      key: "modalDateSelector",
      label: "Modal Date Selector",
      icon: (
        <CalendarIcon className="size-3.5" />
      ),
      type: "custom",
      operators: [
        { value: "is", label: "is" },
        { value: "is_not", label: "is not" },
      ],
      customRenderer: ({ values, onChange }) => (
        <CustomModalDateSelector
          values={values}
          onChange={onChange}
          autoFocus={values === lastAddedValues}
        />
      ),
    },
    {
      key: "customDateRange",
      label: "Date Range",
      icon: (
        <CalendarIcon className="size-3.5" />
      ),
      type: "custom",
      operators: [
        { value: "between", label: "between" },
        { value: "not_between", label: "not between" },
      ],
      customRenderer: ({ values, onChange }) => (
        <CustomDateRangeInput
          values={values}
          onChange={onChange}
          autoFocus={values === lastAddedValues}
        />
      ),
    },
    {
      key: "customDateRangePresets",
      label: "Date Range Presets",
      icon: (
        <CalendarIcon className="size-3.5" />
      ),
      type: "custom",
      operators: [
        { value: "between", label: "between" },
        { value: "not_between", label: "not between" },
      ],
      customRenderer: ({ values, onChange }) => (
        <CustomDateRangeWithPresetsInput
          values={values}
          onChange={onChange}
          autoFocus={values === lastAddedValues}
        />
      ),
    },
    {
      key: "customDateTime",
      label: "Date & Time",
      icon: (
        <ClockIcon className="size-3.5" />
      ),
      type: "custom",
      operators: [
        { value: "is", label: "is" },
        { value: "before", label: "before" },
        { value: "after", label: "after" },
      ],
      customRenderer: ({ values, onChange }) => (
        <CustomDateTimeInput
          values={values}
          onChange={onChange}
          autoFocus={values === lastAddedValues}
        />
      ),
    },
    {
      key: "customSliderRange",
      label: "Slider Range",
      icon: (
        <SlidersVerticalIcon className="size-3.5" />
      ),
      type: "custom",
      className: "w-36",
      operators: [
        { value: "between", label: "between" },
        { value: "not_between", label: "not between" },
      ],
      customRenderer: ({ values, onChange }) => (
        <CustomSliderRangeInput
          values={values}
          onChange={onChange}
          autoFocus={values === lastAddedValues}
        />
      ),
    },
  ]

  const handleFiltersChange = useCallback(
    (newFilters: Filter[]) => {
      // Check if a filter was added by comparing IDs
      const added = newFilters.find(
        (nf) => !filters.some((f) => f.id === nf.id)
      )
      if (added) {
        setLastAddedValues(added.values)
      }
      setFilters(newFilters)
    },
    [filters]
  )

  return (
    <div className="flex w-full max-w-[850px] items-start gap-2.5 space-y-6">
      <div className="flex-1">
        <Filters
          filters={filters}
          fields={fields}
          onChange={handleFiltersChange}
          trigger={
            <Button variant="outline" size="icon">
              <ListFilterIcon />
            </Button>
          }
        />
      </div>

      {filters.length > 0 && (
        <Button variant="outline" onClick={() => setFilters([])}>
          <FunnelXIcon />
          Clear
        </Button>
      )}
    </div>
  )
}

Dependencies

More by 21st

View all
  • Futuristic Hero

    Futuristic Hero

    2.7K
  • Hero ASCII one

    Hero ASCII one

    1.4K
  • Hero ASCII

    Hero ASCII

    1.3K
  • Admit One Ticket

    Admit One Ticket

    961
  • Navigation Menu

    Navigation Menu

    916
  • Border Beam

    Border Beam

    890
  • Border Beam Search

    Border Beam Search

    545
  • Thinking Orbs

    Thinking Orbs

    438