

<Preview name="FiltersBasicExample" />

## Overview [#overview]

The `Filters` composite lets teams surface complex filtering logic through a configurable, accessible UI. Define fields once, then let the component handle operator selection, value inputs, grouped layouts, and optional URL synchronisation.

By default, each field can only be added once. Set `allowMultiple` when you want repeated filters for the same field. When filters are active, the add menu also includes a built-in `Clear all` action.

## Usage [#usage]

```tsx
import {
  Filters,
  defineFilters,
  useFiltersState,
  useFiltersChangeHandler,
} from "@tilt-legal/cubitt-components/composites";
```

```tsx
const filterDefs = defineFilters([
  {
    key: "status",
    label: "Status",
    type: "select",
    options: [
      { value: "todo", label: "To Do" },
      { value: "done", label: "Done" },
    ],
  },
  {
    key: "assignee",
    label: "Assignee",
    type: "multiselect",
    options: [
      { value: "john", label: "John" },
      { value: "jane", label: "Jane" },
    ],
  },
] as const);

const [filters, setFilters] = useFiltersState<typeof filterDefs>([]);
const handleFiltersChange = useFiltersChangeHandler<typeof filterDefs>(setFilters);

return <Filters filters={filters} fields={filterDefs.fields} onChange={handleFiltersChange} />;
```

<Accordions type="single">
  <Accordion title="Filter Definitions">
    Build a `filterDefs` array—flat or grouped definitions of every field you want to expose. Use it to declare every filterable field once and unlock type-safe helpers like `createFilter`.

    ```tsx
    const teamMembers = [
      { value: "john", label: "John Doe" },
      { value: "jane", label: "Jane Smith" },
    ] as const;

    const filterDefs = defineFilters([
      {
        group: "Details",
        fields: [
          {
            key: "status",
            label: "Status",
            type: "select",
            options: [
              { value: "todo", label: "To Do" },
              { value: "done", label: "Done" },
            ],
          },
          {
            key: "assignee",
            label: "Assignee",
            type: "multiselect",
            maxSelections: 3,
            options: teamMembers,
          },
        ],
      },
      {
        key: "createdAt",
        label: "Created",
        type: "date",
      },
    ] as const);
    ```

    The returned object contains:

    * `fields` – the normalized array passed to the `Filters` component.
    * `filterKeys` – list of valid `field` identifiers.
    * `createFilter(field, operator, values, options?)` – a typed factory that builds filters matching your definition (used in programmatic presets).
      Refer to the API Reference below for the full list of field properties.
  </Accordion>

  <Accordion title="URL State Management">
    Persist filters in the query string by providing `paramName`:

    ```tsx
    <Filters
      filters={filters}
      fields={filterDefs.fields}
      onChange={handleFiltersChange}
      paramName="filters"
      paramHistory="replace"
      paramDebounce={200}
    />
    ```

    <Callout type="info">
      Want URL-backed filters with shareable links and reload persistence? Continue below for the
      dedicated URL sync example.
    </Callout>
  </Accordion>
</Accordions>

## Examples [#examples]

### Custom Trigger [#custom-trigger]

