Customizing Filters
Filter Sphere allows you to extend the default filtering capabilities with your own specific logic. This feature is particularly useful when you need to implement complex or domain-specific filtering that isn’t covered by the built-in filters.
For a complete list of available preset filters, see the Preset Filters Reference.
Defining a Custom Filter
Section titled “Defining a Custom Filter”To add custom filters to Filter Sphere, you can use the defineTypedFn function. This allows you to create type-safe, custom filter functions.
Use defineTypedFn to create a custom filter:
const customFilter = defineTypedFn({ name: "your filter name", define: z.function({ input: [schemaToFilter, optionalUserInputSchema], output: z.boolean(), }), implement: (value, userInput) => { // Your filter logic here return value === userInput; },});- name: A string describing your filter. Ensure it is unique and doesn’t duplicate any built-in filter names.
- By default, the name will be used as the filter label in the UI. You can override this by overriding the
mapFilterNameproperty in theuseFilterSpherehook.
- By default, the name will be used as the filter label in the UI. You can override this by overriding the
- define: A Zod schema defining the function signature.
- First argument: The schema of the data being filtered.
- Second argument (optional): The schema for user input, if required.
- implement: The actual filter logic.
Using Custom Filters
Section titled “Using Custom Filters”Add your custom filters to the filterFnList when using the useFilterSphere hook:
import { useFilterSphere, presetFilter } from "@fn-sphere/filter";
const { predicate, context } = useFilterSphere({ schema: yourSchema, filterFnList: [ customFilter, // Other custom filters... ...presetFilter, // Include preset filters if needed ],});This approach allows you to extend Filter Sphere’s functionality with your own custom filtering logic while maintaining type safety and integration with the existing system.
Here’s an example of using a custom filter function to filter the average of an array of numbers:
| scores | average |
|---|---|
| [1] | 1 |
| [1,10] | 5.5 |
| [1,2,3,4,5] | 3 |
| [6,7,8,9,10] | 8 |
import { z } from "zod";import { Table } from "~/components/table";import { FilterSphereProvider, FilterBuilder, useFilterSphere, presetFilter, defineTypedFn, type FnSchema,} from "@fn-sphere/filter";
const data = [ { scores: [1] }, { scores: [1, 10] }, { scores: [1, 2, 3, 4, 5] }, { scores: [6, 7, 8, 9, 10] },].map((item, index) => ({ ...item, average: item.scores.reduce((acc, curr) => acc + curr) / item.scores.length,}));
const schema = z.object({ scores: z.array(z.number()),});
const dataFilters: FnSchema[] = [ // Define a custom filter function defineTypedFn({ name: "Average score is greater than", define: z.function({ // The first argument is the schema that needs filtering // and the second argument is the user input input: [z.array(z.number()), z.number()], output: z.boolean(), }), implement: (value, input) => { // Implement the filter logic const sum = value.reduce((acc, cur) => acc + cur); return sum / value.length > input; }, }), // Preset filters contains all the built-in filters ...presetFilter,];
export default function AdvancedFilter() { const { predicate, context } = useFilterSphere({ schema, // Pass the custom filters to the filterFnList filterFnList: dataFilters, }); return ( <FilterSphereProvider context={context}> <FilterBuilder /> <Table data={data.filter(predicate)} /> </FilterSphereProvider> );}Define a Generic Filter
Section titled “Define a Generic Filter”Generic filters work across multiple data types, unlike custom filters that are bound to specific schemas. For example, the built-in equals filter works with strings, numbers, booleans, and union types without needing separate implementations.
Use defineGenericFn to create a generic filter:
| name | age | active |
|---|---|---|
| Alice | 25 | true |
| Bob | 30 | false |
| alice | 35 | true |
| ALICE | 40 | false |
import { z } from "zod";import { Table } from "~/components/table";import { FilterSphereProvider, FilterBuilder, useFilterSphere, defineGenericFn, type FnSchema,} from "@fn-sphere/filter";import type { $ZodBoolean, $ZodString, $ZodNumber, $ZodUnion, $ZodLiteral,} from "zod/v4/core";
const data = [ { name: "Alice", age: 25, active: true }, { name: "Bob", age: 30, active: false }, { name: "alice", age: 35, active: true }, { name: "ALICE", age: 40, active: false },];
const schema = z.object({ name: z.string(), age: z.number(), active: z.boolean(),});
const genericFilter = defineGenericFn({ name: "generic equals", // Define which types this filter can work with genericLimit: (genericT): genericT is $ZodBoolean | $ZodString | $ZodNumber => genericT._zod.def.type === "boolean" || genericT._zod.def.type === "string" || genericT._zod.def.type === "number", // Define the function signature based on the type define: (genericT) => z.function({ input: [genericT, genericT], output: z.boolean(), }), // Implement logic that handles all types implement: ( value: boolean | string | number, target: boolean | string | number, ) => { if (typeof value === "string" && typeof target === "string") return value.toLowerCase() === target.toLowerCase(); return value === target; },});
export default function GenericFilterExample() { const { predicate, context } = useFilterSphere({ schema, filterFnList: [genericFilter], }); return ( <FilterSphereProvider context={context}> <FilterBuilder /> <Table data={data.filter(predicate)} /> </FilterSphereProvider> );}Key properties:
- name: A unique string identifying your filter.
- genericLimit: A type guard that determines which Zod types this filter applies to.
- define: A function that receives the matched type and returns a Zod function schema.
- implement: The filter logic that handles all specified types.
Nullable and Optional Fields
Section titled “Nullable and Optional Fields”Preset filters support fields declared with .optional(), .nullable(), or
.nullish(). They keep the filters and input UI for their underlying string,
number, date, boolean, enum, literal-union, or array type, and also offer
isEmpty and isNotEmpty.
| title | score | publishedAt |
|---|---|---|
| Compatibility guide | 92 | 9/1/2026 |
| null | null | |
| Filter presets | 64 |
import { z } from "zod";import { Table } from "~/components/table";import { FilterBuilder, FilterSphereProvider, useFilterSphere,} from "@fn-sphere/filter";
const schema = z.object({ title: z.string().optional(), score: z.number().nullable(), publishedAt: z.date().nullish(),});
const data = [ { title: "Compatibility guide", score: 92, publishedAt: new Date() }, { title: undefined, score: null, publishedAt: null }, { title: "Filter presets", score: 64, publishedAt: undefined },];
export default function NullishFieldsExample() { const { context, predicate } = useFilterSphere({ schema });
return ( <FilterSphereProvider context={context}> <FilterBuilder /> <Table schema={schema} data={data.filter(predicate)} /> </FilterSphereProvider> );}Custom filters remain explicit. A filter whose first input is z.string() is
not offered for a z.string().optional() field. Declare the accepted nullish
values and handle them in the implementation:
const nullishContains = defineTypedFn({ name: "nullish contains", define: z.function({ input: [z.string().nullish(), z.string()], output: z.boolean(), }), implement: (value, target) => value?.includes(target) ?? false,});