---
title: Effect kinds
description: Define an effect kind for SVG filters, with a stage that paints its effects.
mode: wide
---

An effect kind fixes what an effect of that kind supplies and what paints it. This page builds
`svgFilter` for the [SVG backend](/docs/rendering/writing-a-backend): an effect supplies filter
primitives from its params, and a stage turns them into a `<filter>` around the actor.

<SvgDotsScene />

## The kind and its stage

```tsx twoslash title="svg-filter.tsx"
// @jsx: react-jsx
// @filename: svg.ts
import { defineBackend } from "@motionactor/core";

export const svg = defineBackend("svg");
// @filename: svg-filter.tsx
// ---cut---
import { useId, type ReactNode } from "react";
import {
  defineEffectKind,
  effectKindTypes,
  readEffectDefinition,
  resolveEffectParams,
  type EffectKindTypes,
  type EffectStage,
} from "@motionactor/core";
import { svg } from "./svg";

/** One SVG filter primitive, as data. */
export type SvgPrimitive =
  | {
      readonly type: "feGaussianBlur";
      readonly stdDeviation: number;
      readonly result?: string;
    }
  | {
      readonly type: "feTurbulence";
      readonly baseFrequency: number;
      readonly numOctaves: number;
      readonly result: string;
    }
  | {
      readonly type: "feDisplacementMap";
      readonly in: string;
      readonly in2: string;
      readonly scale: number;
    }
  | { readonly type: "feMerge"; readonly inputs: readonly string[] };

/** The primitives of one filter, from an effect's params. */
export type SvgPrimitives<P> = (params: P) => readonly SvgPrimitive[];

/** What an `svgFilter` effect supplies beside its params. */
export interface SvgFilterSpec<P> {
  readonly primitives: SvgPrimitives<P>;
}

export interface SvgFilterTypes extends EffectKindTypes {
  readonly spec: SvgFilterSpec<this["params"]>;
  readonly runtime: SvgPrimitives<this["params"]>;
}

const element = (primitive: SvgPrimitive, key: number) => {
  switch (primitive.type) {
    case "feGaussianBlur":
      return (
        <feGaussianBlur
          key={key}
          stdDeviation={primitive.stdDeviation}
          result={primitive.result}
        />
      );
    case "feTurbulence":
      return (
        <feTurbulence
          key={key}
          baseFrequency={primitive.baseFrequency}
          numOctaves={primitive.numOctaves}
          result={primitive.result}
        />
      );
    case "feDisplacementMap":
      return (
        <feDisplacementMap
          key={key}
          in={primitive.in}
          in2={primitive.in2}
          scale={primitive.scale}
        />
      );
    case "feMerge":
      return (
        <feMerge key={key}>
          {primitive.inputs.map((input, i) => (
            <feMergeNode key={i} in={input} />
          ))}
        </feMerge>
      );
  }
};

function Filtered(props: {
  primitives: readonly SvgPrimitive[];
  children: ReactNode;
}) {
  const id = useId();
  return (
    <>
      <filter id={id} x="-50%" y="-50%" width="200%" height="200%">
        {props.primitives.map(element)}
      </filter>
      <g filter={`url(#${id})`}>{props.children}</g>
    </>
  );
}

/** Each effect gets its own filter, wrapped around the ones before it. */
export const svgFilterStage: EffectStage<ReactNode> = {
  wrap: ({ effects }, node) =>
    effects.reduce((inner, effect) => {
      const definition = readEffectDefinition(effect.binding);
      const primitives = definition && svgFilter.runtimeOf(definition);
      if (!primitives) return inner;
      return (
        <Filtered primitives={primitives(resolveEffectParams(effect))}>
          {inner}
        </Filtered>
      );
    }, node),
};

export const svgFilter = defineEffectKind(svg, {
  name: "filter",
  types: effectKindTypes<SvgFilterTypes>(),
  applies: "actor",
  lower: (spec) => spec.primitives,
});
```

`types` names two slots in terms of the effect's own params. `spec` is what `defineEffect` takes
beside its name and params, and `runtime` is what the stage reads. `lower` turns one into the
other once, when the effect is defined. Here the effect gives a `primitives` function and the
stage calls it, so `lower` only unwraps it. The stage reads it back with
`svgFilter.runtimeOf(definition)`, typed by the kind, which is `undefined` for another kind's
effect.

A stage is one function, `wrap(context, node)`. It gets the actor's content and the effects it
owns, and returns the content wrapped in their treatment. The DOM style stage wraps it in a
styled element, and the Pixi filter stage in a container with filters. This one gives each
effect its own `<filter>` and `<g>`, so two filters on one actor stack in the order they were
applied. `Filtered` is a component so it can call `useId`, which keeps filter ids unique when
the same scene renders twice on a page.

The kind names no stage. The backend pairs them, `stages: [{ kinds: [svgFilter], stage:
svgFilterStage }]`, so the same kind could paint through a Vue stage under another binding. Here
both live in one React file for brevity; a backend shared across bindings keeps the kind in a
framework-free module. The ref in `svg.ts` stays React-free, and a scene that applies SVG
filters imports `./svg-filter`.

A node a view [binds](/docs/rendering/renderers#bindings) to an actor has no wrapper to render.
An inline stage that can paint on one anyway adds `node` to its pairing: given the same
context, it returns the `transform`, `filter`, `opacity` and `clipPath` the node folds into its
own style, or `unsupported` naming what it would need. The DOM style stage has one. A binding
rejects an effect whose stage has none.

## Effects

Every effect of the kind is a `defineEffect` call, typed from its params. The code beside the
figure is its scene: `blur` is one primitive, `halo` merges a blurred copy under the dot, and
`wobble` displaces the dot by a noise field. `primitives` returns `SvgPrimitive[]`, so a
misspelled primitive or a missing `in2` fails the type check.

Each effect registers beside its definition, keyed `"svg:<name>"`, so every actor reads
`dot.fx.blur()`. `filter.play` drives `progress` from 0 to 1 as it does for a DOM effect, and
`filter.progress.tween(0)` brings it back. See [Effects](/docs/rendering/effects#call-by-name).

## Raster

A kind also declares a `raster`. Inline, the default, paints over the actor as it is drawn.
Offscreen means the stage needs the actor's pixels first, so offscreen stages wrap the content
before inline ones do. An SVG filter is inline: the browser rasterizes the filtered group
itself. Two kinds can share a stage if they agree on its raster; one kind pairs with one stage.

## Effects that arrive by name

An effect a scene imports carries its definition, and `readEffectDefinition` returns it. An
effect that crossed a process boundary as `{ backend: "svg", name: "halo" }` has lost it.
`groupEffectsByStage` rebinds it from the effect registry, where `registerEffects([halo])` put it
when its module was imported, and skips it when nothing registered that wire id. A host that
reads effects itself, without a stage, rebinds the same way with `registeredEffect(binding)` from
`@motionactor/core/internal`.