<Tabs items="[&#x22;Preview&#x22;, &#x22;Code&#x22;]">
  <Tab value="Preview">
    <Preview name="FiltersCustomAddButtonExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    "use client";

    import {
      Avatar,
      AvatarFallback,
      AvatarImage,
      Button,
    } from "@tilt-legal/cubitt-components/primitives";
    import {
      defineFilters,
      Filters,
      useFiltersChangeHandler,
      useFiltersState,
    } from "@tilt-legal/cubitt-components/composites";
    import {
      Calendar,
      Check,
      Envelope,
      Filter2Plus,
      Globe,
      Star,
      Tag,
      User,
      Xmark,
    } from "@tilt-legal/cubitt-icons/ui/outline";

    const PriorityIcon = ({ priority }: { priority: string }) => {
      const colors = {
        low: "text-green-500",
        medium: "text-yellow-500",
        high: "text-orange-500",
        urgent: "text-red-500",
      };
      return <Star className={colors[priority as keyof typeof colors]} />;
    };

    export function FiltersCustomAddButtonExample() {
      const filterDefs = defineFilters([
        {
          key: "text",
          label: "Text",
          icon: <Tag className="size-3.5" />,
          type: "text",
          className: "w-36",
          placeholder: "Search text...",
        },
        {
          key: "email",
          label: "Email",
          icon: <Envelope className="size-3.5" />,
          type: "email",
          className: "w-48",
          placeholder: "user@example.com",
        },
        {
          key: "website",
          label: "Website",
          icon: <Globe className="size-3.5" />,
          type: "url",
          className: "w-40",
          placeholder: "https://example.com",
        },
        {
          key: "assignee",
          label: "Assignee",
          icon: <User className="size-3.5" />,
          type: "multiselect",
          className: "w-[200px]",
          options: [
            {
              value: "john",
              label: "John Doe",
              icon: (
                <Avatar className="size-5">
                  <AvatarImage alt="John Doe" src="https://randomuser.me/api/portraits/men/1.jpg" />
                  <AvatarFallback>JD</AvatarFallback>
                </Avatar>
              ),
            },
            {
              value: "jane",
              label: "Jane Smith",
              icon: (
                <Avatar className="size-5">
                  <AvatarImage alt="Jane Smith" src="https://randomuser.me/api/portraits/women/2.jpg" />
                  <AvatarFallback>JS</AvatarFallback>
                </Avatar>
              ),
            },
            {
              value: "bob",
              label: "Bob Johnson",
              icon: (
                <Avatar className="size-5">
                  <AvatarImage alt="Bob Johnson" src="https://randomuser.me/api/portraits/men/3.jpg" />
                  <AvatarFallback>BJ</AvatarFallback>
                </Avatar>
              ),
            },
            {
              value: "alice",
              label: "Alice Brown",
              icon: (
                <Avatar className="size-5">
                  <AvatarImage
                    alt="Alice Brown"
                    src="https://randomuser.me/api/portraits/women/4.jpg"
                  />
                  <AvatarFallback>AB</AvatarFallback>
                </Avatar>
              ),
            },
            {
              value: "nick",
              label: "Nick Bold",
              icon: (
                <Avatar className="size-5">
                  <AvatarImage alt="Nick Bold" src="https://randomuser.me/api/portraits/men/4.jpg" />
                  <AvatarFallback>NB</AvatarFallback>
                </Avatar>
              ),
            },
          ],
        },
        {
          key: "priority",
          label: "Priority",
          icon: <Star className="size-3.5" />,
          type: "multiselect",
          className: "w-[180px]",
          options: [
            { value: "low", label: "Low", icon: <PriorityIcon priority="low" /> },
            {
              value: "medium",
              label: "Medium",
              icon: <PriorityIcon priority="medium" />,
            },
            {
              value: "high",
              label: "High",
              icon: <PriorityIcon priority="high" />,
            },
            {
              value: "urgent",
              label: "Urgent",
              icon: <PriorityIcon priority="urgent" />,
            },
          ],
        },
        {
          key: "dueDate",
          label: "Due Date",
          icon: <Calendar className="size-3.5" />,
          type: "date",
          className: "w-36",
        },
        {
          key: "score",
          label: "Score",
          icon: <Star className="size-3.5" />,
          type: "number",
          min: 0,
          max: 100,
          step: 1,
        },
        {
          key: "isActive",
          label: "Active Status",
          icon: <Check className="size-3.5" />,
          type: "boolean",
        },
      ] as const);

      const [filters, setFilters] = useFiltersState<typeof filterDefs>([
        filterDefs.createFilter("assignee", "is_any_of", ["john", "nick", "alice"], {
          id: "filters-custom-add-button-assignee",
        }),
      ]);

      const handleFiltersChange = useFiltersChangeHandler<typeof filterDefs>(setFilters);

      return (
        <div className="space-y-4">
          <Filters
            className="justify-center"
            fields={filterDefs.fields}
            filters={filters}
            onChange={handleFiltersChange}
            trigger={
              <Button mode="icon">
                <Filter2Plus />
              </Button>
            }
          />

          {filters.length > 0 && (
            <Button onClick={() => setFilters([])} variant="secondary">
              <Xmark /> Clear
            </Button>
          )}
        </div>
      );
    }
    ```
  </Tab>
</Tabs>

### Validation [#validation]

Try a non-`@tilt.legal` address, then blur the field to see the custom
validation message.

<Tabs items="[&#x22;Preview&#x22;, &#x22;Code&#x22;]">
  <Tab value="Preview">
    <Preview name="FiltersValidationExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    const filterDefs = defineFilters([
      {
        key: "email",
        label: "Email",
        type: "email",
        validation: (value) => {
          const email = String(value).trim().toLowerCase();

          if (!email.endsWith("@tilt.legal")) {
            // Returning an object overrides the default fallback validation message.
            return {
              valid: false,
              message: "Use a @tilt.legal address for this filter.",
            };
          }

          return true;
        },
      },
    ] as const);
    ```
  </Tab>
