Skip to main content

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);