Premium open source motion components for Remotion. Fourteen scenes that power the launch video templates at curvable.ai, drop in ready, MIT licensed.
![]() Floating stack |
![]() Prompt input |
![]() Stats grid |
Live previews and prop knobs: curvable.ai/library
Every Remotion component in this library follows the same contract:
- Deterministic on frame. Render the same input at the same frame and you get the same pixels, every time. Works in
<Player>for browser preview and inrenderMediafor headless MP4 output without any conditional code paths. - One brand color knob. Pass a single
primaryhex and every shade the component needs is derived for you in HSL space. Hue stays locked, lightness and saturation shift, so a non orange primary still produces a coherent palette. - Frozen defaults. Each component exports a
Defaultsobject you can spread intoinputProps, plus aMetaobject with width, height and fps, plus acompute<Name>Durationhelper for the variable length scenes.
No agent in your pipeline has to invent timing or colors. Pass a string in, get pixels out.
npm install @curvable/motion remotion reactTwo optional peer dependencies extend the surface:
oglis the WebGL micro-runtime that drives theGrainientBgshader. Install it if you plan to useGrainientBg,FloatingStack,PromptInputorStatsGrid(all four use a Grainient backdrop).@remotion/google-fontsships the Geist + Geist Mono webfonts used by thePromptInputscene's mini dashboard text.
npm install ogl @remotion/google-fontsThe PromptInput scene also renders an Apple Intelligence style glow behind the preview pane via CanvasKit. For that to work at runtime, host canvaskit.js and canvaskit.wasm at the root of your site (the source loads them from /canvaskit.js and /canvaskit.wasm). Copy them from canvaskit-wasm into your public folder, or comment out the glow if you don't need it.
import { Player } from '@remotion/player';
import { Typewriter, TypewriterDefaults, TypewriterMeta, computeTypewriterDuration } from '@curvable/motion';
export function Preview() {
return (
<Player
component={Typewriter}
inputProps={{ ...TypewriterDefaults, text: 'Ship faster.', primary: '#6633CC' }}
durationInFrames={computeTypewriterDuration({
text: 'Ship faster.',
framesPerChar: TypewriterDefaults.framesPerChar,
fadeFrames: TypewriterDefaults.fadeFrames,
startDelayFrames: TypewriterDefaults.startDelayFrames,
holdFrames: TypewriterDefaults.holdFrames,
})}
fps={TypewriterMeta.fps}
compositionWidth={TypewriterMeta.width}
compositionHeight={TypewriterMeta.height}
autoPlay
loop
controls={false}
/>
);
}For headless renders, the same component slots into renderMedia:
import { renderMedia } from '@remotion/renderer';
import { TypewriterMeta, computeTypewriterDuration, TypewriterDefaults } from '@curvable/motion';
await renderMedia({
composition: {
id: 'typewriter',
width: TypewriterMeta.width,
height: TypewriterMeta.height,
fps: TypewriterMeta.fps,
durationInFrames: computeTypewriterDuration({ ...TypewriterDefaults }),
defaultProps: { ...TypewriterDefaults, text: 'Ship faster.' },
},
serveUrl: bundled,
codec: 'h264',
outputLocation: 'out.mp4',
});Fourteen scenes across three categories.
Larger scenes that combine a backdrop, a foreground composition, and timing into one launch tile sized render. All three derive every accent shade (cursor, slab glows, card highlights, bulb halo) from one primary knob.
Each component exports five things. Using Typewriter as the canonical example:
import {
Typewriter, // React.FC<TypewriterProps>
type TypewriterProps, // Public props type
TypewriterDefaults, // Frozen default values
TypewriterMeta, // { width, height, fps, durationFrames? }
computeTypewriterDuration, // (params) => number of frames
} from '@curvable/motion';For components with a single fixed loop length, Meta.durationFrames is the number you want. For variable length scenes (typewriter, cascading text, etc.), call compute<Name>Duration(params) with the same params you pass as inputProps and it returns the total frame count, including all reveal and hold phases.
<Name>Defaults ships pre-tuned values for every prop. Spread them and override only what you need:
<Player
component={CascadingText}
inputProps={{
...CascadingTextDefaults,
text: 'Your custom text goes here.',
primary: '#6633CC',
}}
durationInFrames={computeCascadingTextDuration({
text: 'Your custom text goes here.',
fadeDuration: CascadingTextDefaults.fadeDuration,
})}
fps={CascadingTextMeta.fps}
compositionWidth={CascadingTextMeta.width}
compositionHeight={CascadingTextMeta.height}
autoPlay
loop
controls={false}
/>Most components accept a single primary hex and derive every dependent shade in HSL space. This is the same recipe the Grainient component uses, exposed as deriveGrainientPalette(primary) so you can match accents across scenes that do not share a registry:
import { deriveGrainientPalette } from '@curvable/motion';
const { color1, color2, color3 } = deriveGrainientPalette('#6633CC');Components that need light versus dark backgrounds also accept a lightMode: boolean prop. The frozen palettes in palettes.ts adjust shade lightness and contrast for both surfaces.
Spin up the registry locally to tweak knobs in your browser:
Each preset card hovers to play and clicks through to a tweak page with every knob exposed, plus the source TSX.
MIT. Copyright (c) 2026 Curvable.
You can use, fork and ship these in commercial products, branded videos, agency work, anything. Attribution welcome but not required.
Curvable, an AI launch video generator for SaaS founders. From URL to MP4 in minutes.













