Overlays Reference

Every overlay type Renderly supports - fields, units, gotchas, and minimal examples. The companion to the OpenAPI spec.

This page is the human-readable companion to the OpenAPI reference. It documents every overlay type a composition can contain, calls out the units each field uses, and flags the surprises (fields that look optional but aren't, fields the renderer silently ignores).

For the literal schema and copy-pasteable examples, see POST /renders.

Fields every overlay has

These come from BaseOverlay and exist on text, video, image, sound, shape, sticker, caption, effect, and group alike.

FieldUnitPurpose
idintegerUnique numeric id within the composition.
typestringDiscriminator: "text", "video", "image", "sound", "shape", "sticker", "caption", "effect", "group".
fromframesFirst frame the overlay is visible on the timeline.
durationInFramesframesHow long the overlay stays visible.
rowintegerTimeline track index. Lower row numbers render above higher row numbers - row: 0 sits on top, row: 5 sits at the back. See Row stacking below.
top, leftpixelsPosition of the layer's top-left corner relative to the canvas.
width, heightpixelsLayer box size.
rotationdegreesRotation around the layer's center. Defaults to 0.
isDynamicbooleanMarks the overlay as a template variable.
namestringVariable key used to match the replacements object.
motionobjectOptional keyframe animation. See Motion.
layerStyleobjectOptional static CSS on the layer box (background, border, shadow, mask). See layerStyle.

Positioning cheatsheet

top and left are pixels from the top-left corner of the canvas - (0, 0) is the top-left. To pin a 300×300 element to the bottom-right of a 1080×1920 canvas with a 40 px margin:

top  = canvasHeight - height - margin   →  1920 - 300 - 40 = 1580
left = canvasWidth  - width  - margin   →  1080 - 300 - 40 =  740

Row stacking

row is the timeline track index, and it directly drives stacking order: the renderer computes zIndex = 100 - row * 10, so lower row numbers sit on top.

  • A text overlay on row: 0 renders above a video overlay on row: 1 - the video becomes the background for the text.
  • Background video / full-frame effects belong on the highest row numbers (e.g. row: 4 or row: 5).
  • Headlines, CTAs, and anything that should sit in front of everything else belong on row: 0 or row: 1.
  • Give each visually-stacked overlay its own row. Two overlays on the same row will collide - they share a z-index, and the order falls back to the array order, which is fragile.

A safe convention for a full composition: row: 5 audio · row: 4 background video/effect · row: 3 scrim/dim shape · row: 2 decorative graphics · row: 1 body text · row: 0 headline.

text

{
  "id": 1, "type": "text",
  "content": "Hello world",
  "from": 0, "durationInFrames": 90,
  "row": 1, "top": 800, "left": 60, "width": 960, "height": 200,
  "styles": {
    "fontSize": "4rem", "fontWeight": "900",
    "color": "#FFFFFF", "textAlign": "center",
    "animation": { "enter": "fade", "exit": "fade", "enterDuration": 0.4 }
  }
}
  • content is the rendered string. When isDynamic is true, this is what replacements swaps.
  • styles.fontSize accepts any CSS size string ("3rem", "48px").
  • styles.animation - preset enter/exit. enterDuration / exitDuration are in seconds.
  • styles.animatedText, styles.typeWriter, styles.counterMode, styles.codeBlock - alternate text modes, each with enabled: true to activate. See the editor doc for what each does.

video

{
  "id": 0, "type": "video",
  "src": "https://example.com/clip.mp4",
  "from": 0, "durationInFrames": 180,
  "row": 0, "top": 0, "left": 0, "width": 1080, "height": 1920,
  "videoStartTime": 2,
  "speed": 1,
  "styles": {
    "objectFit": "cover", "opacity": 1
  }
}
  • src - public URL of the source media. Replaced by replacements if isDynamic is true.
  • videoStartTime - in-point of the source clip in seconds. Skips the first N seconds of the source.
  • speed - playback speed multiplier. 2 plays twice as fast; 0.5 plays at half speed.
  • mediaSrcDuration - total length of the source media in seconds. Helps the editor compute trim boundaries.
  • segments - ordered list of { startFrame, endFrame, speed? } slices to chain together. When set, videoStartTime is ignored. Frames are absolute source frames.
  • greenscreen - chroma-key removal. { enabled: true, sensitivity, threshold: { red, green, blue }, smoothing, spill }.
  • styles.volume - 0-1. Defaults to the source's natural volume; set to 0 to mute.
  • styles.cropEnabled / cropX / cropY / cropWidth / cropHeight - percentages (0-100) of the source frame to display. Set cropEnabled: true to apply.

image

{
  "id": 2, "type": "image",
  "src": "https://example.com/photo.jpg",
  "from": 0, "durationInFrames": 90,
  "row": 1, "top": 200, "left": 100, "width": 400, "height": 400,
  "styles": {
    "objectFit": "cover", "borderRadius": "16px", "opacity": 0.85
  }
}
  • styles.opacity - 0-1. Combined with the composition's backgroundColor, this is how you tint or darken an image.
  • greenscreen - same shape as video.
  • styles.layout3D.layout - tilt the image into 3D. One of none, tilt-up, tilt-down, card-left, card-right, book, floating, billboard, skewed. See 3D layouts below.
  • styles.layout3D.settle - true makes the tilt an animation instead of a fixed pose: the image arrives at the full angle and rotates to flat. See 3D layouts.
  • styles.animation - enter/exit preset with optional duration in seconds.
  • Crop fields are the same as video.

3D layouts

layout3D is the only depth control, and it works on image overlays only. On its own it is a fixed pose - the image holds the same tilt for its whole life:

{ "styles": { "layout3D": { "layout": "card-right" } } }

Add "settle": true and the tilt becomes a move. The image arrives at the preset's full angle and rotates to flat over its entry animation, landing squarely on its top/left/width/height box. This is what you want for a screenshot, product card or device frame that should arrive in 3D rather than sit in 3D:

{
  "id": 4, "type": "image",
  "src": "https://example.com/screenshot.png",
  "from": 0, "durationInFrames": 120,
  "row": 2, "top": 150, "left": 1120, "width": 520, "height": 1040,
  "styles": {
    "borderRadius": "62px",
    "layout3D": { "layout": "card-right", "settle": true },
    "animation": {
      "enter": "slideLeft",
      "enterDuration": 1.15,
      "enterEasing": "easeOutCubic"
    }
  }
}

The rotation is timed by animation.enterDuration (seconds) and shaped by animation.enterEasing, so it runs together with whatever entry animation you pick. easeOutCubic over 1.0-1.4 s reads well; anything under ~0.6 s is over before the eye registers it.

Pick the preset by which way the image should face. card-left angles the face to the right (use it on the left of frame), card-right angles it to the left (use it on the right of frame), and floating is a gentle all-purpose tilt. book and skewed are extreme and only work on small boxes - under roughly 600 px wide.

Without settle, the pose is static, so pair it with styles.animation.during (sway or float) to keep it from reading as a still. With settle, keep during subtle - an intensity around 0.12 is enough once the rotation is doing the work.

sound

{
  "id": 3, "type": "sound",
  "src": "https://example.com/music.mp3",
  "from": 0, "durationInFrames": 180,
  "row": 2, "top": 0, "left": 0, "width": 0, "height": 0,
  "startFromSound": 5,
  "speed": 1,
  "mediaSrcDuration": 145,
  "styles": {
    "volume": 0.8, "fadeIn": 1, "fadeOut": 1.5
  }
}

Sound overlays still need top / left / width / height (they come from the shared BaseOverlay), but they're ignored at render time. Set them to 0.

  • startFromSound - in-point of the source audio in seconds. Use this to skip an intro.
  • speed - playback speed multiplier.
  • styles.fadeIn, styles.fadeOut - fade durations in seconds.
  • styles.volume - 0-1.

shape

{
  "id": 4, "type": "shape",
  "content": "rectangle",
  "from": 0, "durationInFrames": 90,
  "row": 1, "top": 100, "left": 100, "width": 200, "height": 200,
  "styles": {
    "fill": "#FF0080", "borderRadius": "16px",
    "animation": { "enter": "scale", "exit": "fade" }
  }
}
  • content - shape identifier. One of "rectangle", "circle", "triangle", "star", "diamond", "hexagon", "arrow", "line". Unknown values fall back to "rectangle".
  • styles.gradient - single CSS gradient string.
  • styles.gradientTransition - animated transition between multiple gradient strings.

sticker

{
  "id": 5, "type": "sticker",
  "content": "discount-50",
  "category": "Discounts",
  "from": 30, "durationInFrames": 90,
  "row": 2, "top": 500, "left": 740, "width": 300, "height": 300,
  "styles": { "scale": 1, "animation": { "enter": "bounce" } }
}
  • content - sticker identifier from the sticker library.
  • category - one of "Shapes", "Discounts", "Emojis", "Reviews", "Default".
  • styles.scale - uniform scale multiplier.

caption

{
  "id": 6, "type": "caption",
  "captions": [
    {
      "text": "Hello world",
      "startMs": 0, "endMs": 1200,
      "timestampMs": null, "confidence": 0.97,
      "words": [
        { "word": "Hello", "startMs": 0,   "endMs": 500,  "confidence": 0.98 },
        { "word": "world", "startMs": 600, "endMs": 1200, "confidence": 0.96 }
      ]
    }
  ],
  "from": 0, "durationInFrames": 90,
  "row": 3, "top": 1500, "left": 60, "width": 960, "height": 200,
  "styles": {
    "fontSize": "3rem", "color": "#FFFFFF",
    "textAlign": "center",
    "highlightStyle": { "color": "#FFD700", "scale": 1.1 }
  }
}

Caption timing is in milliseconds and is interpreted at 30 FPS regardless of the composition's fps. If you build captions at 60 FPS the words will fall out of sync.

  • captions[] carries one entry per phrase, each with word-level timing.
  • styles.highlightStyle is what the currently-spoken word looks like - karaoke-style emphasis.
  • template - an optional named style template (set by the editor).

effect

{
  "id": 7, "type": "effect",
  "content": "confetti",
  "from": 0, "durationInFrames": 180,
  "row": 4, "top": 0, "left": 0, "width": 1080, "height": 1920,
  "styles": { "particleCount": 200, "speed": 1, "size": 8 }
}
  • content is the effect preset: one of "snow", "confetti", "rain", "fireflies", "sparkles", "bubbles", "matrix", "grid-stagger", "fracture", "scrolling-columns".
  • Each preset takes its own subset of styles. Matrix effects use matrixFontSize / matrixColor / matrixDensity / matrixCharset; grid effects use gridCols / gridRows / cellColors / cellImages / staggerDelay; scrolling columns use columns[] / columnGap / imageHeight.

Motion: keyframes, groups, and beats

The styles.animation presets give an overlay one enter, one exit and one loop. For motion-graphics work - camera moves, whip transitions, counters, hits on the music - every overlay also takes keyframe tracks, and a group overlay moves many overlays as one. Everything here is optional: an overlay without these fields renders exactly as before.

This example is a complete 3-second inputProps. A scene group pushes in slowly, the headline whips in with a motion blur, a counter steps up, and a chip hits on beat 2 of a 120 BPM grid:

{
  "width": 1920, "height": 1080, "fps": 30, "durationInFrames": 90,
  "backgroundColor": "#0A0A0A",
  "beatGrid": { "bpm": 120, "offsetSec": 0 },
  "overlays": [
    {
      "id": 1, "type": "group", "name": "scene-1",
      "from": 0, "durationInFrames": 90,
      "row": 1, "top": 0, "left": 0, "width": 1920, "height": 1080,
      "layerStyle": { "background": "radial-gradient(circle at 30% 25%, #2a1f5c, #0a0a0a 65%)" },
      "motion": { "tracks": { "scale": [{ "t": 0, "v": 1 }, { "t": 90, "v": 1.05, "ease": "sine.inOut" }] } },
      "children": [
        {
          "id": 2, "type": "text", "name": "headline", "isDynamic": true,
          "content": "Launch day",
          "from": 0, "durationInFrames": 90,
          "row": 0, "top": 360, "left": 160, "width": 1600, "height": 200,
          "styles": { "fontFamily": "Inter", "fontSize": "150px", "fontSizeFixed": true, "fontWeight": "800", "color": "#FFFFFF", "textAlign": "left", "lineHeight": 1 },
          "motion": {
            "tracks": {
              "x": [{ "t": 0, "v": 240 }, { "t": 12, "v": 0, "ease": "expo.out" }],
              "opacity": [{ "t": 0, "v": 0 }, { "t": 4, "v": 1 }],
              "blurX": [{ "t": 0, "v": 30 }, { "t": 12, "v": 0, "ease": "expo.out" }]
            }
          }
        },
        {
          "id": 3, "type": "text",
          "content": "0",
          "from": 15, "durationInFrames": 75,
          "row": 1, "top": 600, "left": 160, "width": 700, "height": 120,
          "styles": { "fontFamily": "JetBrains Mono", "fontSize": "96px", "fontSizeFixed": true, "fontWeight": "500", "color": "#A3A3A3", "textAlign": "left", "lineHeight": 1 },
          "motion": {
            "content": [{ "t": 0, "v": "0" }, { "t": 6, "v": "12,480" }, { "t": 12, "v": "48,210" }, { "t": 18, "v": "100,000" }],
            "tracks": { "opacity": [{ "t": 0, "v": 0 }, { "t": 4, "v": 1 }] }
          }
        },
        {
          "id": 4, "type": "text",
          "content": "videos rendered",
          "from": 15, "durationInFrames": 75,
          "row": 2, "top": 780, "left": 160, "width": 520, "height": 90,
          "layerStyle": { "background": "#6C47FF", "borderRadius": "45px", "boxShadow": "0 20px 60px rgba(108,71,255,.45)" },
          "styles": { "fontFamily": "Inter", "fontSize": "40px", "fontSizeFixed": true, "fontWeight": "600", "color": "#FFFFFF", "textAlign": "center", "lineHeight": 1 },
          "motion": {
            "origin": "0% 50%",
            "tracks": {
              "opacity": [{ "t": 0, "v": 0 }, { "t": 4, "v": 1 }],
              "scale": [{ "t": 0, "v": 1 }, { "t": "beat:2", "v": 1.15, "ease": "step" }, { "t": "beat:2.5", "v": 1, "ease": "power4.out" }]
            }
          }
        }
      ]
    }
  ]
}

Send it as inputProps to POST /renders (or POST /previews first - it is free). replacements work here too: { "headline": "Ship it" } changes the headline inside the group.

Keyframe tracks: motion.tracks

Each track is an array of keys { "t", "v", "ease"? } that animates one property of the overlay's box:

TrackUnitEffect
x, y, zpixelsMove the box from its left / top position.
scale, scaleX, scaleYfactorScale around motion.origin (default: the centre).
rotatedegreesAdded to rotation.
rotateX, rotateYdegrees3D rotation. Give the parent group a styles.perspective, or set motion.perspective.
skewX, skewYdegreesSkew.
opacity0-1Multiplies the preset fades.
blurpixelsGaussian blur.
blurX, blurYpixelsDirectional motion blur - use it during fast moves.
brightness, contrast, saturatefactorColour filters.
clipPathCSS stringAnimated number by number: inset(0 100% 0 0) → inset(0 0% 0 0) is a left-to-right wipe.

How keys work:

  • t is in frames from the overlay's own from (fractions are fine), or "beat:N" - see Beats.
  • Between two keys, the value follows the later key's ease. Before the first key the value is the first key's v, and after the last key it is the last key's v. A single key is a constant.
  • ease takes GSAP names - linear, power1 to power4, sine, expo, circ, back, quad, cubic, quart, quint, each with .in, .out or .inOut (no suffix means .out), and back.out(1.7) for an overshoot. The Renderly easing names from styles.animation work too. Default: linear.
  • "ease": "step" holds the previous value and jumps exactly at t. Use it for hard cuts, strobes, shakes and beat hits.
  • Values are numbers, colours (#rrggbb, rgb(), rgba()), or strings with the same numbers-and-units shape on every key.

Text that changes: motion.content

On a text overlay, motion.content replaces the text at each key - every key is a step. This is how counters and typing are built (overlay 3 above). Give the overlay styles.fontSizeFixed: true and a box wide enough for the longest value, or the size changes at every step. Do not use motion.content on an isDynamic text: its steps would replace the value sent in replacements.

layerStyle

Static CSS on the overlay's box: background, border, borderRadius, boxShadow, overflow (visible / hidden), mixBlendMode, isolation, backdropFilter, maskImage, clipPath, filter, and textRendering. Use it for gradient backgrounds, cards, chips (overlay 4 above), masks and blend modes, instead of stacking shape overlays.

  • Boxes are border-box: a border sits inside width / height.
  • For a bitmap, use an image overlay. A url() background is not waited for, so it can be blank on some frames.

group

A group is a container. Its children are overlays of any type, including other groups.

  • Children are positioned relative to the group box and timed relative to the group's from. A child that runs past the group's end is cut there.
  • The group's motion moves all its children together: a camera push-in, a shake, a whip to the next scene.
  • row orders children inside the group. The group's own row places the whole group among its siblings.
  • layerStyle.overflow: "hidden" on a group makes it a mask: a child that slides in from outside the box is revealed by the edge.
  • styles.perspective (pixels) gives the children one shared 3D camera; styles.preserve3d: true keeps nested 3D.
  • Ids must be unique across the whole tree, children included.
  • isDynamic overlays inside groups are template variables like any other - replacements reach them.

The editor shows a group as one timeline item and has no controls for keyframes or children. Saving a project in the editor keeps them. Do not duplicate or split a group in the editor.

Beats: beatGrid

A top-level { "bpm": 120, "offsetSec": 0 } lets keys say "t": "beat:N" instead of a frame. Beat N is at offsetSec + N × 60 / bpm seconds, and the key lands on the first frame at or after that time. To put the same video on a track with another tempo, change only bpm and offsetSec - every beat key moves with the music. from and durationInFrames stay in frames.

Render cost

Transforms and opacity are cheap. Blur tracks, backdropFilter and large 3D layers are expensive, so a dense motion video renders more slowly than a simple template. Our 15-second launch template (about 135 layers, with blur during moves) takes about 1.8× as long as a typical 15-second template.

  • Blur only while something moves, and end every blur track at 0. A finished blur track still leaves an empty filter on the box, so when the move is done, end that overlay and let an unblurred copy take over.
  • Never keep a blur on a full-frame layer.
  • Prefer a few large video clips over a wall of small ones.

Composition-level fields

These sit at the top of inputProps, not inside any overlay:

FieldUnitPurpose
width, heightpixelsCanvas size.
fpsframes per secondComposition frame rate.
durationInFramesframesTotal composition length.
backgroundColorCSS colorSolid fill behind every overlay. Defaults to "white".
overlays[]arrayThe overlays themselves.
beatGridobjectOptional { bpm, offsetSec } for "beat:N" keyframe times. See Beats.

Where to next

  • Templates - turn any overlay into a per-render variable
  • Rendering - kick off a render job and poll for the result
  • API reference - every field with its OpenAPI schema