</Tabs>

### Data Table Integration [#data-table-integration]

Use `Filters` to power the rows rendered by `DataTable`.

<Tabs
  items="[&#x22;Preview&#x22;,
&#x22;Code&#x22;]"
>
  <Tab value="Preview">
    <Preview name="FiltersDataTableIntegrationExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    "use client";

    import { Badge } from "@tilt-legal/cubitt-components/primitives";
    import {
      DataTable,
      defineFilters,
      Filters,
      useFiltersTanstack,
    } from "@tilt-legal/cubitt-components/composites";
    import type { ColumnDef } from "@tanstack/react-table";

    const employees = [
      {
        id: "1",
        name: "Alice Johnson",
        email: "alice@example.com",
        status: "Active",
        department: "Engineering",
        location: "New York",
      },
      {
        id: "2",
        name: "Michael Chen",
        email: "michael@example.com",
        status: "Active",
        department: "Design",
        location: "San Francisco",
      },
      {
        id: "3",
        name: "Priya Nair",
        email: "priya@example.com",
        status: "On Leave",
        department: "Marketing",
        location: "Toronto",
      },
      {
        id: "4",
        name: "Daniel Martinez",
        email: "daniel@example.com",
        status: "Inactive",
        department: "Sales",
        location: "Mexico City",
      },
      {
        id: "5",
        name: "Fatima Al-Sayed",
        email: "fatima@example.com",
        status: "Active",
        department: "Product",
        location: "Dubai",
      },
    ];

    type Employee = (typeof employees)[number];

    const filterDefs = defineFilters([
      {
        key: "search",
        type: "text",
        label: "Search",
        placeholder: "Search name, email, or department",
        operators: [{ value: "contains", label: "Contains" }],
      },
      {
        key: "status",
        type: "select",
        label: "Status",
        operators: [
          { value: "is", label: "Is" },
          { value: "is_not", label: "Is not" },
        ],
      },
      {
        key: "department",
        type: "select",
        label: "Department",
        operators: [{ value: "is", label: "Is" }],
      },
    ]);

    const baseColumns: ColumnDef<Employee>[] = [
      {
        accessorKey: "name",
        header: "Name",
        cell: ({ row }) => (
          <div className="font-medium text-fg-1">{row.getValue("name") as string}</div>
        ),
      },
      {
        accessorKey: "email",
        header: "Email",
      },
      {
        accessorKey: "status",
        header: "Status",
        cell: ({ row }) => {
          const status = row.getValue("status") as string;
          const variant =
            status === "Active" ? "success" : status === "On Leave" ? "secondary" : "destructive";
          return <Badge variant={variant}>{status}</Badge>;
        },
      },
      {
        accessorKey: "department",
        header: "Department",
      },
      {
        accessorKey: "location",
        header: "Location",
      },
    ];

    export function FiltersDataTableIntegrationExample() {
      const {
        filters,
        handleFiltersChange,
        filterFields,
        columns,
        columnFilters,
        onColumnFiltersChange,
      } = useFiltersTanstack<Employee, typeof filterDefs>({
        definition: filterDefs,
        columns: baseColumns,
        data: employees,
        fieldColumnMap: { search: "name" },
        searchResolvers: {
          search: (row) => [row.name, row.email, row.department],
        },
      });

      return (
        <div className="space-y-4">
          <Filters fields={filterFields} filters={filters} onChange={handleFiltersChange} />
          <DataTable
            columnFilters={columnFilters}
            columns={columns}
            data={employees}
            enableFiltering
            enableSorting
            onColumnFiltersChange={onColumnFiltersChange}
          />
        </div>
      );
    }
    ```
  </Tab>
</Tabs>

### Size Variants [#size-variants]

<Tabs items="[&#x22;Preview&#x22;, &#x22;Code&#x22;]">
  <Tab value="Preview">
    <Preview name="FiltersSizeExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    "use client";

    import { Button } from "@tilt-legal/cubitt-components/primitives";
    import {
      defineFilters,
      Filters,
      useFiltersChangeHandler,
      useFiltersState,
    } from "@tilt-legal/cubitt-components/composites";
    import {
      Ban,
      Calendar,
      Check,
      Clock,
      Envelope,
      Globe,
      Plus,
      Star,
      Tag,
    } from "@tilt-legal/cubitt-icons/ui/outline";

    const PriorityIcon = ({ priority }: { priority: string }) => {
      const colors = {
        low: "text-green-500",
        medium: "text-yellow-500",
        high: "text-orange-500",
        urgent: "text-red-500",
      };
      return <Star className={colors[priority as keyof typeof colors]} />;
    };

    export function FiltersSizeExample() {
      const filterDefs = defineFilters([
        {
          key: "text",
          label: "Text",
          icon: <Tag className="size-3.5" />,
          type: "text",
          className: "w-36",
          placeholder: "Search text...",
        },
        {
          key: "email",
          label: "Email",
          icon: <Envelope className="size-3.5" />,
          type: "email",
          className: "w-48",
          placeholder: "user@example.com",
        },
        {
          key: "website",
          label: "Website",
          icon: <Globe className="size-3.5" />,
          type: "url",
          className: "w-40",
          placeholder: "https://example.com",
        },
        {
          key: "status",
          label: "Status",
          icon: <Clock className="size-3.5" />,
          type: "select",
          searchable: false,
          className: "w-[200px]",
          options: [
            {
              value: "todo",
              label: "To Do",
              icon: <Clock className="size-3 text-brand-1" />,
            },
            {
              value: "in-progress",
              label: "In Progress",
              icon: <Clock className="size-3 text-yellow-500" />,
            },
            {
              value: "done",
              label: "Done",
              icon: <Check className="size-3 text-green-500" />,
            },
            {
              value: "cancelled",
              label: "Cancelled",
              icon: <Ban className="size-3 text-destructive" />,
            },
          ],
        },
        {
          key: "priority",
          label: "Priority",
          icon: <Star className="size-3.5" />,
          type: "multiselect",
          className: "w-[180px]",
          options: [
            { value: "low", label: "Low", icon: <PriorityIcon priority="low" /> },
            {
              value: "medium",
              label: "Medium",
              icon: <PriorityIcon priority="medium" />,
            },
            {
              value: "high",
              label: "High",
              icon: <PriorityIcon priority="high" />,
            },
            {
              value: "urgent",
              label: "Urgent",
              icon: <PriorityIcon priority="urgent" />,
            },
          ],
        },
        {
          key: "dueDate",
          label: "Due Date",
          icon: <Calendar className="size-3.5" />,
          type: "date",
          className: "w-36",
        },
        {
          key: "score",
          label: "Score",
          icon: <Star className="size-3.5" />,
          type: "number",
          min: 0,
          max: 100,
          step: 1,
        },
        {
          key: "isActive",
          label: "Active Status",
          icon: <Check className="size-3.5" />,
          type: "boolean",
        },
      ] as const);

      const [smallFilters, setSmallFilters] = useFiltersState<typeof filterDefs>([
        filterDefs.createFilter("priority", "is_any_of", ["high", "urgent"]),
      ]);

      const [mediumFilters, setMediumFilters] = useFiltersState<typeof filterDefs>([
        filterDefs.createFilter("dueDate", "is", ["2025-01-01"]),
      ]);

      const [largeFilters, setLargeFilters] = useFiltersState<typeof filterDefs>([
        filterDefs.createFilter("email", "contains", ["example@example.com"]),
      ]);

      const handleSmallFiltersChange = useFiltersChangeHandler<typeof filterDefs>(setSmallFilters);
      const handleMediumFiltersChange = useFiltersChangeHandler<typeof filterDefs>(setMediumFilters);
      const handleLargeFiltersChange = useFiltersChangeHandler<typeof filterDefs>(setLargeFilters);

      return (
        <div className="flex w-full flex-col items-center justify-center gap-6">
          <Filters
            fields={filterDefs.fields}
            filters={smallFilters}
            onChange={handleSmallFiltersChange}
            size="sm"
            trigger={
              <Button mode="icon" size="sm" variant="secondary">
                <Plus />
              </Button>
            }
          />

          <Filters
            className="justify-center"
            fields={filterDefs.fields}
            filters={mediumFilters}
            onChange={handleMediumFiltersChange}
            size="md"
            trigger={
              <Button mode="icon" variant="secondary">
                <Plus />
              </Button>
            }
          />

          <Filters
            fields={filterDefs.fields}
            filters={largeFilters}
            onChange={handleLargeFiltersChange}
            size="lg"
            trigger={
              <Button mode="icon" size="lg" variant="secondary">
                <Plus />
              </Button>
            }
          />
        </div>
      );
    }
    ```
  </Tab>
