VHS Filter
Hold a scene under analog tape — chroma bleed, tape wobble, a head-switch tear and luma noise
This filter re-reads the real pixels of the scene it wraps through the experimental html-in-canvas browser API. The preview above shows the true effect in Chrome 149+ with chrome://flags/#canvas-draw-element enabled (nested canvases need 152.0.7944.0+). Every other browser — Safari, Firefox, Chrome without the flag — falls back to a CSS approximation, so the page and your video still play.
Rendering is supported through the CLI, Studio, Lambda and SSR with --gl=angle, or --gl=swangle on a machine without a GPU. Chrome may change or discontinue the API; the fallback is what keeps your composition renderable if that happens.
Installation
$ pnpm dlx shadcn@latest add @remocn/vhs-filterUsage
VhsFilter is a wrapper. Put a scene inside it and every frame of that scene is
sampled and repainted as tape: the colour bleeds sideways off every edge while
the brightness stays sharp, rows slide out of line on a slow wander, the bottom
of the frame tears where a real deck switches heads, and noise settles over all
of it.
It has no duration of its own — it lasts exactly as long as the scene it wraps,
so there is nothing to time and no Sequence to budget.
import { VhsFilter } from "@/components/remocn/vhs-filter";
export const MyScene = () => (
<VhsFilter>
<Scene />
</VhsFilter>
);Driving it per frame
Every knob is a plain number, so the tape can get worse on a beat and recover
after it. intensity scales all four behaviours at once, which makes it the one
to interpolate when you want the scene to surface out of the noise.
import { interpolate, useCurrentFrame } from "remotion";
import { VhsFilter } from "@/components/remocn/vhs-filter";
export const MyScene = () => {
const frame = useCurrentFrame();
const intensity = interpolate(frame, [0, 24], [1, 0.15], {
extrapolateRight: "clamp",
});
return (
<VhsFilter intensity={intensity}>
<Scene />
</VhsFilter>
);
};Props
| Prop | Type | Default | Description |
|---|---|---|---|
bleed | number | 1 | How far colour smears sideways from where it belongs. Brightness never smears with it, which is what makes the picture read as tape rather than as a blur. |
wobble | number | 1 | How far rows wander out of line. A slow drift across the frame plus per-line jitter, so edges never sit quite straight. |
noise | number | 1 | Strength of the luma grain laid over the whole frame. |
intensity | number | 1 | Master amount, 0 to 1. Scales bleed, wobble, noise, scanlines and the head-switch tear together. At 0 the scene comes through untouched. |
children | React.ReactNode | — | The scene the filter wraps and re-reads every frame. |
Notes
The filter expects a full-bleed, opaque scene. It reads pixels from a displaced coordinate, so where the scene is transparent it has nothing to move and the untouched scene shows through underneath — put a background inside the wrapper rather than behind it.
The wobble and the head-switch tear are functions of the current frame, not of a running clock, and they oscillate rather than accumulate. Two renders of the same frame produce the same pixels, and a filter left on for a minute looks the same at the end as at the start.