EmpireUI
Get Pro
← Blog8 min read#theatre.js#animation#react

Theatre.js: Visual Animation Editor for React and Three.js

Theatre.js brings a timeline-based visual editor to React and Three.js animations. Here's how it actually works and when you'd want to reach for it.

Timeline editor interface showing animation keyframes on dark background

What Theatre.js Actually Is

Theatre.js is an animation library with a built-in visual editor — think a proper keyframe timeline that runs right inside your browser alongside your app. You tweak values, scrub through time, and the changes write back to your code as JSON. It's not a design tool that generates animation boilerplate. It's closer to a production-grade motion editor that ships with your project.

The project hit 0.5 in 2022 and has been gaining serious traction in the creative development space ever since. If you've ever spent 45 minutes tweaking easing curves in code, refreshing the browser, adjusting by 0.02, and repeating — you'll immediately understand the pitch. That loop is genuinely painful, and Theatre.js kills it.

Honestly, it's the kind of tool that looks like a toy until you see someone use it on a real Three.js scene. Then it clicks. You get a full timeline with scrubbing, grouped objects, per-property curves, and playback controls — all without leaving localhost.

Worth noting: Theatre.js has two tiers. @theatre/core handles the runtime and ships to production. @theatre/studio is the editor UI and stays in dev-only mode. You don't pay an animation overhead in production builds — the studio never makes it to users.

Setting Up Theatre.js in a React Project

Installation is two packages. The studio is a devDependency; core goes in your main dependencies. That separation matters for bundle size.

npm install @theatre/core
npm install --save-dev @theatre/studio

In your app entry point, initialize the studio in dev mode only. This is the standard pattern and it's important — if you skip the condition, the studio panel bleeds into production builds.

// main.tsx or _app.tsx
import { getProject } from '@theatre/core'
import studio from '@theatre/studio'

if (process.env.NODE_ENV === 'development') {
  studio.initialize()
}

export const project = getProject('My App')

From there you create a sheet (a timeline) and sheet objects (the things you're animating). Each object holds a set of typed values — numbers, colors, booleans — and Theatre handles the interpolation between keyframes you set in the UI. One more thing — the project state (all your keyframe data) exports as a JSON file you commit to your repo. No magic cloud sync, just a file.

Animating React Components

For DOM-based animations in React, you bind sheet object values to component state using Theatre's onChange callback or via the @theatre/react bindings. The react package gives you a useCurrentSheet hook and a PairedBinding component that wires values directly to refs without re-renders — which is exactly what you want for 60fps DOM animations.

import { useCurrentSheet } from '@theatre/react'
import { val } from '@theatre/core'

function AnimatedBox() {
  const sheet = useCurrentSheet()
  const boxObj = sheet.object('Box', {
    x: 0,
    opacity: 1,
    scale: 1,
  })

  const divRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    return boxObj.onValuesChange((values) => {
      if (!divRef.current) return
      divRef.current.style.transform =
        `translateX(${values.x}px) scale(${values.scale})`
      divRef.current.style.opacity = String(values.opacity)
    })
  }, [boxObj])

  return <div ref={divRef} className="w-24 h-24 bg-purple-500" />
}

That onValuesChange callback fires on every frame during playback. You're touching the DOM directly via refs, not triggering React renders. That's intentional. In practice, this pattern handles complex multi-property animations without any jank you'd normally get from state-based approaches.

If you want something that pairs well visually with Theatre animations, check out the glassmorphism components on Empire UI — the frosted-glass cards look particularly good when you're animating entrance sequences with staggered opacity and scale.

Quick aside: Theatre's val() utility lets you read the current value synchronously outside the callback. You'd use this if you need to sample a value on a click event rather than tracking it continuously.

Theatre.js with Three.js and R3F

This is where Theatre.js really separates itself from every other animation library. Three.js scenes have hundreds of animatable properties — camera position, material color, fog density, light intensity — and tweaking them via code/refresh cycles is brutal. Theatre gives you sliders and color pickers for all of it, live, in the same browser tab.

With React Three Fiber (R3F), the integration uses @theatre/r3f. You wrap your canvas in a SheetProvider and then call editable() on any R3F component you want to control from the timeline.

import { Canvas } from '@react-three/fiber'
import { SheetProvider, editable as e } from '@theatre/r3f'
import { getProject } from '@theatre/core'

const project = getProject('3D Scene')
const sheet = project.sheet('Scene')

function Scene() {
  return (
    <Canvas>
      <SheetProvider sheet={sheet}>
        <e.mesh
          theatreKey="MyMesh"
          position={[0, 0, 0]}
        >
          <boxGeometry args={[1, 1, 1]} />
          <meshStandardMaterial color="#6366f1" />
        </e.mesh>
        <e.pointLight theatreKey="MainLight" intensity={1} />
      </SheetProvider>
    </Canvas>
  )
}

