Remotion for Rails. Make videos from your app's own components and data, written in Ruby — no React, no video editor, no After Effects.
Remotion made it possible to build videos in React. AnimateIt brings the same idea to Ruby on Rails — without React, a JavaScript project, or a video editor.
You describe a video as a Ruby class and a HAML or ERB template, preview it in a bundled Studio UI, and render it to MP4/WebM/MOV/GIF. The declarative runtime renders the template once per structural layer, records compact animation tracks, and seeks those tracks in the browser. You still get your app's own components, styles, fonts, and real data without asking Rails to render every frame.
It's built for indie hackers and Rails developers who want to promote their projects with polished product demos, launch clips, and social ads — reusing the UI they've already built, staying in Ruby, and skipping the whole "learn a video editor" detour.
Requirements
- Ruby >= 3.3, Rails >= 7.2 (tested against Rails 7.2 and 8.1)
- FFmpeg on the PATH (rendering)
- Node + Playwright with a Chromium build (rendering):
npx playwright install chromium - HAML (composition sidecar templates + Studio views)
Playwright and FFmpeg are only needed when you actually render; the gem loads Playwright lazily so production boot / asset precompile never touch it.
Installation
# Gemfile
gem "animate_it"
bundle install
bin/rails generate animate_it:install
The generator adds config/initializers/animate_it.rb and mounts the Studio in
config/routes.rb (development/test only):
mount AnimateIt::Engine, at: AnimateIt.config.mount_path if Rails.env.local?
Visit http://localhost:3000/animate_it to open the Studio.
Writing a composition
Compositions live in app/videos/. They auto-register (via the id "..." DSL)
and reload in development.
# app/videos/hello_video.rb
class HelloVideo < AnimateIt::Composition
id "hello"
client_driven!
fps 30
size 1080, 1080
duration 3.seconds
props do
integer :counter_start, default: 0
end
verification_props({}, { counter_start: 100 })
assets_dir "app/assets/images/videos"
output_basename "hello"
outputs do
mp4
gif
png frame: 45
end
beat :intro, at: 0, length: 45
class Scene < AnimateIt::Scene
track_vars :root do
{ title_opacity: at_act(:intro, [0, 30], [0, 1]) }
end
text_track(:counter) { (props[:counter_start] + local_frame).to_s }
def body
absolute_fill(vars: :root) do
safe_join([render_scene_template("canvas"), animate_text(:counter)])
end
end
end
scene Scene
end
-# app/videos/hello_video/canvas.html.haml
:css
.title { opacity: var(--title-opacity, 1); font-size: 6rem; font-weight: 800; }
.stage
.title Hello 👋
Animation helpers (at_global, at_act, beat_frame, beat_range), sampled
CSS variables (track_vars), dynamic text (text_track), named beats, and
audio / audio_loop tracks are available. Compositions can render your app's
real partials and ViewComponents — see AnimateIt.config.render_stylesheets
below.
Browser playback runtime
client_driven! switches export and Studio playback to /player. Rails renders
each structural layer once and records a schema-v2 track document; then
window.__animateIt.setFrame(frame) updates CSS variables, text, layer
visibility, word reveals, and native CSS/Web Animations without another frame
request.
Use structure_epochs only when a template's DOM shape changes. Each epoch adds
one pre-rendered layer. Track schema v2 scopes bindings to their timeline segment;
the player still accepts v1 documents and rejects unknown versions.
structure_epochs 60, 120
class Scene < AnimateIt::Scene
track_vars(:card) { { x: "#{at_global([0, 30], [-20, 0])}px" } }
text_track(:score) { (progress * 100).round }
def body
absolute_fill(vars: :card) do
safe_join([render_scene_template("canvas"), animate_text(:score)])
end
end
end
Production frontend playback
Public playback is opt-in per composition. public_player! enables the
standalone browser clock, Play/Pause control, looping, and synchronized
music/voice/SFX. It does not expose Studio, frame, filmstrip, props, or render
endpoints, and public playback always uses the composition's default props.
class HelloVideo < AnimateIt::Composition
id "hello"
public_player! autoplay: false, loop: true
# ...
end
Mount the engine in every environment, then embed the allowlisted composition from a host view:
# config/routes.rb
mount AnimateIt::Engine, at: AnimateIt.config.mount_path
<%= animate_it_player "hello", title: "Hello product demo" %>
The iframe renders each structural layer once and advances entirely in the browser. Audio-capable players fall back to the visible Play button when the browser blocks autoplay.
HAML or ERB
Scene sidecar templates can be authored in HAML (canvas.html.haml) or
ERB (canvas.html.erb) — Rails resolves whichever file exists, so you can
mix engines across scenes. The HAML canvas above is equivalent to:
<%# app/videos/hello_video/canvas.html.erb %>
<style>.title { opacity: var(--title-opacity, 1); font-size: 6rem; font-weight: 800; }</style>
<div class="stage">
<div class="title">Hello 👋</div>
</div>
The gem bundles HAML for its own Studio UI, so you never need to add HAML to your app to use it — ERB-only apps work out of the box.
Rendering
The renderer needs your Rails server running. It drives a real browser
against /player for client-driven compositions and the legacy /filmstrip
endpoint for other compositions.
# start the app
bin/rails server
# in another shell — render a composition's declared outputs
bin/rails 'animate_it:render[hello]'
bin/rails 'animate_it:render[hello,0..45]' # just a frame range
# compare /player with the legacy /filmstrip (step 1 checks every frame)
bin/rails 'animate_it:verify[hello,1]'
bin/rails animate_it:verify_all
# or the packaged CLI (writes to tmp/animate_it/ by default)
bundle exec render_animate_it_video hello
bundle exec render_animate_it_video hello tmp/hello.mp4
You can also render from the Studio UI: open a composition, scrub the timeline, and click Render Video to watch progress live.
Host resolution
The renderer points the browser at ANIMATE_IT_HOST (falling back to the legacy
RAILS_MOTION_HOST, then RAILS_HOST, then http://127.0.0.1:3000):
ANIMATE_IT_HOST=http://127.0.0.1:3001 bundle exec render_animate_it_video hello
Configuration
# config/initializers/animate_it.rb
AnimateIt.configure do |config|
# Where the Studio mounts (development/test only). Default "/animate_it".
config.mount_path = "/animate_it"
# Host stylesheets to inject into every rendered frame. Only needed when your
# compositions re-use host partials/components that expect their CSS. Names
# are passed to `stylesheet_link_tag`. Default [].
config.render_stylesheets = %w[application components/star-ratings]
end
When one composition declares multiple formats for the same frame range, AnimateIt captures the ordered Chromium frames once and reuses them for each encoder. Studio rendering intentionally keeps one sequential capture stream so frame ordering, progress, and cancellation stay deterministic.
Deterministic asset preflight
Host applications can record local composition inputs in
config/animate_it_assets.yml. bin/rails animate_it:preflight checks that each
file exists, matches its SHA-256 checksum, and has a provenance provider. Media
stays in the host app and is not packaged in the gem.
version: 1
assets:
- path: app/audio/launch/music.mp3
kind: music
compositions: [hello]
sha256: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
provenance:
provider: elevenlabs
generated_with_ai: true
model_id: not-recorded
generation_id: not-recorded
For parameterized compositions, declare verification_props or override them
at the command line:
ANIMATE_IT_PROPS_JSON='{"title":"Variant"}' bin/rails 'animate_it:verify[hello,1]'
ANIMATE_IT_PROPS_MATRIX_JSON='[{}, {"title":"Variant"}]' bin/rails animate_it:verify_all
Verification requires a running server and writes comparison screenshots under
tmp/animate_it/verify.
Claude skill
If you use Claude Code, this repo ships an
Agent Skill that teaches Claude how to author AnimateIt compositions — the DSL,
the render pipeline, embedding, rendering your app's real partials, motion
craft, and the gotchas. Install it into your project's .claude/skills/:
npx animate-it-skills # copies the skill into ./.claude/skills
The skill source lives in skills/.
Development
bin/setup # bundle install
bundle exec rspec # unit + request specs (uses spec/dummy)
bundle exec rubocop
gem build animate_it.gemspec
# full render smoke test (needs ffmpeg + Playwright chromium)
RUN_RENDER_SMOKE=1 bundle exec rspec spec/rendering_spec.rb
# run the suite against a specific Rails version (see Appraisals)
bundle exec appraisal install
bundle exec appraisal rails-7.2 rspec
bundle exec appraisal rails-8.1 rspec
Specs run against a minimal host app in spec/dummy — no external services, no
database.
Credits
AnimateIt is inspired by Remotion — the framework that pioneered making videos programmatically in React. AnimateIt brings that "videos as code" idea to the Ruby on Rails ecosystem, natively and without React. Huge thanks to the Remotion team for the inspiration.
License
AnimateIt is released under the MIT License — free and open source, with no restrictions on commercial or business use. Individuals, startups, agencies, and companies of any size can use it in open-source and closed-source projects at no cost, forever. No paid tiers, no seat limits.