Duotone Plate Hero
A real two-colour separation via SVG filters — with the sRGB interpolation flag that everyone leaves out.
DOM + CSS0 deps1 file · 5 KB TSX
Props
"use client";
import { useId } from "react";
/** Hex to the 0..1 channel triple an feFunc table wants. */
function chan(hex: string): [number, number, number] {
const h = hex.replace("#", "");
const n = parseInt(h.length === 3 ? h.split("").map((c) => c + c).join("") : h, 16);
return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];
}
export function DuotonePlateHero({
headline = "BLACKHORSE YARD",
standfirst = "Twelve rooms, one loading bay, and a licence until four.",
image = "https://images.unsplash.com/photo-1470225620780-dba8ba36b745?auto=format&fit=crop&w=1600&q=70",
shadow = "#0b1b3f",
highlight = "#d6ff00",
grain = true,
// ---------------------------------------------------------------
// Full source available with a viberdy Pro subscription.
// https://viberdy.dev/pricing
// ---------------------------------------------------------------
}Details
About this heroe
The duotone people usually ship is a chain of CSS filters — sepia, saturate, hue-rotate — tuned by eye until it looks approximately right for one image. It clips, it cannot reach arbitrary colour pairs, and it drifts between browsers. A real duotone is two SVG filter primitives: collapse the image to a single tone with a colour matrix carrying Rec.709 weights on all three output rows, then remap that tone with a component transfer whose table has exactly TWO entries per channel — the shadow colour, then the highlight. Two entries is the whole separation: a straight ramp from one colour to the other. The detail that decides whether it matches the palette you chose is a single attribute. SVG filters compute in linearRGB unless told otherwise, so without forcing sRGB interpolation the result comes out markedly darker and more contrasty than the two colours picked, with nothing in the code to suggest why — it is the most surprising default in the whole filter spec. Two smaller traps are handled too: the filter id comes from useId so a second duotone on the same page cannot silently inherit the first one's colours, and the defs svg is sized to nothing rather than hidden with display none, which has historically stopped filter references resolving at all.
Curator’s note
Leave out colorInterpolationFilters="sRGB" and the duotone comes out darker than the colours you picked, with nothing to tell you why.

