What if a CSS animation could leave the browser?
demo.mp4
Cross Platform ⭐ AI Generatable ⭐ Human Readable ⭐ Converts Lottie & SVG
Popkorn is a CSS-based portable format for motion graphics. You write a self-contained
scene in syntax you already know (@keyframes, transform, offset-path, z-index),
and the same file runs on the web and on mobile today, natively through React Native. It keeps the
familiar, readable syntax of CSS, so a scene is never an opaque binary or JSON
blob. And because it's CSS-shaped, language models already know it: no
fine-tuning or special prompting required. You can make edits or even generate entire scenes from scratch with natural language.
Popkorn already renders vector shapes, gradients, masks, motion paths, and
path morphing. And it isn't only playback: the familiar CSS pseudo-classes
:hover and :active just work ✨, and state machines drive toggles, taps, and
app-state behavior with no scripting. It even imports real
Lottie files and SVGs, often a touch smaller than the source they came from.
Popkorn started out as a what-if experiment, but it's grown surprisingly capable. It's early still, but the core idea, that a CSS animation can be a portable artifact, is looking promising.
Because of the familiar CSS syntax, LLMs are proving very capable at authoring scenes from scratch and at editing what's already there, like an imported Lottie, changing its colors, timing, or motion. A case in point: the playground's example gallery is itself entirely LLM-generated.
▶ Try it live in the playground: edit scenes in the browser, no install.
A scene, in full
This is a complete Popkorn scene: a red ball, falling and bouncing, with the easing an animator would reach for.
:root { width: 400px; height: 400px; background: #1a1a2e; } @keyframes bounce { 0% { transform: translateY(0); animation-timing-function: cubic-bezier(0.33, 0, 1, 1); } 50% { transform: translateY(180px); animation-timing-function: cubic-bezier(0, 0, 0.67, 1); } 100% { transform: translateY(0); } } #ball { type: circle; cx: 200px; cy: 80px; r: 36px; fill: #ff6b6b; animation: bounce 1.2s linear infinite; transition: fill 250ms ease; &:hover { fill: #ffd166; } }
If you've written CSS, you can already read every line. So can a language model, which is the point (see Why CSS).
Point at the ball and its color warms, smoothly tweened by the transition,
while the bounce never pauses or restarts. This is one of Popkorn's nicer
surprises: interactive states like :hover and :active compose on top of
running animations rather than fighting them, because the whole scene plays on
one continuous timeline. Dropping a small interaction onto an animating element
just works.
Making a scene
It's early enough that there are no authoring tools yet, but you can already make scenes today. Most start one of two ways, and because the format is readable underneath both, you can always drop into the code to adjust:
- From an existing animation. Already have a Lottie or an SVG? Drop it into the playground with the Import button and it becomes a Popkorn scene you can read and tweak on the spot. No starting from a blank file.
- By prompting. The playground's Popkorn Copilot builds a scene from a description or edits the live one on request. It works because the format stays close enough to CSS that a model already knows it, no fine-tuning required. See Prompting with AI for what that looks like.
Hand-authoring is a first-class option too, for simple scenes or for anyone who enjoys writing CSS, and a visual creation tool may come in time. But whichever path you take, you land on the same thing: one legible file you can open and edit.
Getting started
The quickest way in is the playground. No install, it runs in your browser:
Edit the example scenes live, tweak values and watch them update, or import a Lottie or SVG to see it convert.
To put a scene on your own page, the simplest way is the <popkorn-player> web
component:
<script type="module"> import "@popkorn/player"; </script> <popkorn-player width="400" height="400"></popkorn-player> <script> document.querySelector("popkorn-player").source = ` #dot { type: circle; cx: 200px; cy: 200px; r: 40px; fill: #e94560; } `; </script>
Driving the renderer yourself? The parser, scene builder, and renderers are all
exported from @popkorn/player; its README covers the
programmatic API.
To run the playground or hack on Popkorn locally:
bun install
bun run dev # http://localhost:5173What it can do
Popkorn covers most of what people reach for in real motion graphics:
- Runs where you need it. One scene file plays in the browser (Canvas2D or SVG) and on native mobile through React Native and Skia. No re-export per target, no runtime lock-in.
- Imports what you already have. Real Lottie files and SVGs convert into readable Popkorn scenes and play back faithfully, so you can bring an existing library across instead of starting from a blank file.
- Interactivity, built into the format. Drive multi-state behavior with
hand-written state machines, react to pointer with
:hoverand:activetweened by CSStransitions, and bind properties to live input withvar()andinput(cursor.x). Because it all runs on one continuous timeline, interactive states compose cleanly on top of running animations instead of restarting them. And it's a two-way street with the host page or app: push live values in withsetVariable, fire named events withfire(), and listen forstatechangecoming back out, so your app state and the scene stay in sync. There's no script engine; the reactivity is part of the format (docs/state-machines.md). - Shapes & paint. Circles, rects, ellipses, polygons, polystars, and full
SVG paths. Solid fills, linear and radial gradients, strokes with dashes and
caps. CSS
filterfunctions (blur, drop-shadow, color adjustments),box-shadow, andmix-blend-modefor compositing, all covered by the cross-backend conformance suite so they render the same on every renderer. - Text & images. Laid out and transformed as first-class scene nodes.
- Animation.
@keyframeswith per-keyframe easing, spring-style beziers, holds (step-end), staggering (negativeanimation-delay), and additive layering of animations on one node. - Motion & morphing.
offset-pathfor motion along a curve, trim paths and dashes, and path morphing between shapes. - Composition. A real scene graph with parent/child transforms, symbols
(reusable definitions),
z-indexlayering, clipping, masks and track mattes, visibility windows, and per-subtree time scaling.
The playground shows each of these as a live scene, and
the sources live in examples/popkorn/.
Why CSS
Choosing CSS wasn't a shortcut. It's the whole idea, and Popkorn holds itself to
one rule: if CSS already has a way to express something, use it, with its real
semantics. Motion paths are offset-path. Holds are step-end. Staggering is
a negative animation-delay. Layering is z-index. Popkorn never invents syntax
that CSS already has. (It isn't exactly CSS. It's a close dialect, kept as near
to the real thing as we can, and maybe some of the good parts go upstream one
day 🤞)
Staying this close buys something rare: one format that two very different audiences already read.
- People already speak it. There's a vibrant community making genuinely beautiful art in hand-written CSS. Popkorn meets them where they are, with no new mental model and no editor to learn.
- Language models already speak it too. Because Popkorn stays so close to real CSS, a model already knows most of it from its training data. There's no fine-tuned model and no bespoke format to teach. Hand it the small extra vocabulary and it writes valid, working Popkorn. That's a property you only get by refusing to invent syntax.
The payoff is a format that's hand-authorable, diffable in a pull request, and generatable by an LLM, all at once.
Documentation
The full docs live in docs/ and render live in the playground
under /docs:
- Introduction and Getting Started
- State machines, Importing Lottie and SVG, and the Player API
- CSS art → Popkorn: single-div CSS art tricks, translated to a real scene graph
- Format reference and Architecture
Status & what's next
In the browser it already works well, with two renderers behind it (Canvas2D and SVG) and a wide range of real Lottie files converting and playing faithfully. The same scenes run on mobile through a React Native (Skia) renderer, with a demo Expo app in the repo; it's still marked work-in-progress but holds up just as well. The clearest frontier from here is performance: deeper optimization and benchmarking still to do.
It's early enough that even the file extension is unsettled. Scenes are .css
for now, mostly because it earns free syntax highlighting almost everywhere, a
side benefit of staying so close to CSS. A custom file extension is something we may explore later.
It's a personal what-if that turned out to work, shared in case the idea is as interesting to you as it was to build. Feedback and curiosity welcome.
Packages
| Package | What it is |
|---|---|
@popkorn/parser |
The format parser: source to typed AST. Zero dependencies, no build step. |
@popkorn/player |
The <popkorn-player> web component and the Canvas2D + SVG runtimes. |
@popkorn/converters |
Lottie and SVG to Popkorn importers (CLI + library). |
@popkorn/react-native |
React Native / Skia renderer, running scenes natively on mobile. |
@popkorn/expo-demo |
Expo app demoing the native renderer. |
@popkorn/playground |
A live scene editor: example gallery, Lottie/SVG import, and Popkorn Copilot. |
License
MIT