Use this rule only when the top-level skill lists an effect that matches the requested look, or when the user asks to create a reusable custom effect.
Docs: https://www.remotion.dev/docs/effects Custom effect docs: https://www.remotion.dev/docs/create-effect
Usage
Install the package that provides the chosen effect:
npx remotion add @remotion/effectsUse npx remotion add @remotion/light-leaks for lightLeak() and npx remotion add @remotion/starburst for starburst().
Effects are functions passed to the effects prop of canvas-based components such as <Video> from @remotion/media, <Solid>, <CanvasImage>, and <HtmlInCanvas>.
import {Video} from '@remotion/media';
import {blur} from '@remotion/effects/blur';
<Video src="https://remotion.media/video.mp4" effects={[blur({radius: 8})]} />;Use the effect docs for exact props and imports. Most @remotion/effects imports use @remotion/effects/<effect-slug>; uvTranslate() and xyTranslate() use @remotion/effects/translate; lightLeak() uses @remotion/light-leaks; starburst() uses @remotion/starburst.
These effects use WebGL2. During renders, enable WebGL with:
import {Config} from '@remotion/cli/config';
Config.setChromiumOpenGlRenderer('angle');Custom effects
Use createEffect() from remotion when the user wants a reusable effect factory that works in the same effects array as @remotion/effects.
Prefer a custom effect over <HtmlInCanvas onPaint> when the transformation should be reusable, parameterized, editable in Studio, or stackable with other effects.
For quick project-specific effects, keep the effect next to the composition, for example src/effects/palette-map.ts. For library effects intended for @remotion/effects, follow the repository's add-effect skill instead.
createEffect() expects:
type: stable reverse-DNS identifier, for examplecom.example.paletteMap.label: Studio label, commonlypaletteMap().documentationLink: URL ornull.backend:"2d","webgl2"or"webgpu".calculateKey(params): stable string containing every resolved parameter that changes output.setup(target): create reusable backend state, or returnnull.apply({source, target, width, height, params, state, flipSourceY}): draw the transformed result intotarget.cleanup(state): free resources created bysetup().schema: anInteractivitySchemafor Studio controls.disabledis added automatically.validateParams(params): throw on missing or invalid values.
Use backend: "2d" for simple pixel, filter, drawImage, or image-data effects. Use WebGL2 only when shader math or GPU performance is needed; during renders, enable WebGL as shown above.
import {createEffect, type InteractivitySchema} from 'remotion';
type MyEffectParams = {
readonly amount?: number;
};
const myEffectSchema = {
amount: {
type: 'number',
min: 0,
max: 1,
step: 0.01,
default: 1,
description: 'Amount',
},
} as const satisfies InteractivitySchema;
const resolve = (params: MyEffectParams) => ({
amount: params.amount ?? 1,
});
export const myEffect = createEffect<MyEffectParams, null>({
type: 'com.example.myEffect',
label: 'myEffect()',
documentationLink: null,
backend: '2d',
calculateKey: (params) => {
const {amount} = resolve(params);
return `my-effect-${amount}`;
},
setup: () => null,
apply: ({source, target, width, height, params}) => {
const ctx = target.getContext('2d');
if (!ctx) {
throw new Error('Could not get a 2D context for myEffect().');
}
const {amount} = resolve(params);
ctx.clearRect(0, 0, width, height);
ctx.filter = `opacity(${amount * 100}%)`;
ctx.drawImage(source, 0, 0, width, height);
ctx.filter = 'none';
},
cleanup: () => undefined,
schema: myEffectSchema,
validateParams: ({amount = 1}) => {
if (typeof amount !== 'number' || !Number.isFinite(amount) || amount < 0 || amount > 1) {
throw new TypeError('amount must be a number between 0 and 1');
}
},
});For a WebGL2 effect, compile/link shaders in setup(), keep the program, fullscreen quad, texture, and uniform locations in state, upload source in apply(), and free GPU resources in cleanup(). Minimal shape:
import {createEffect, type InteractivitySchema} from 'remotion';
type RgbShiftParams = {
readonly amount?: number;
};
type RgbShiftState = {
readonly gl: WebGL2RenderingContext;
readonly program: WebGLProgram;
readonly vao: WebGLVertexArrayObject;
readonly vbo: WebGLBuffer;
readonly texture: WebGLTexture;
readonly uSource: WebGLUniformLocation | null;
readonly uOffset: WebGLUniformLocation | null;
};
const rgbShiftSchema = {
amount: {
type: 'number',
min: 0,
max: 80,
step: 1,
default: 12,
description: 'Amount',
},
} as const satisfies InteractivitySchema;
export const rgbShift = createEffect<RgbShiftParams, RgbShiftState>({
type: 'com.example.rgbShift',
label: 'rgbShift()',
documentationLink: null,
backend: 'webgl2',
calculateKey: ({amount = 12}) => `rgb-shift-${amount}`,
setup: (target) => {
const gl = target.getContext('webgl2', {
premultipliedAlpha: true,
alpha: true,
preserveDrawingBuffer: true,
});
if (!gl) {
throw new Error('Could not get a WebGL2 context for rgbShift().');
}
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
// Compile/link shaders, create a fullscreen quad VAO/VBO, create a
// CLAMP_TO_EDGE RGBA texture, and get uSource/uOffset uniform locations.
return createRgbShiftState(gl);
},
apply: ({source, width, height, params, state, flipSourceY}) => {
const amount = params.amount ?? 12;
const {gl} = state;
gl.viewport(0, 0, width, height);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, flipSourceY);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, state.texture);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
gl.RGBA,
gl.UNSIGNED_BYTE,
source as TexImageSource,
);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.useProgram(state.program);
if (state.uSource) gl.uniform1i(state.uSource, 0);
if (state.uOffset) gl.uniform2f(state.uOffset, amount / width, 0);
gl.bindVertexArray(state.vao);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
},
cleanup: ({gl, program, vao, vbo, texture}) => {
gl.deleteTexture(texture);
gl.deleteBuffer(vbo);
gl.deleteProgram(program);
gl.deleteVertexArray(vao);
},
schema: rgbShiftSchema,
validateParams: ({amount = 12}) => {
if (typeof amount !== 'number' || !Number.isFinite(amount) || amount < 0 || amount > 80) {
throw new TypeError('amount must be a number between 0 and 80');
}
},
});For a complete 2D and WebGL2 pair, see packages/example/src/EffectsTestbed/sample-posterize-2d.ts and packages/example/src/EffectsTestbed/sample-rgb-shift-webgl.ts.
Use the returned factory in an effects array:
import {CanvasImage, staticFile} from 'remotion';
import {myEffect} from './effects/my-effect';
export const MyComp: React.FC = () => {
return (
<CanvasImage
src={staticFile('image.png')}
effects={[myEffect({amount: 0.8})]}
/>
);
};When generating a custom effect, also:
- Include
disabled?: booleanonly through the returned factory; do not add it to the custom params type or schema. - Validate required parameters at factory-call time with
validateParams. - Include all defaults in both
schemaand theresolve()helper. - Reset mutable 2D context state such as
filter,globalAlpha, transforms, and compositing after drawing. - Preserve alpha unless the requested effect intentionally changes transparency.