Code components
Code components are free-form code — write whatever you want to build any effect, from WebGL shaders to custom interactions. The easiest way is the built-in AI chat, which has specialized context about code components: describe the effect and it writes it for you. You get a live preview in the middle and the component's controls on the right.

Exposing controls
Add a few JSDoc comments at the top of the file to turn your code's props into editable controls in the properties panel:
/** @label "Aurora Background" */
/** @comment "GPU-rendered animated aurora using a custom WebGL fragment shader" */
/** @controls {
"speed": { "type": "slider", "label": "Speed", "min": 0.1, "max": 3, "default": 0.6, "step": 0.05 },
"intensity": { "type": "slider", "label": "Intensity", "min": 0.3, "max": 2.5, "default": 1.2, "step": 0.05 },
"colorA": { "type": "color", "label": "Color A", "default": "#0ea5e9" },
"bgColor": { "type": "color", "label": "Background", "default": "#020617" }
} */- @label — the component's display name in the editor.
- @comment — a short description shown alongside it.
- @controls — a JSON map. Each entry becomes one editable control, with a
type, alabel, and adefault, plus a few options that depend on the type.
Using the values
Each key in @controls becomes a prop on your component. Declare them as function arguments — with the same defaults — and use them anywhere in the code:
function AuroraBackground({
speed = 0.6,
intensity = 1.2,
scale = 1.4,
colorA = '#0ea5e9',
colorB = '#a855f7',
colorC = '#ec4899',
bgColor = '#020617',
...props
}) {
// use speed, colorA, ... inside — pass them to the shader, styles, etc.
}When someone drags a control in the panel, the component re-renders with the new value, so the preview and the live site update instantly. The keys in @controls and your prop names must match.
Control types
| Type | What it is | Extra options |
|---|---|---|
| slider | a numeric slider | min, max, step, unit |
| number | a number input | min, max, step, unit |
| color | a color picker | — |
| text | a text input | placeholder |
| select | a dropdown | options: [{ label, value }] |
| toggle | an on/off switch | — |
| upload | upload a file (image, model, sequence) | accept, multiple |
| slot | connect canvas elements in as children | slotMax: 1, a number, or "infinite" |
| group | a button that opens a popup of nested controls | controls |
| transition | a button that opens the Motion transition editor | — |
Every control also takes an optional description to show a hint below it.
On the canvas
A code component behaves like any other component — instance it from the Library panel, then set its controls in the right panel.
Examples
Full code components you can drop in and adapt.
Wave gradient
An animated multi-color wave field driven by color and slider controls.
'use client';
/** @label "Wave Gradient" */
/** @comment "Animated multi-color wave field — pick four colors, sculpt the flow with frequency + amplitude." */
/** @controls {
"color1": { "type": "color", "label": "Color 1", "default": "#FF3624" },
"color2": { "type": "color", "label": "Color 2", "default": "#9EABFF" },
"color3": { "type": "color", "label": "Color 3", "default": "#FFAE00" },
"color4": { "type": "color", "label": "Color 4", "default": "#E29EFF" },
"seed": { "type": "slider", "label": "Seed", "min": 0, "max": 999, "step": 1, "default": 32 },
"speed": { "type": "slider", "label": "Speed", "min": 0, "max": 6, "step": 0.1, "default": 1.5 },
"freqX": { "type": "slider", "label": "Freq X", "min": 0, "max": 6, "step": 0.05, "default": 0.9 },
"freqY": { "type": "slider", "label": "Freq Y", "min": 0, "max": 12, "step": 0.1, "default": 6 },
"angle": { "type": "slider", "label": "Angle", "min": 0, "max": 360, "step": 1, "default": 105 },
"amplitude": { "type": "slider", "label": "Amplitude", "min": 0, "max": 4, "step": 0.05, "default": 2.1 },
"softness": { "type": "slider", "label": "Softness", "min": 0, "max": 1, "step": 0.01, "default": 0.74 },
"blend": { "type": "slider", "label": "Blend", "min": 0, "max": 1, "step": 0.01, "default": 0.54 }
} */
import React, { useEffect, useRef } from 'react';
import { withResponsiveProps, useStaticCanvas } from '@revyme/runtime';
function hexToRgb(hex) {
const m = String(hex).match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i);
if (!m) return [255, 255, 255];
return [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16)];
}
function WaveGradient({
color1 = '#FF3624',
color2 = '#9EABFF',
color3 = '#FFAE00',
color4 = '#E29EFF',
seed = 32,
speed = 1.5,
freqX = 0.9,
freqY = 6,
angle = 105,
amplitude = 2.1,
softness = 0.74,
blend = 0.54,
...props
}) {
const canvasRef = useRef(null);
const isStatic = useStaticCanvas();
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
let raf = 0;
let t0 = isStatic
? performance.now() - 1000 / Math.max(speed, 0.1)
: performance.now();
const SRC = 96;
const off = document.createElement('canvas');
off.width = SRC; off.height = SRC;
const oc = off.getContext('2d');
if (!oc) return;
const img = oc.createImageData(SRC, SRC);
const palette = [hexToRgb(color1), hexToRgb(color2), hexToRgb(color3), hexToRgb(color4)];
const seedOff = (seed % 100) * 0.03;
let lastW = 0, lastH = 0;
const syncSize = () => {
const w = canvas.clientWidth, h = canvas.clientHeight;
if (w === 0 || h === 0) return false;
if (w !== lastW || h !== lastH) {
const dpr = isStatic ? 1 : (window.devicePixelRatio || 1);
canvas.width = w * dpr; canvas.height = h * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
lastW = w; lastH = h;
}
return true;
};
const draw = () => {
if (!syncSize()) { if (!isStatic) raf = requestAnimationFrame(draw); return; }
const t = ((performance.now() - t0) / 1000) * speed + seedOff;
const ang = (angle * Math.PI) / 180;
const ca = Math.cos(ang), sa = Math.sin(ang);
const data = img.data;
const soft = 0.05 + softness * 0.95;
for (let y = 0; y < SRC; y++) {
for (let x = 0; x < SRC; x++) {
const u = (x / SRC - 0.5) * 2;
const v = (y / SRC - 0.5) * 2;
const ru = u * ca - v * sa;
const rv = u * sa + v * ca;
const w1 = Math.sin(ru * freqX * 3 + t) * 0.5 + 0.5;
const w2 = Math.sin(rv * (freqY * 0.5) + t * 1.3) * 0.5 + 0.5;
const w3 = Math.sin((ru + rv) * 1.4 + t * 0.7 + amplitude) * 0.5 + 0.5;
const w4 = Math.cos((ru - rv) * 2.1 + t * 1.1) * 0.5 + 0.5;
const sum = w1 + w2 + w3 + w4 + 0.0001;
const r = (palette[0][0] * w1 + palette[1][0] * w2 + palette[2][0] * w3 + palette[3][0] * w4) / sum;
const g = (palette[0][1] * w1 + palette[1][1] * w2 + palette[2][1] * w3 + palette[3][1] * w4) / sum;
const b = (palette[0][2] * w1 + palette[1][2] * w2 + palette[2][2] * w3 + palette[3][2] * w4) / sum;
const k = 1 - blend * 0.5;
const i = (y * SRC + x) * 4;
data[i] = r * k * soft + (1 - soft) * 255 * 0.5;
data[i + 1] = g * k * soft + (1 - soft) * 255 * 0.5;
data[i + 2] = b * k * soft + (1 - soft) * 255 * 0.5;
data[i + 3] = 255;
}
}
oc.putImageData(img, 0, 0);
ctx.imageSmoothingEnabled = true;
ctx.clearRect(0, 0, lastW, lastH);
ctx.drawImage(off, 0, 0, lastW, lastH);
if (!isStatic) raf = requestAnimationFrame(draw);
};
if (isStatic) {
draw();
const ro = new ResizeObserver(draw);
ro.observe(canvas);
return () => ro.disconnect();
}
raf = requestAnimationFrame(draw);
const ro = new ResizeObserver(() => syncSize());
ro.observe(canvas);
return () => { cancelAnimationFrame(raf); ro.disconnect(); };
}, [color1, color2, color3, color4, seed, speed, freqX, freqY, angle, amplitude, softness, blend, isStatic]);
const style = { ...(props.style || {}), position: 'relative', overflow: 'hidden' };
return (
<div data-id={props['data-id']} data-name={props['data-name']} style={style}>
<canvas ref={canvasRef} style={{ width: '100%', height: '100%', display: 'block' }} />
</div>
);
}
export default withResponsiveProps(WaveGradient);Marquee with slot connections
A slot control lets the user connect their own canvas nodes as items; the strip scrolls them in a seamless loop.
'use client';
/** @label "Marquee" */
/** @comment "A strip of connected nodes that scrolls continuously in a seamless loop. Connect canvas nodes as items." */
/** @controls {
"children": { "type": "slot", "label": "Items", "slotMax": "infinite" },
"speed": { "type": "slider", "label": "Speed", "min": 10, "max": 300, "step": 10, "default": 60 },
"direction": { "type": "select", "label": "Direction", "default": "left", "options": [
{ "label": "Left", "value": "left" },
{ "label": "Right", "value": "right" },
{ "label": "Up", "value": "up" },
{ "label": "Down", "value": "down" }
]},
"gap": { "type": "slider", "label": "Gap", "min": 0, "max": 160, "step": 4, "default": 32 },
"pauseOnHover": { "type": "toggle", "label": "Pause on Hover", "default": true },
"draggable": { "type": "toggle", "label": "Draggable", "default": false },
"fade": { "type": "group", "label": "Edge Fade", "controls": {
"fadeEdges": { "type": "toggle", "label": "Fade Edges", "default": false },
"fadeSize": { "type": "slider", "label": "Fade Size", "min": 0, "max": 240, "step": 8, "default": 64 }
}}
} */
import { useRef, useEffect, useState, useLayoutEffect, Children, cloneElement, isValidElement } from 'react';
import { withResponsiveProps, useStaticCanvas } from '@revyme/runtime';
function Marquee({
speed = 60, direction = 'left', gap = 32, pauseOnHover = true,
draggable = false, fadeEdges = false, fadeSize = 64, children, ...props
}) {
const boxRef = useRef(null);
const trackRef = useRef(null);
const setRef = useRef(null);
const isEmpty = Children.count(children) === 0;
const vertical = direction === 'up' || direction === 'down';
const isStatic = useStaticCanvas();
const [copies, setCopies] = useState(isStatic ? 1 : 4);
useLayoutEffect(() => {
if (isStatic) return;
const box = boxRef.current, set = setRef.current;
if (!box || !set) return;
const sizeProp = vertical ? 'offsetHeight' : 'offsetWidth';
const boxSize = box[sizeProp], setSize = set[sizeProp];
if (setSize <= 0) return;
const needed = Math.min(40, Math.max(2, Math.ceil(boxSize / setSize) + 1));
if (needed !== copies) setCopies(needed);
}, [vertical, isStatic, children, copies]);
useEffect(() => {
const box = boxRef.current, track = trackRef.current, set = setRef.current;
if (!box || !track || !set) return;
Array.from(track.children).forEach(function (oneSet) {
Array.from(oneSet.children).forEach(function (c) {
const s = c.style;
s.position = 'relative';
s.left = 'auto'; s.top = 'auto'; s.right = 'auto'; s.bottom = 'auto';
s.margin = '0'; s.flex = '0 0 auto';
});
});
if (isStatic) return;
const sizeProp = vertical ? 'offsetHeight' : 'offsetWidth';
const span = set[sizeProp];
const sign = (direction === 'right' || direction === 'down') ? 1 : -1;
let offset = sign === 1 ? -span : 0;
let last = performance.now();
let paused = false, dragging = false, raf = 0;
const apply = function () {
track.style.transform = vertical
? 'translateY(' + offset + 'px)'
: 'translateX(' + offset + 'px)';
};
function tick(now) {
const dt = (now - last) / 1000;
last = now;
if (!paused && !dragging && span > 0) {
offset += sign * speed * dt;
if (offset <= -span) offset += span;
if (offset >= 0) offset -= span;
apply();
}
raf = requestAnimationFrame(tick);
}
raf = requestAnimationFrame(tick);
const enter = function () { if (pauseOnHover) paused = true; };
const leave = function () { paused = false; };
box.addEventListener('mouseenter', enter);
box.addEventListener('mouseleave', leave);
let down = false, startPos = 0, startOffset = 0;
const onDown = function (e) {
if (!draggable) return;
down = true; dragging = true;
startPos = vertical ? e.clientY : e.clientX;
startOffset = offset;
};
const onMove = function (e) {
if (!down || span <= 0) return;
offset = startOffset + ((vertical ? e.clientY : e.clientX) - startPos);
while (offset <= -span) offset += span;
while (offset >= 0) offset -= span;
apply();
};
const onUp = function () { down = false; dragging = false; };
if (draggable) {
box.addEventListener('pointerdown', onDown);
window.addEventListener('pointermove', onMove);
window.addEventListener('pointerup', onUp);
}
return function () {
cancelAnimationFrame(raf);
box.removeEventListener('mouseenter', enter);
box.removeEventListener('mouseleave', leave);
box.removeEventListener('pointerdown', onDown);
window.removeEventListener('pointermove', onMove);
window.removeEventListener('pointerup', onUp);
track.style.transform = '';
};
}, [speed, direction, gap, pauseOnHover, draggable, children, copies, isStatic]);
if (isEmpty) {
return (
<div
data-id={props['data-id']}
data-name={props['data-name']}
style={{
position: 'relative', ...props.style, boxSizing: 'border-box',
display: 'flex', flexDirection: 'column', alignItems: 'center',
justifyContent: 'center', gap: '8px', padding: '20px', textAlign: 'center',
background: '#141414', border: '1px dashed rgba(255,255,255,0.14)',
}}
>
<div style={{ fontWeight: 700, fontSize: '15px', color: '#e5e5e5' }}>Connect Content</div>
<div style={{ fontSize: '13px', color: '#8a8a8a' }}>Add items to scroll in the marquee</div>
</div>
);
}
const safeFade = 'min(' + fadeSize + 'px, calc(50% - 1px))';
const fadeMask = fadeEdges
? 'linear-gradient(' + (vertical ? 'to bottom' : 'to right') +
', transparent, #000 ' + safeFade + ', #000 calc(100% - ' + safeFade + '), transparent)'
: undefined;
const setStyle = {
display: 'flex', flexDirection: vertical ? 'column' : 'row', alignItems: 'center',
gap: gap + 'px',
paddingRight: vertical ? 0 : gap + 'px',
paddingBottom: vertical ? gap + 'px' : 0,
flex: '0 0 auto',
};
return (
<div
ref={boxRef}
data-id={props['data-id']}
data-name={props['data-name']}
style={{
position: 'relative', ...props.style, overflow: 'hidden',
display: 'flex', alignItems: 'center',
justifyContent: vertical ? 'center' : 'flex-start',
cursor: draggable ? 'grab' : 'default',
maskImage: fadeMask, WebkitMaskImage: fadeMask,
}}
>
<div
ref={trackRef}
style={{ display: 'flex', flexDirection: vertical ? 'column' : 'row', alignItems: 'center', willChange: 'transform' }}
>
{Array.from({ length: copies }).map(function (_, copyIdx) {
if (copyIdx === 0) {
return (
<div key="orig" ref={setRef} data-set="true" style={setStyle}>
{children}
</div>
);
}
return (
<div key={copyIdx} aria-hidden="true" style={setStyle}>
{Children.map(children, function (child, i) {
if (!isValidElement(child)) return child;
return cloneElement(child, {
key: copyIdx + ':' + i,
'data-id': undefined,
'data-canvas-node': undefined,
'data-name': undefined,
});
})}
</div>
);
})}
</div>
</div>
);
}
export default withResponsiveProps(Marquee);Rotating scroll text
A 3D cylinder of words that rotates as the page scrolls.
'use client';
/** @label "Rotating 3D Text" */
/** @comment "A 3D cylinder of words that rotates with scroll. Words alternate between two colors." */
/** @controls {
"words": { "type": "text", "label": "Words (comma separated)", "default": "INNOVATE,CREATE,DESIGN,BUILD,SCALE,GROW" },
"perspective": { "type": "slider", "label": "Perspective", "min": 200, "max": 2000, "default": 500, "step": 50 },
"fontSize": { "type": "slider", "label": "Font Size", "min": 24, "max": 200, "default": 96, "step": 4 },
"textColor": { "type": "color", "label": "Text Color", "default": "#EC4899" },
"accentColor": { "type": "color", "label": "Accent Color", "default": "#ffffff" },
"fontFamily": { "type": "text", "label": "Font", "default": "Inter, sans-serif" }
} */
import { useEffect, useRef, useMemo } from 'react';
import { withResponsiveProps } from '@revyme/runtime';
function RotatingText3D({
words = 'INNOVATE,CREATE,DESIGN,BUILD,SCALE,GROW',
perspective = 500, fontSize = 96,
textColor = '#EC4899', accentColor = '#ffffff',
fontFamily = 'Inter, sans-serif',
...props
}) {
const ringRef = useRef(null);
const wordList = useMemo(() => {
const list = words.split(',').map(w => w.trim()).filter(Boolean);
return list.length >= 2 ? list : [...list, 'TEXT'];
}, [words]);
const angleStep = 360 / wordList.length;
const radius = (fontSize * 1.5) / (2 * Math.tan(Math.PI / wordList.length));
useEffect(() => {
const ring = ringRef.current;
if (!ring) return;
let currentRotation = 0;
let lastScrollY = window.scrollY;
const onScroll = () => {
const scrollY = window.scrollY;
const delta = scrollY - lastScrollY;
lastScrollY = scrollY;
currentRotation += delta * 0.1;
ring.style.transform = 'rotateX(' + currentRotation + 'deg)';
};
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
return (
<div
{...props}
style={{
position: 'relative', overflow: 'hidden',
perspective: perspective + 'px',
transformStyle: 'preserve-3d',
...(props.style || {}),
}}
>
<div
ref={ringRef}
style={{
position: 'absolute', inset: 0,
transformStyle: 'preserve-3d',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}
>
{wordList.map((word, i) => (
<div
key={i}
style={{
position: 'absolute', left: '50%', top: '50%',
transformOrigin: '50% 50%', backfaceVisibility: 'hidden',
whiteSpace: 'nowrap',
fontSize: fontSize + 'px', fontFamily, fontWeight: 800,
color: i % 2 === 0 ? textColor : accentColor,
WebkitTextStroke: '1px black',
transform: 'translate(-50%, -50%) rotateX(' + (i * angleStep) + 'deg) translateZ(' + radius + 'px)',
}}
>
{word}
</div>
))}
</div>
</div>
);
}
export default withResponsiveProps(RotatingText3D);