GitHubX

Filters

PreviousNext

A composable filter system for building dynamic, type-aware filter UIs with dropdown field selection, operators, and multiple value input types

      
      <script setup lang="ts">
import { CalendarDate, today } from "@internationalized/date";
import type { FieldGroup, Filter } from "@/components/filters";
import { FiltersClear, FiltersGroup, FiltersItem, FiltersMenu, FiltersProvider } from "@/components/filters";
import { CalendarIcon, DollarSignIcon, TagIcon, ToggleRightIcon, UserIcon } from "lucide-vue-next";
import { computed, h, type Ref, ref } from "vue";

const fields = ref<FieldGroup[]>([
  {
    group: "Base",
    fields: [
      {
        key: "status",
        name: "Status",
        type: "text",
        icon: TagIcon,
        options: {
          searchable: true,
          items: [
            { label: "Active", value: "active" },
            { label: "Inactive", value: "inactive" },
            { label: "Pending", value: "pending" },
            { label: "Cancelled", value: "cancelled" },
          ],
          optionDisplay: (option) => {
            const color = {
              active: "bg-green-500",
              inactive: "bg-red-500",
              pending: "bg-yellow-500",
              cancelled: "bg-gray-500",
            } as const;
            return h("div", { class: "flex items-center gap-2" }, [h("span", { class: `size-2 rounded-full ${color[option.value as keyof typeof color]}` }), h("span", option.label)]);
          },
        },
        operators: [
          { label: "is", value: "eq", inputType: "select" },
          { label: "is not", value: "neq", inputType: "select" },
          { label: "includes", value: "in", inputType: "multi-select", default: true },
        ],
      },
      {
        key: "name",
        name: "Name",
        type: "text",
        icon: UserIcon,
        operators: [
          { label: "is", value: "eq" },
          { label: "contains", value: "contains" },
          { label: "starts with", value: "starts_with" },
        ],
      },
    ],
  },
  {
    group: "Dates & Numbers",
    fields: [
      {
        key: "created_at",
        name: "Created At",
        type: "date",
        icon: CalendarIcon,
        min: today(),
        max: today().add({ days: 10 }),
        multiple: false,
        operators: [
          { label: "is", value: "eq" },
          { label: "between", value: "btw", inputType: "date-range" },
        ],
      },
      {
        key: "price",
        name: "Price",
        type: "number",
        icon: DollarSignIcon,
        min: 0,
        max: 1000,
        step: 10,
        numberFormat: { style: "currency", currency: "USD", maximumFractionDigits: 0 },
        operators: [
          { label: "less than", value: "lt", inputType: "number", default: true, defaultValue: 500 },
          { label: "between", value: "btw", inputType: "number-range" },
        ],
      },
      {
        key: "is_active",
        name: "Is Active",
        icon: ToggleRightIcon,
        type: "boolean",
        operators: [
          { label: "is", value: "eq", defaultValue: true },
          { label: "is not", value: "neq" },
        ],
      },
    ],
  },
]);

const filters: Ref<Filter[]> = ref([
  { field: "status", operator: "eq", value: "active" },
  { field: "price", operator: "btw", value: [100, 500] },
]);
</script>

<template>
  <div class="min-w-96 space-y-4 flex">
    <FiltersProvider v-model:filters="filters" :fields="fields" variant="outline" class="flex-nowrap items-start">
      <div class="flex items-start gap-2">
        <FiltersMenu />

        <FiltersGroup v-slot="{ removeFilter }">
          <template v-for="(filter, index) in filters" :key="filter.field">
            <FiltersItem
              v-if="!filter.hidden"
              :filter="filter"
              @delete="removeFilter(index)"
            />
          </template>
        </FiltersGroup>
      </div>
      
      <FiltersClear v-if="filters.length > 0" class="flex-none" />
    </FiltersProvider>
  </div>
</template>
    

Features

  • Field-based configuration — Define fields as text, date, number, or boolean with optional icons and options
  • Grouped fields — Organize fields into groups in the add-filter menu
  • Operators per field — Each field supports multiple operators (is, is not, contains, between, etc.) with configurable default
  • Multiple value input types — Text, select, multi-select, date, date range, number, number range, boolean switch, or none
  • Custom option display — Customize how options appear in selects and in the add-filter submenu
  • Variants and sizes — Outline or secondary variant; sm, default, or lg size
  • Clear all — One action to remove all active filters
  • Composable — Slot-based layout; use FiltersMenuTrigger and FiltersMenuContent for custom layouts

Installation

Install from the Vuzeno registry with the shadcn-vue CLI:

      
      bunx --bun shadcn-vue@latest add https://vuzeno.com/r/filters.json
    

Fields configuration

FiltersProvider receives a fields prop: an array of Field or FieldGroup items. Each Field defines a filterable column: its key, label, type, which operators are available, and optionally a fixed set of options or constraints.

Structure

  • Flat list — Pass an array of Field to list all fields in the add-filter menu without groups.
  • Grouped list — Pass an array of FieldGroup. Each group has a group (label) and a fields array. Groups appear as sections in the menu (e.g. “Base”, “Dates”, “Numbers”).

Field properties

PropertyTypeDescription
keystringUnique identifier; used as filter.field when a filter is added.
namestringDisplay name in the menu and on filter chips.
typetext | date | number | booleanDrives the default input type when the operator doesn’t specify one.
operatorsOperator[]List of operators (e.g. “is”, “contains”, “between”). See Operators.
iconComponent | (() => VNode)Icon shown next to the field name in the menu and on chips.
multiplebooleanIf true, allows multiple filters on this field (e.g. several status values).
minTMinimum value (date/number); used by date picker and number inputs.
maxTMaximum value (date/number).
stepnumberStep for number inputs.
numberFormatIntl.NumberFormatOptionsFormatting for number/currency display (e.g. { style: "currency", currency: "USD" }).
optionsobjectPredefined choices for select/multi-select. See Options.

Operators

Each operator is an object with:

PropertyTypeDescription
labelstringLabel in the operator dropdown (e.g. “is”, “between”).
valuestringUnique value stored in filter.operator.
defaultbooleanIf true, this operator is selected when the filter is first added.
defaultValueT | T[]Initial filter.value when this operator is selected.
inputTypestringOverrides the input: select, multi-select, text, date, date-range, number, number-range, boolean, none. Omit to use the field’s type.

Use inputType: "none" for operators that don’t need a value (e.g. “is empty”, “is not empty”).

Options

When a field has predefined choices, set options:

PropertyTypeDescription
items{ label: string, value: T }[]Options for select or multi-select.
searchablebooleanEnable search in the options list.
minSelectionsnumberMinimum selected items (multi-select).
maxSelectionsnumberMaximum selected items (multi-select).
optionDisplay(option) => VNode | stringCustom render for each option (e.g. label + badge).

Fields with options.items show a submenu in the add-filter dropdown so the user can pick a value when adding the filter.

Filter and FilterValue

Active filters are stored as Filter objects: { field, operator, value, hidden? }. The value type (FilterValue) depends on the operator and field: string, string[], number, number[], boolean, CalendarDate, CalendarDate[], or a range { start, end } for date/number ranges.