</Tabs>

### Programmatic Presets [#programmatic-presets]

<Tabs items="[&#x22;Preview&#x22;, &#x22;Code&#x22;]">
  <Tab value="Preview">
    <Preview name="FiltersLocalPresetExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    "use client";

    import { Button } from "@tilt-legal/cubitt-components/primitives";
    import {
      defineFilters,
      Filters,
      useFiltersChangeHandler,
      useFiltersState,
    } from "@tilt-legal/cubitt-components/composites";
    import { Bell, Sliders } from "@tilt-legal/cubitt-icons/ui/outline";

    const filterDefs = defineFilters([
      {
        key: "status",
        label: "Status",
        type: "select",
        icon: <Bell />,
        options: [
          { value: "todo", label: "To Do" },
          { value: "in-progress", label: "In Progress" },
          { value: "done", label: "Done" },
        ],
      },
      {
        key: "priority",
        label: "Priority",
        type: "multiselect",
        icon: <Sliders />,
        options: [
          { value: "low", label: "Low" },
          { value: "medium", label: "Medium" },
          { value: "high", label: "High" },
        ],
      },
    ] as const);

    export function FiltersLocalPresetExample() {
      const [filters, setFilters] = useFiltersState<typeof filterDefs>([]);
      const handleFiltersChange = useFiltersChangeHandler<typeof filterDefs>(setFilters);

      const applyPreset = () => {
        handleFiltersChange([
          filterDefs.createFilter("status", "is", ["done"], {
            id: "local-preset-status-done",
          }),
          filterDefs.createFilter("priority", "is_any_of", ["high"], {
            id: "local-preset-priority-high",
          }),
        ]);
      };

      return (
        <div className="space-y-4">
          <div className="flex gap-1">
            {filters.length === 0 && (
              <Button onClick={applyPreset} variant="secondary">
                Apply filter set
              </Button>
            )}
            {filters.length > 0 && (
              <Button onClick={() => setFilters([])} variant="secondary">
                Clear
              </Button>
            )}
          </div>
          <Filters
            className="justify-center"
            fields={filterDefs.fields}
            filters={filters}
            onChange={handleFiltersChange}
          />
        </div>
      );
    }
    ```
  </Tab>
</Tabs>

### URL Sync [#url-sync]

<Tabs items="[&#x22;Preview&#x22;, &#x22;Code&#x22;]">
  <Tab value="Preview">
    <Preview name="FiltersUrlStateExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    "use client";

    import { Button } from "@tilt-legal/cubitt-components/primitives";
    import {
      defineFilters,
      Filters,
      useFiltersChangeHandler,
      useFiltersState,
    } from "@tilt-legal/cubitt-components/composites";
    import {
      Ban,
      Bell,
      Calendar,
      Check,
      Clock,
      Envelope,
      Globe,
      Phone,
      Sliders,
      Star,
      User,
      Xmark,
    } from "@tilt-legal/cubitt-icons/ui/outline";

    const PriorityIcon = ({ priority }: { priority: string }) => {
      const colors = {
        low: "bg-green-500",
        medium: "bg-yellow-500",
        high: "bg-violet-500",
        urgent: "bg-orange-500",
        critical: "bg-red-500",
      };

      return (
        <div className={`size-2.25 shrink-0 rounded-full ${colors[priority as keyof typeof colors]}`} />
      );
    };

    export function FiltersUrlStateExample() {
      const filterDefs = defineFilters([
        {
          group: "Basic",
          fields: [
            {
              key: "text",
              label: "Text",
              type: "text",
              icon: <Envelope />,
              placeholder: "Search text...",
            },
            {
              key: "email",
              label: "Email",
              type: "email",
              icon: <Envelope />,
              placeholder: "user@example.com",
            },
            {
              key: "website",
              label: "Website",
              icon: <Globe />,
              type: "url",
              className: "w-40",
              placeholder: "https://example.com",
            },
            {
              key: "phone",
              label: "Phone",
              icon: <Phone />,
              type: "tel",
              className: "w-40",
              placeholder: "+1 (123) 456-7890",
            },
            {
              key: "isActive",
              label: "Is active ?",
              icon: <Check />,
              type: "boolean",
            },
          ],
        },
        {
          group: "Select",
          fields: [
            {
              key: "status",
              label: "Status",
              icon: <Bell />,
              type: "select",
              searchable: false,
              className: "w-[200px]",
              options: [
                {
                  value: "todo",
                  label: "To Do",
                  icon: <Clock className="size-3 text-brand-1" />,
                },
                {
                  value: "in-progress",
                  label: "In Progress",
                  icon: <Clock className="size-3 text-yellow-500" />,
                },
                {
                  value: "done",
                  label: "Done",
                  icon: <Check className="size-3 text-green-500" />,
                },
                {
                  value: "cancelled",
                  label: "Cancelled",
                  icon: <Ban className="size-3 text-destructive" />,
                },
              ],
            },
            {
              key: "priority",
              label: "Priority",
              icon: <Sliders />,
              type: "multiselect",
              className: "w-[180px]",
              options: [
                {
                  value: "low",
                  label: "Low",
                  icon: <PriorityIcon priority="low" />,
                },
                {
                  value: "medium",
                  label: "Medium",
                  icon: <PriorityIcon priority="medium" />,
                },
                {
                  value: "high",
                  label: "High",
                  icon: <PriorityIcon priority="high" />,
                },
                {
                  value: "urgent",
                  label: "Urgent",
                  icon: <PriorityIcon priority="urgent" />,
                },
                {
                  value: "critical",
                  label: "Critical",
                  icon: <PriorityIcon priority="critical" />,
                },
              ],
            },
            {
              key: "assignee",
              label: "Assignee",
              icon: <User />,
              type: "multiselect",
              maxSelections: 5,
              options: [
                {
                  value: "john",
                  label: "John Doe",
                  icon: <div className="size-3 rounded-full bg-blue-400" />,
                },
                {
                  value: "jane",
                  label: "Jane Smith",
                  icon: <div className="size-3 rounded-full bg-green-400" />,
                },
                {
                  value: "bob",
                  label: "Bob Johnson",
                  icon: <div className="size-3 rounded-full bg-purple-400" />,
                },
                {
                  value: "unassigned",
                  label: "Unassigned",
                  icon: <User className="size-3 text-gray-400" />,
                },
              ],
            },
            {
              key: "userType",
              label: "User Type",
              icon: <User />,
              type: "select",
              searchable: false,
              className: "w-[200px]",
              options: [
                {
                  value: "premium",
                  label: "Premium",
                  icon: <Star className="size-3 text-yellow-500" />,
                },
                {
                  value: "standard",
                  label: "Standard",
                  icon: <User className="size-3 text-blue-500" />,
                },
                {
                  value: "trial",
                  label: "Trial",
                  icon: <Clock className="size-3 text-gray-500" />,
                },
              ],
            },
          ],
        },
        {
          group: "Date & Time",
          fields: [
            {
              key: "dueDate",
              label: "Due Date",
              icon: <Calendar />,
              type: "date",
              className: "w-36",
            },
            {
              key: "orderDate",
              label: "Order Date",
              icon: <Calendar />,
              type: "select",
              searchable: false,
              className: "w-[200px]",
              options: [
                { value: "past", label: "in the past" },
                { value: "24h", label: "24 hours from now" },
                { value: "3d", label: "3 days from now" },
                { value: "1w", label: "1 week from now" },
                { value: "1m", label: "1 month from now" },
                { value: "3m", label: "3 months from now" },
              ],
            },
            {
              key: "dateRange",
              label: "Date Range",
              icon: <Calendar />,
              type: "daterange",
            },
            {
              key: "createdAt",
              label: "Created At",
              icon: <Clock />,
              type: "datetime",
            },
            {
              key: "workingHours",
              label: "Working Hours",
              icon: <Clock />,
              type: "time",
            },
          ],
        },
        {
          group: "Numbers",
          fields: [
            {
              key: "score",
              label: "Score",
              icon: <Star />,
              type: "number",
              min: 0,
              max: 100,
              step: 1,
            },
            {
              key: "salary",
              label: "Salary",
              icon: <Star />,
              type: "number",
              prefix: "$",
              className: "w-24",
              min: 0,
              max: 500_000,
              step: 1000,
            },
            {
              key: "completion",
              label: "Completion",
              icon: <Check />,
              className: "w-24",
              suffix: "%",
              type: "number",
              step: 5,
            },
          ],
        },
      ] as const);

      const [filters, setFilters] = useFiltersState<typeof filterDefs>([]);
      const handleFiltersChange = useFiltersChangeHandler<typeof filterDefs>(setFilters);

      return (
        <div className="space-y-4">
          <Filters
            className="justify-center"
            fields={filterDefs.fields}
            filters={filters}
            onChange={handleFiltersChange}
            paramHistory="replace"
            paramName="url-state-demo"
          />
          {filters.length > 0 && (
            <Button onClick={() => setFilters([])} variant="secondary">
              <Xmark /> Clear
            </Button>
          )}
        </div>
      );
    }
    ```
  </Tab>
