Filters

The filter style runs a chain of GPU shader passes over a node's rendered result: the node itself and its whole subtree, as one image. bevy-react ships fourteen filters (blur, color adjustments, bloom, drop shadows, outlines, gradient recolors and a pinch distortion), and apps can register their own.

Usage

<image src="images/parrot.png" style={{ filter: { name: "grayscale" } }} />

A chain runs its entries in array order, each one over the previous one's output:

<node
  style={{
    filter: [{ name: "blur", params: { radius: 4 } }, { name: "sepia" }],
  }}
>
  <text>Old photo</text>
</node>

Chains

The value is one { name, params } entry or an ordered array of entries.

  • params and every param in it are optional. An omitted param takes the filter's default, which for built-ins is a visible effect, unlike CSS: a bare { name: "blur" } is a 20px blur, and a bare { name: "grayscale" } is full grayscale.
  • Names and params are typed from your generated bevy.ts, which lists the built-ins and your own filters (see TypeScript codegen). Without generated bindings the field accepts no value.
  • Length params take a number of logical pixels or a string like "12px". Angle params take degrees, or a string such as "0.5turn" or "1rad". Color params take a CSS color string.
  • An entry with an unknown name is skipped with a filterUnknown devtools warning, and an entry with invalid params (an unknown param, a % blur radius) with a filterParams warning. The rest of the chain still runs.
  • Morph filters (crossfade, linearWipe, pixelize) belong to morphFilter and are rejected here the same way.
  • An empty array is the same as no filter.

What gets filtered

  • A non-empty filter promotes the node to a composited layer: the subtree is rendered into a texture once, and the chain runs over that texture. Changing a param re-runs only the filter passes, not the content, so animating params is cheap.
  • The image includes the node's own background and border. It works on a leaf too: an <image>, a <text> or an empty colored <node>.
  • Filters that reach outside the content (a blur, a glow, a shadow, an outline) enlarge the layer by their reach on every side, so their effect is not cut off at the node's edge. The reach of a chain is the sum of its entries' reaches.
  • An ancestor's overflow clipping clips the filtered result, as on the web.
  • opacity on the same node fades the filtered result as a whole.
  • filter and backdropFilter are independent and can be combined on one node.

Built-in filters

Each table lists the params and their defaults. Lengths are logical pixels and angles are degrees.

Color adjustments

Each takes one param and recolors every pixel, keeping its alpha:

Filter Param Default Effect
brightness amount 1 Multiplies the color: 0 is black, above 1 brightens
contrast amount 1 Scales the distance from mid-gray: 0 is flat gray
saturate amount 1 0 is grayscale, above 1 oversaturates
grayscale amount 1 Mix toward grayscale: 0 none, 1 full
sepia amount 1 Mix toward sepia: 0 none, 1 full
invert amount 1 Mix toward the inverted color: 0 none, 1 full
hueRotate angle 0 Rotates the hue around the color wheel

The defaults follow CSS: brightness, contrast and saturate default to no change, grayscale, sepia and invert to the full effect.

blur

A Gaussian blur.

Param Default Effect
radius 20 Blur radius; the blur reaches three times this far.

bloom

A glow: bright areas bleed light over their surroundings.

Param Default Effect
radius 12 How far the glow spreads (it reaches three times this far)
threshold 0.7 Brightness (0 to 1) above which pixels glow
intensity 1 Strength of the glow added on top; 0 is no effect

threshold is perceived brightness: 1 makes nothing glow, 0 makes everything glow.

chromaticAberration

Color fringing: the red channel shifts one way, blue the opposite way, and green stays.

Param Default Effect
offset 4 Shift distance of the red and blue channels
angle 0 Shift direction, clockwise from the +x axis (0 is right)
rotation 0 Swirl: red turns this many degrees about the center, blue the other way

rotation is a plain number of degrees (no unit strings). The swirl grows toward the edges and leaves the center clean.

gradientMap

Recolors the content with a linear gradient laid across the node, keeping the content's alpha. On text, this is gradient text.

Param Default Effect
angle 0 Gradient direction: 0 points up, clockwise, like backgroundGradient
stops #38bdf8 to #a78bfa Up to six { color, position? } stops
amount 1 Mix from the original color (0) to the gradient (1)
<text
  style={{
    fontSize: 48,
    filter: {
      name: "gradientMap",
      params: {
        angle: 120,
        stops: [
          { color: "#38bdf8" },
          { color: "#a78bfa", position: 0.6 },
          { color: "#f472b6" },
        ],
      },
    },
  }}
>
  Gradient
</text>
  • position is a fraction from 0 to 1 along the gradient line, not a percentage. Missing positions are spread evenly, as in CSS.
  • The gradient spans the node's border box; the reach of other filters in the chain does not stretch it.
  • A stop's alpha scales the mix locally: a transparent stop leaves the original color.
  • Colors interpolate in linear RGB, which can differ slightly from the same stops in backgroundGradient.

outline

A ring of color around the content's silhouette, drawn under the content: outlined text, sticker-style icons.

Param Default Effect
width 2 Ring width
color "black" Ring color
softness 0 Feathers the outer edge over this many more pixels, like a glow

The outline follows whatever the chain has produced so far: [gradientMap, outline] outlines the recolored glyphs.

shadow

A drop shadow of the content's silhouette, like CSS drop-shadow(), drawn under the content.

Param Default Effect
color rgba(0,0,0,0.6) Shadow color
offsetX 0 Horizontal offset; positive moves right
offsetY 4 Vertical offset; positive moves down
spread 6 Blur radius of the shadow (not a CSS spread)

Unlike boxShadow, which shadows the node's box, shadow follows the shape of what is drawn: glyphs, transparent images, rounded children.

pinch

