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.

The code component editor with code, live preview, and controls

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, a label, and a default, 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

TypeWhat it isExtra options
slidera numeric slidermin, max, step, unit
numbera number inputmin, max, step, unit
colora color picker
texta text inputplaceholder
selecta dropdownoptions: [{ label, value }]
togglean on/off switch
uploadupload a file (image, model, sequence)accept, multiple
slotconnect canvas elements in as childrenslotMax: 1, a number, or "infinite"
groupa button that opens a popup of nested controlscontrols
transitiona 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.

Design
Build
Ship
Scale
Iterate
Launch
Refine
'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.

INNOVATE
CREATE
DESIGN
BUILD
SCALE
GROW
'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);