</Tabs>

## Set Presets (Server-Side) [#set-presets-server-side]

When you need to seed filters outside the UI—redirects, emails, or server-rendered pages—use `createFiltersSerializer` to produce a validated query string that the Filters component can hydrate from.

If your app configures a custom TanStack Router `parseSearch` / `stringifySearch`, pass those same functions into `createFiltersSerializer` so preset URLs match runtime routing behavior.

```ts
import { createFiltersSerializer } from "@tilt-legal/cubitt-components";
import {
  DEFAULT_I18N,
  createFilterArrayValidator,
  defineFilters,
} from "@tilt-legal/cubitt-components/composites";

const definition = defineFilters([
  {
    key: "status",
    label: "Status",
    type: "select",
    options: [
      { value: "todo", label: "To Do" },
      { value: "done", label: "Done" },
    ],
  },
] as const);

const validator = createFilterArrayValidator(definition.fields, DEFAULT_I18N);
const serializeFilters = createFiltersSerializer(validator);

const presetUrl = serializeFilters(
  [
    definition.createFilter("status", "is", ["done"], {
      id: "preset-status-done",
    }),
  ],
  "https://app.tilt.dev/tasks",
);
// -> https://app.tilt.dev/tasks?filters=[...]
```

## API Reference [#api-reference]