Once that's wired up, select the mesh in the Theatre studio panel and you get position, rotation, scale, and any other props as editable tracks. Set keyframes by hitting S while values are selected. Scrub the timeline. It's genuinely the fastest way to block out 3D motion.

In practice, I've seen teams cut Three.js animation work from days to hours using this workflow — especially for scroll-linked scenes where you need frame-perfect choreography between a dozen different objects. Look, you could achieve the same result with GSAP and a lot of discipline, but Theatre makes it visual without sacrificing runtime control.

The Studio Panel and Keyframe Workflow

When you run your app in dev mode with studio.initialize(), a panel appears in the bottom-left corner. You can drag it, resize it, and collapse it. It won't interfere with your existing layout at all — it renders in a shadow DOM to avoid style conflicts. The panel shows all sheets and objects you've registered.

The keyframe workflow feels immediately familiar if you've touched any motion graphics software. Click on an object in the outliner, its properties appear in the detail panel on the right. Drag the playhead to a point in time, change a value, hit S — keyframe set. The curve editor lives in the timeline and gives you full bezier control per property.

One thing that trips people up: Theatre works in seconds by default. If your animation is meant to be 2400ms long, you set the sheet length to 2.4. Convert your timing to seconds mentally before you start — it saves confusion later.

The state saves automatically to localStorage during development. When you're happy with the animation, you export the state JSON from the studio panel and put it in your project. Then load it at runtime via getProject('Name', { state: animationJSON }). That's the full dev-to-production pipeline right there.

If you're building UI components that need animation design tokens alongside your keyframe data, tools like the gradient generator or box shadow generator complement this workflow nicely — you can prototype the visual style alongside the motion in the same session.

Theatre.js vs Alternatives: When to Actually Use It

Theatre.js isn't always the right call. GSAP is still the king for imperative, code-driven UI animations. Framer Motion wins for declarative React component transitions. Theatre's niche is complex, multi-object, timeline-based animations where a visual editor genuinely accelerates the work — think hero sections, scroll-jacked narratives, product configurators, Three.js experiences.

The threshold is roughly: if you're animating more than three objects with more than two properties each and the timing matters a lot visually, Theatre starts paying off. For a simple button hover or a page fade-in? GSAP or Framer Motion, no contest.

That said, nothing stops you from mixing them. Theatre handles the choreographed sequence; GSAP handles the micro-interactions triggered by user input. They don't conflict at runtime because Theatre's core is just a scheduler with a value system — it doesn't fight other animation engines for DOM control.

Worth noting: Theatre.js is MIT licensed for core. The studio is also MIT. There's a paid @theatre/r3f pro tier with advanced features like sequence composition, but the free tier covers most creative dev use cases you'd actually hit.

Performance, Production Build, and Common Pitfalls

The most common mistake is forgetting to exclude the studio from production. Check your bundler config explicitly — if you're on Next.js 14+, a dynamic import with ssr: false and a dev check handles it cleanly.

// pages/_app.tsx or app/layout.tsx
useEffect(() => {
  if (process.env.NODE_ENV === 'development') {
    import('@theatre/studio').then(({ default: studio }) => {
      studio.initialize()
    })
  }
}, [])

The runtime core (@theatre/core) is around 56 KB gzipped. That's reasonable, but not nothing. If you're animating only one or two properties with simple linear interpolation, that weight might not be worth it. Profile your bundle before committing to Theatre on a performance-sensitive page.

Another pitfall: the JSON state file grows with every animation you build. Keep it organized by project/sheet name from the start — cleaning up a sprawling state JSON later is tedious. Also, commit the state file every time you make animation changes, not just when a feature is done. Otherwise you'll lose work when the localStorage clears.

For components that need to look great while Theatre handles the motion — UI kits like Empire UI's aurora or cyberpunk styles pair really well with Three.js scenes and timeline animations. The visual language in those style hubs already assumes you're building something that moves.

FAQ

Does Theatre.js work without Three.js?

Yes, completely. Theatre.js animates any numeric or color values — you can use it for pure DOM/CSS animations in React without touching WebGL at all. Three.js integration is optional via @theatre/r3f.

Is the Theatre.js studio included in production builds?

Only if you initialize it without a dev guard. The @theatre/core runtime ships to production; @theatre/studio should always be behind a process.env.NODE_ENV === 'development' check or a dynamic import.

How does Theatre.js store animation data?

Keyframe state saves to localStorage during development. You export it as a JSON file, commit it to your repo, and load it at runtime via the state option in getProject(). No external service involved.

Can I use Theatre.js with GSAP or Framer Motion at the same time?

Yes, they don't conflict. Theatre handles timeline-choreographed sequences while GSAP or Framer Motion can still own interactive micro-animations. Many production projects mix all three.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Lottie Animations in React 2026: @lottiefiles/react, DotLottie SetupGSAP ScrollTrigger in React: Pinning, Scrubbing and Timeline SyncThree.js with React: Particles, Blobs and Interactive 3D Scenesreact-three-fiber Intro: 3D Scenes in React With Three.js