/ component

Filter Builder

Edit the CSS filter / backdrop-filter property β€” a space-separated list of filter functions β€” with compile-time FUNCTION-LIST DISPATCH. Each function's name is looked up in a signature table that validates its argument count and every argument's dimension. blur(45deg) β†’ never (wants length); hue-rotate(10px) β†’ never (wants angle); drop-shadow's trailing color is validated against the color-picker ColorLiteral. The strict tier resolves any violation to never.

/ basic-usage

Build a filter list

Controlled value + onChange. Add functions, edit each argument with the right units, and the editor emits a valid space-separated filter string.

blur(2px) brightness(1.1) saturate(1.3)
/ live-preview

See the stack on a real surface

The scrubbers compose blur/brightness/contrast/saturate/hue-rotate into the typed string and write it back. Flip the toggle to apply the same value as backdrop-filter on a translucent panel over a busy background.

preview
saturate(1.4) contrast(1.1) drop-shadow(6px 6px 10px rgb(0 0 0 / 0.4))
preview
saturate(1.4) contrast(1.1) drop-shadow(6px 6px 10px rgb(0 0 0 / 0.4))
/ types

Three usage tiers

From useState-and-go to per-function dimension-typed dispatch.

01 casual
string

Pass any string

useState<string>. No compile-time validation; the runtime parser handles whatever you type β€” including calc() and var() arguments.

blur(4px) brightness(1.2)
const [value, setValue] = useState<string>("blur(4px) brightness(1.2)")
02 intellisense
FilterString

Literal-shaped hints

State typed as FilterString β€” the union of every blur()/brightness()/drop-shadow()/… head plus none. It is also the onChange return type.

saturate(1.5)
const [value, setValue] = useState<FilterString>("saturate(1.5)")
03 strict
FilterLiteral<S>

Per-function dimension typing at compile time

cssFilter() dispatches on each function name and resolves a wrong dimension, arg count, or bad drop-shadow color to never β€” a type error before you run the code.

blur(4px) brightness(1.2) drop-shadow(2px 2px 4px #000)
cssFilter("blur(4px) drop-shadow(2px 2px 4px #000)") // βœ“
// @ts-expect-error blur wants a length
cssFilter("blur(45deg)")
// @ts-expect-error bad drop-shadow color
cssFilter("drop-shadow(2px 2px 4px wrong)")

Note: calc() / var() inside an argument are undecidable at compile time β€” use the casual or IntelliSense tier for those.

/ api

API

Public surface β€” component props, runtime helpers, and the type exports.

Β§ FilterBuilder / FilterBuilderPanel

<FilterBuilder
  value: FilterString | (string & {})
  onChange: (next: FilterString) => void
  mode?: "filter" | "backdrop-filter"
  className?: string
  aria-label?: string
/>

FilterBuilder is popover-wrapped; FilterBuilderPanel renders the same editor inline. Both are controlled.

PropTypeDescription
valueFilterString | (string & {})Current CSS filter value. Required. `none` is the empty state.
onChange(next: FilterString) => voidFires when the edited list changes. Emits the normalized space-separated string (or `none`).
mode"filter" | "backdrop-filter"Which property the live preview targets. Both share the grammar, so this does NOT narrow the output type. Default `filter`.

Β§ Sub-components

<FilterFunctionRow item onChange onRemove />

One editable function row β€” a grouped function select, the right per-argument editors (drop-shadow gets x/y/blur + a color control), and a remove button. The dispatch made visible.

<FilterArgEditor label kind value onChange />

A single argument editor: a numeric field plus a unit select for length/angle slots; a number + optional % for amount slots; raw text for opaque calc()/var().

<AddFilterMenu onAdd />

Grouped select of every supported function; calls onAdd with the chosen name.

<FilterPreview value mode? onChange? />

A busy-background stage with a card driven by the value, a filter ↔ backdrop-filter toggle, plus blur/brightness/contrast/saturate/hue-rotate scrubbers β€” the showcase.

Β§ Runtime helpers

cssFilter<S extends string>(value: S & FilterLiteral<S>): S

Call-site validator. Mirrors cssTransform() / cssCalc() / color() / easing().

parseFilter(src: string): FilterItem[] | null

String β†’ typed items, or null on unknown-function / arity / syntax error. `none` and empty β†’ []. Tolerates calc()/var() args and a leading-color drop-shadow.

formatFilter(items: FilterItem[]): string

Canonical re-serialization (single spaces; drop-shadow color-last). Empty list β†’ `none`.

itemToCss(item: FilterItem): string

One item β†’ its CSS function string.

filterFunctions(src: string): string[]

Runtime mirror of FunctionsOf β€” the function names in order.

defaultItem(fn): FilterItem

Seed a fresh row with sensible defaults (blur(4px), brightness(1), hue-rotate(90deg), a soft drop-shadow, url(#filter)).

argSpec(fn): { min, max, kind, label }

The runtime dispatch table β€” token-count range, argument kind, and the control label that drive the UI.

Β§ Types

FilterLiteral<S>
Strict validator β€” S if every function validates (arity + arg dimensions, incl. drop-shadow color), else never.
FilterString
Suggestion union: per-function heads + `none`.
FilterStringMap / FilterFn
Function β†’ output-string map and its key union.
FilterFunctionName / AmountFn
Union of every function name; the amount-function subset.
FunctionsOf<S>
Tuple of the function names in a filter string.
FunctionCountOf<S>
Number of functions in the list.
HasDropShadow<S>
Whether the list contains a drop-shadow (the one function with a color arg).
FilterItem
Per-function discriminated-union state (exported for advanced use).

Β§ Strict-tier scope

  • Full function-list dispatch: SplitBySpace β†’ ParseFunction β†’ a signature table validating each function's arity and every argument's dimension (IsLength/IsAngle/IsNonNegativeNumber/IsPercentage).
  • drop-shadow validates 2–3 lengths plus an optional trailing color, checked against the color-picker's ColorLiteral. A functional color whose own body has spaces and a slash β€” rgb(0 0 0 / 0.5) β€” stays one token thanks to the paren-aware split.
  • Strict accepts color-last only. A leading-color shadow (drop-shadow(red 2px 2px)) resolves to never at the type level, but the runtime parser accepts it and normalizes to color-last. Bare keyword colors (red) are not in ColorLiteral β€” use hex / functional colors.
  • Dimension + arity only β€” numeric values are not range-checked (no "opacity must be ≀ 1").
  • calc() / var() inside an argument resolve to never at the strict tier (undecidable at compile time). The runtime parser accepts them β€” use the casual / IntelliSense tier.
/ install

Drop it in

One command via the shadcn CLI.

$ pnpm dlx shadcn@latest add https://turtiesocks.github.io/ridiculous/r/filter-builder.json