### Filters [#filters]

| Prop              | Type                                     | Default        | Description                                                     |
| ----------------- | ---------------------------------------- | -------------- | --------------------------------------------------------------- |
| `filters`         | `Filter[]`                               | `[]`           | Controlled list rendered by the component.                      |
| `fields`          | `FilterFieldsConfig`                     | -              | Definition produced by `defineFilters`.                         |
| `onChange`        | `(filters: Filter[]) => void`            | -              | Called whenever a filter is added, edited, or removed.          |
| `className`       | `string`                                 | -              | Additional class names applied to the Filters wrapper.          |
| `size`            | `"sm" \| "md" \| "lg"`                   | `"md"`         | Chip sizing.                                                    |
| `i18n`            | `Partial<FilterI18nConfig>`              | `DEFAULT_I18N` | Merge custom strings with the default copy.                     |
| `showSearchInput` | `boolean`                                | `true`         | Display the field search input in the add menu.                 |
| `trigger`         | `React.ReactNode`                        | -              | Custom trigger element shown instead of the default add button. |
| `allowMultiple`   | `boolean`                                | `false`        | Allow multiple filters with the same field key.                 |
| `debug`           | `boolean \| (filters: Filter[]) => void` | `false`        | Enable logging of filter changes or provide a debug hook.       |

