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.
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)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.
saturate(1.4) contrast(1.1) drop-shadow(6px 6px 10px rgb(0 0 0 / 0.4))Three usage tiers
From useState-and-go to per-function dimension-typed dispatch.
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)")
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)")
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
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.
| Prop | Type | Description |
|---|---|---|
| value | FilterString | (string & {}) | Current CSS filter value. Required. `none` is the empty state. |
| onChange | (next: FilterString) => void | Fires 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-shadowvalidates 2β3 lengths plus an optional trailing color, checked against the color-picker'sColorLiteral. 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 toneverat the type level, but the runtime parser accepts it and normalizes to color-last. Bare keyword colors (red) are not inColorLiteralβ use hex / functional colors. - Dimension + arity only β numeric values are not range-checked (no "opacity must be β€ 1").
calc()/var()inside an argument resolve toneverat the strict tier (undecidable at compile time). The runtime parser accepts them β use the casual / IntelliSense tier.
Drop it in
One command via the shadcn CLI.
$ pnpm dlx shadcn@latest add https://turtiesocks.github.io/ridiculous/r/filter-builder.json