Squeezes the content toward a point, or bulges it away, with optional lighting that shades the distortion like a dented surface.

Param Default Effect
x, y 0.5 Center of the effect, 0 to 1 across the node
strength 0.5 1 is a full pinch, -1 a full bulge, 0 no effect
radius 0.8 Size of the effect, as a fraction of the node's larger side
light 0 Diffuse shading; 1 is nominal, more overdrives
lightAngle -135 Where the light comes from, clockwise from +x (-135 is top-left)
gloss 0 White specular highlight; 1 is nominal
glossSize 0.3 Highlight size, from 0 (a pinpoint) to 1 (a broad sheen)
outerSoftness 0.5 How the effect meets its rim: 0 a crease, 1 imperceptible
innerSoftness 0.5 How it peaks at its center: 0 a point, 1 a flat floor

Every param is relative to the node, so the effect scales with it. x and y use the same 0 to 1 coordinates as pointer events, so a pinch can follow the cursor directly:

import { useSharedValue, withSpring, withTiming } from "bevy-react";

function PinchCard() {
  const strength = useSharedValue(0);
  const x = useSharedValue(0.5);
  const y = useSharedValue(0.5);
  return (
    <node
      onPointerDown={(e) => {
        x.value = e.x;
        y.value = e.y;
        strength.value = withTiming(0.6, { duration: 120 });
      }}
      onPointerUp={() => {
        strength.value = withSpring(0);
      }}
      style={{
        filter: {
          name: "pinch",
          params: {
            x: { animated: x },
            y: { animated: y },
            strength: { animated: strength },
            radius: 0.6,
          },
        },
      }}
    >
      <text>Press me</text>
    </node>
  );
}

The filter reference lists the same params with their types.

Transitions

transition: { filter } eases the chain between style states (the base style, hoverStyle, pressStyle, focusStyle):

  • Same names in the same order: every param interpolates. Colors ease in linear RGB, and angles numerically (350 to 10 goes the long way).
  • One chain extends the other at the end, built-in filters only: the extra entries fade through their no-effect values, such as a blur from radius 0 or a shadow from transparent. They do not start from the defaults.
  • Anything else (different names, a reordered chain, a custom filter added) swaps at the halfway point.
  • Easing to no chain at all, by removing filter or setting [], snaps: the filter disappears at once.

Keep a no-effect entry in the base style when the filter should fade out as well as in:

<node
  style={{
    filter: { name: "blur", params: { radius: 0 } },
    transition: { filter: { duration: 250 } },
  }}
  hoverStyle={{ filter: { name: "blur", params: { radius: 6 } } }}
/>

A filter in a state style replaces the base chain as a whole. A no-effect entry still keeps the node a composited layer, with its memory and capture cost (see Layers).

Animated params

Any param accepts an inline { animated } binding to a shared value, driven every frame on the Bevy side without re-rendering React:

import { useSharedValue, withTiming } from "bevy-react";

function Reveal() {
  const radius = useSharedValue(12);
  return (
    <node
      onPointerEnter={() => {
        radius.value = withTiming(0);
      }}
      style={{
        filter: {
          name: "blur",
          params: { radius: { animated: radius, seed: 12 } },
        },
      }}
    />
  );
}
  • Values are in the param's units: pixels for lengths, degrees for angles. Color params take an interpolateColor binding.
  • gradientMap stops cannot be bound; its angle and amount can.
  • Bindings work in the base style only. A binding in hoverStyle, pressStyle or focusStyle is ignored with a warning.
  • Any binding on a filter param parks transition: { filter } for that node: the binding wins.
  • seed is the static value used in the binding's place for what is computed up front, such as how far a blur reaches outside the node. Size it for the largest value the animation reaches. Without a seed the filter's default is used (20px for blur).

See Animated values for the drivers.

Shader compilation

Each filter is a GPU pipeline, compiled in the background before its first use. While a node's filters are compiling, the node is not drawn at all, rather than flashing unfiltered. By default every built-in and registered filter is compiled at startup, so this normally only shows if you narrow it with precompile_filters:

use bevy_react::{FilterSelection, PrecompileFilters};

app.add_plugins(ReactPlugins.set(
    ReactUiPlugin::default().precompile_filters(PrecompileFilters {
        builtins: FilterSelection::Names(vec!["blur".into()]),
        ..default()
    }),
));

builtins, filters (your add_react_filter filters) and morphs (your morph filters) each take All (the default), Names(..) or Off. A shader that fails to compile keeps the node invisible and logs the error to the terminal, naming the layer.

Limits

  • bloom and shadow draw their effect over the node's unfiltered content, so every filter before them in the chain is lost: [blur, bloom] shows no blur. Put them first in the chain; the filters after them apply normally.
  • A pinch bulge, and a chromaticAberration swirl (rotation), can only spill about 16px outside the node; beyond that they are cut off at a straight edge. Keep a margin inside the node, or a smaller strength.
  • outline stays crisp up to about 12 physical pixels of width plus softness; typical text outlines are 1 to 6 pixels.
  • gradientMap takes at most six stops.
  • A nested <text> span cannot take a filter (spanLayerStyle warning); put it on the outer <text> or a wrapping <node>.
  • The layer limits apply: content painted outside the node's border box (plus the chain's reach) is cut off, and inside a <surface> the node renders unfiltered.

A gallery of built-in filters: grayscale, sepia, invert and hue-rotate parrots, a grayscaled product card, a blur and sepia chain, bloom on neon text and chromatic aberration.

To write your own filters in WGSL, see Custom filters.

See filter in the style reference, and bloom, blur, brightness, chromaticAberration, contrast, gradientMap, grayscale, hueRotate, invert, outline, pinch, saturate, sepia and shadow in the filter reference.