#### URL State Props [#url-state-props]

When `paramName` is provided the Filters component synchronises its value to TanStack Router search params.

| Prop                  | Type                                           | Default  | Description                                                                |
| --------------------- | ---------------------------------------------- | -------- | -------------------------------------------------------------------------- |
| `paramName`           | `string`                                       | —        | Querystring key used to persist filter state. Enables URL synchronisation. |
| `paramHistory`        | `"push" \| "replace"`                          | `"push"` | Browser history behaviour applied to URL updates.                          |
| `paramDebounce`       | `number`                                       | `150`    | Debounce duration (ms) for URL updates (set `0` to disable).               |
| `paramClearOnDefault` | `boolean`                                      | `true`   | Remove the URL parameter when filters match the default state.             |
| `paramSerializer`     | `(filters: Filter[]) => Promise<void> \| void` | —        | Optional override invoked before writing filter state to the URL.          |

### Filter Definitions [#filter-definitions]

| Property                  | Type                                                                                                       | Description                                                                                             |
| ------------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `key`                     | `string`                                                                                                   | Unique identifier for the filterable field.                                                             |
| `label`                   | `string`                                                                                                   | Display name shown in the UI.                                                                           |
| `type`                    | `'text' \| 'select' \| 'multiselect' \| 'number' \| 'date' \| 'daterange' \| 'boolean' \| 'custom' \| ...` | Determines default operators and value editors.                                                         |
| `icon`                    | `React.ReactNode`                                                                                          | Optional icon rendered with the field label.                                                            |
| `options`                 | `FilterOption[]`                                                                                           | Required for select-style fields; each option may include `value`, `label`, `icon`, and `metadata`.     |
| `operators`               | `FilterOperator[]`                                                                                         | Override the default operator list for the field.                                                       |
| `defaultOperator`         | `string`                                                                                                   | Force a specific operator when the filter is first created.                                             |
| `placeholder`             | `string`                                                                                                   | Placeholder text for value inputs.                                                                      |
| `searchable`              | `boolean`                                                                                                  | Enables search within select / multiselect menus.                                                       |
| `maxSelections`           | `number`                                                                                                   | Limit multiselect selections.                                                                           |
| `className`               | `string`                                                                                                   | Field-level class names applied to the rendered value control.                                          |
| `min` / `max` / `step`    | `number`                                                                                                   | Numeric input bounds and stepping for number fields.                                                    |
| `prefix` / `suffix`       | `React.ReactNode \| string`                                                                                | Inline adornments rendered inside text and number inputs.                                               |
| `group` / `fields`        | `FilterFieldGroup`                                                                                         | Group fields under a named section.                                                                     |
| `customRenderer`          | `(props: CustomRendererProps) => React.ReactNode`                                                          | Provide a bespoke value entry UI.                                                                       |
| `customValueRenderer`     | `(values, options) => React.ReactNode`                                                                     | Custom rendering for chip values.                                                                       |
| `value` / `onValueChange` | `unknown[]` / `(values: unknown[]) => void`                                                                | Opt into controlled values for a specific field.                                                        |
| `validation`              | `(value: unknown) => boolean \| { valid: boolean; message?: string }`                                      | Additional validation hook for user-entered values. Return an object to provide a custom error message. |

### Helpers [#helpers]

* `defineFilters` – create strongly typed filter definitions.
* `createFilter` – build filters manually.
* `useFiltersState` – convenience hook for local state.
* `useFiltersChangeHandler` – adapter that forwards `onChange` results to `setState`.
* `createFilterArrayValidator` – validator compatible with Filters URL-state parsing and serializer helpers.
* `createFiltersSerializer` – pure helper for generating validated preset URLs. Pass custom router `parseSearch` / `stringifySearch` functions if your app overrides TanStack Router's defaults.
