Takumi

Visual Effects

Create CSS image effects with Takumi, including gradient text, masks, halftone, and dithering.

Build these effects with CSS gradients, shadows, masks, and SVG filters. Each preview is Takumi test output. The full markup of every scene is in takumi/tests/fixtures-html.

Aurora and film grain

Stack soft radial ellipses that fade out at 68% for the mesh. A blurred conic layer on top adds color drift.

Aurora and film grain
background-color: #05010f;
background-image:
  radial-gradient(ellipse 80% 60% at 70% 20%, rgba(139, 92, 246, 0.55), transparent 68%),
  radial-gradient(ellipse 70% 60% at 20% 80%, rgba(236, 72, 153, 0.45), transparent 68%),
  radial-gradient(ellipse 60% 50% at 60% 65%, rgba(45, 212, 191, 0.4), transparent 68%);

The grain is a feTurbulence filter graph on an empty layer, blended with overlay. The title crosses the whole hue wheel with two stops, through in oklch longer hue:

Aurora and film grain
.grain {
  filter: url("data:image/svg+xml,<filter>…feTurbulence…</filter>");
  mix-blend-mode: overlay;
}
.title {
  background-image: linear-gradient(to right in oklch longer hue, #ff5f6d, #ffc371);
  background-clip: text;
  color: transparent;
}

Aurora mesh with film grain

Neon CRT

  • Neon: stacked text-shadows that widen in the brand color
  • Chromatic aberration: two hard shadows in opposite directions
  • Scanlines and vignette: gradient overlays
Neon CRT
.neon {
  color: #fff;
  text-shadow:
    0 0 6px #fff,
    0 0 18px #ff2d95,
    0 0 42px #ff2d95,
    0 0 90px #ff2d95;
}
.chromatic {
  text-shadow:
    -2px 0 rgba(255, 0, 85, 0.7),
    2px 0 rgba(0, 255, 255, 0.7);
}
.scanlines {
  background-image: repeating-linear-gradient(
    180deg,
    rgba(0, 0, 0, 0) 0px,
    rgba(0, 0, 0, 0) 3px,
    rgba(0, 0, 0, 0.3) 3px,
    rgba(0, 0, 0, 0.3) 5px
  );
}
.vignette {
  background-image: radial-gradient(ellipse at 50% 50%, transparent 55%, rgba(0, 0, 0, 0.6) 100%);
}

Neon CRT

Chrome type

Clip a gradient with hard color stops to the glyphs for a metallic finish. feSpecularLighting adds the bevel rim. The starburst behind is one repeating-conic-gradient:

Chrome type
.chrome {
  background-image: linear-gradient(
    180deg,
    #0b1020 0%,
    #34304b 14%,
    #c3b9d5 26%,
    #ffffff 34%,
    #5a6cae 48%,
    #ffffff 56%,
    #e8ecff 84%,
    #c3b9d5 90%,
    #211c35 100%
  );
  background-clip: text;
  color: transparent;
  filter: url("data:image/svg+xml,<filter>…</filter>");
}
.stage {
  background-image: repeating-conic-gradient(
    from 0deg at 50% 130%,
    rgba(94, 80, 163, 0.35) 0deg 5deg,
    rgba(11, 6, 23, 0) 5deg 10deg
  );
}
Chrome type
<filter x="-20%" y="-20%" width="140%" height="140%">
  <feGaussianBlur stdDeviation="1.4" in="SourceGraphic" result="blur" />
  <feSpecularLighting surfaceScale="1.6" specularConstant="0.9" specularExponent="40"
    lighting-color="#ffffff" in="blur" result="spec">
    <fePointLight x="600" y="-60" z="900" />
  </feSpecularLighting>
  <feComposite operator="in" in="spec" in2="SourceAlpha" result="rim" />
  <feMerge>
    <feMergeNode in="SourceGraphic" />
    <feMergeNode in="rim" />
  </feMerge>
</filter>

Chrome type

Halftone

Build a dot screen in three steps:

  • Multiply a tiled radial gradient with a tone ramp.
  • Crush it with contrast().
  • Re-ink the black dots with a lighten layer. Set isolation: isolate on the parent.
Halftone
.dots {
  background-image:
    radial-gradient(circle closest-side, #666, #fff), linear-gradient(115deg, #333, #fff);
  background-size:
    14px 14px,
    100% 100%;
  background-repeat: space, no-repeat;
  background-blend-mode: multiply;
  filter: contrast(14);
}
.ink {
  background-color: #e11d48;
  mix-blend-mode: lighten;
}

Halftone

Grid hero

A hairline grid faded by mask-image, corner glows, and a card with a hard offset shadow:

Grid hero
.grid {
  background-image:
    linear-gradient(rgba(255, 255, 255, 0.09) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.09) 1px, transparent 1px);
  background-size: 40px 40px;
  mask-image: radial-gradient(ellipse 70% 60% at 50% 100%, #000 60%, transparent 100%);
}
.card {
  background-color: #fdfdf7;
  border: 4px solid #111;
  box-shadow: 18px 18px 0 #22c55e;
}

Grid hero

Gradient text

backgroundClip: "text" fills the glyphs with the background. Make the text color transparent:

Gradient text
<h1
  style={{
    backgroundImage: "linear-gradient(90deg, #ff3b30, #ffcc00, #34c759, #007aff, #5856d6)",
    backgroundClip: "text",
    color: "transparent",
  }}
>
  Gradient Text
</h1>

Gradient text

Masks

maskImage fades content through a gradient's alpha channel:

Masks
<div style={{ maskImage: "radial-gradient(circle, black 60%, transparent 100%)" }}>
  <img src={photo} style={{ width: "100%", height: "100%" }} />
</div>

Masked image

Clip shapes

clipPath cuts an element to a <basic-shape>:

Clip shapes
<div style={{ clipPath: "polygon(50% 0%, 0% 100%, 100% 100%)" }} />

Clip path triangle

Text on a path

offsetPath places an element along a shape, and offsetDistance picks the spot. To set text on a circle, wrap each glyph and spread the distances:

Text on a path
const glyphs = [...ring].map((char, i) => (
  <div
    key={i}
    style={{
      position: "absolute",
      offsetPath: "circle(240px at 600px 315px)",
      offsetDistance: `${(i / ring.length) * 100}%`,
    }}
  >
    {char}
  </div>
));

Circular text

Dithering and duotone

filter takes inline SVG filter graphs as data: URLs, next to filter functions. Every SVG 1.1 filter primitive works, from feColorMatrix and feComponentTransfer to feImage, feTile, and feTurbulence. Browsers parse the same markup, so prototype the effect on an HTML page and paste it in:

Dithering and duotone
const filterUrl = (markup: string) => `url("data:image/svg+xml,${encodeURIComponent(markup)}")`;

<div style={{ filter: `contrast(1.4) ${filterUrl(markup)}` }} />;

Threshold effects need color-interpolation-filters="sRGB" on the <filter>, because the spec default is linearRGB. Posterize snaps each channel to fixed levels:

Dithering and duotone
<filter color-interpolation-filters="sRGB" x="0" y="0" width="100%" height="100%">
  <feComponentTransfer>
    <feFuncR type="discrete" tableValues="0 0.333 0.667 1" />
    <feFuncG type="discrete" tableValues="0 0.333 0.667 1" />
    <feFuncB type="discrete" tableValues="0 0.333 0.667 1" />
  </feComponentTransfer>
</filter>

For ordered dithering, tile a Bayer matrix with feImage and feTile, and add it as noise before the quantize step. For duotone, map a 1-bit luma threshold to a two-color table. The full graphs are in style_filter_reference_dither.html and style_filter_reference_duotone.html.

Ordered dithering

Duotone

To remove banding from smooth gradients, set the dithering render option. It dithers gradient fills as they paint. See Dithering.

Last updated on