← Back to Blog Posts

svjeez: Animations I Can Review in a Diff

svjeez: Animations I Can Review in a Diff

A friend of mine made svjeez, which turns a TSX file into one animated SVG with no JavaScript in it. That's how I found it. I kept it because a bot can write the scene and I can read the result as code. This is what it is, how I use it, and where it falls short.

A friend of mine made svjeez, and that's how I found it. I wasn't looking for an animation tool. It turned out to fit how I already work: everything as text in a repository, where I can read a change before it lands.

Most animation tools give you a timeline in a window. That's fine for one person on one afternoon. It's a poor fit when the thing making the animation is a script, or a bot, or me six months from now.

svjeez works the other way. You write a scene in TSX. It compiles to a single SVG file.

What it is

svjeez is an open-source library. My friend wrote it, not me. The pitch from its README is "write scenes in TSX, compile them into a single animated SVG, zero runtime JavaScript." I checked each part of that.

The motion compiles to CSS keyframes inside the file. Fonts are subset and embedded. The file adapts to light and dark mode and to reduced-motion settings on its own. Because nothing in it runs code, it plays anywhere an <img> tag works. That includes a README and a blog post like this one.

A scene is a function that returns render(...):

import { draw, ease, enter, render } from "svjeez";

export default function build(): string {
  return render(
    { width: 200, height: 120, duration: 4, title: "Hello", desc: "A curve draws on, then a dot pops in." },
    <>
      <path d="M20 100C60 20 140 20 180 100" fill="none" stroke="#FF5FA8" strokeWidth={4}
        animation={draw({ at: 0.2, over: 1.2 })} />
      <circle cx={100} cy={40} r={8} fill="#3EE6D6"
        animation={enter({ at: 1.2, over: 0.5 }, { scale: 0 }, ease.pop)} />
    </>,
  );
}

Each element carries its own timing: start at 0.2 seconds, take 1.2 seconds. There's no separate timeline to keep in sync.

A pink curve draws itself across a dark card, then a teal dot pops in at the top.
That exact scene, as a 1.1 KB file. It loops every four seconds.

That's the whole idea. The rest of this post is what else fits in the same shape.

What else it can do

Four more scenes, all written for this post. Each is one SVG in an <img> tag.

A terminal that types

svjeez has a terminal helper. You give it commands and their output, and it types them out in a window. It follows your light or dark setting, so this one changes if you flip your system theme.

A terminal window types "npm i svjeez", then "npx svjeez svg hello.tsx --out hello.svg", each followed by its output.
Two commands and their output. 6.2 KB.

I like this for docs. The command in the animation is the command I ran, and I can change it in one place.

A diagram that draws itself

Flowcharts take a typed spec: nodes, and the edges between them. A misspelled node name is a compile error, not a broken picture. svjeez lays it out and plays it rank by rank.

This one is how this very post gets from a draft to a page.

A flowchart draws left to right: Draft, Script, SQL, Review?, Database, Page. A "no" arrow loops from Review back to Draft.
Draft to page. 14.5 KB, the biggest of the set.

It's true, too. The draft is a markdown file. A script turns it into a SQL file. I read that file, then it goes into the database. If I don't like it, I go back and edit the draft.

Motion from a formula

You can bake any function of time into keyframes. This weight is on a damped spring. The scene doesn't draw frames. It has one line of maths, and svjeez samples it.

const bob = sampled((t) => ({ y: -70 * Math.exp(-1.1 * t) * Math.cos(7 * t) }), {
  duration: 4,
  tolerance: { translate: 0.4 },
});
A pink ball drops on a spring, overshoots a yellow line, and settles, with two teal ghosts trailing it.
A spring from a formula. 5.6 KB.

The tolerance is how much error I'll accept. A bigger number means fewer keyframes and a smaller file.

There's more I haven't used: hand-inked strokes that write like handwriting, code that morphs from one version to the next, sequence and state-machine diagrams, and pixel-art voxel scenes. Its docs cover each.

Why it fits

Three things made it worth keeping.

It's reviewable. An animation change is a code diff. I can see that a dot now enters at 1.2 seconds instead of 0.8. A binary project file can't show me that.

It's safe to hand to a bot. The scene is code, so a bot can write it. The output is a file with no scripts in it. Before anything gets published I check for script tags, event handlers and external links in the SVG, and I refuse the file if any show up. The scene itself runs in a sandbox with no network.

It's one file. No sprite sheet, no player, no embed code. I put it in an <img> and it works.

What I've used it for

I've used it for short explainer videos, an animated visual for a piece of music, and quick "how does X work" animations made on request. A small tool turns the SVG into MP4 or GIF when a platform wants a video instead.

Two of them show what a bigger scene looks like.

Membership growth for *Space

I wanted a chart of how many members *Space has, with a forecast. The data comes from the join and leave logs in the bot's database. The forecast is a one-year projection from Google's TimesFM model, run on my own GPU, with its 10–90% range drawn as a band. The scene is a Star Trek–style control panel, with the whole history first and then a zoom onto the logged months and the forecast.

An animated membership chart for *Space. A line climbs to 383 members, then splits into a trend line to 456 and a model forecast near 391, with a shaded range around them.
Members over time, with a one-year forecast. 1.27 MB.

The chart is a script that reads a CSV, so I can run it again next month and get the new picture.

Where the primes live

The other one is a prime number visualization. Wrap the number line around a cylinder, six numbers per turn, and the primes land on only two vertical lines. Every other line is multiples of 2 or 3. Wrap it thirty around and eight lines are left. The animation shows it happening instead of just stating it.

An animated cylinder with dots for the numbers 1 to 204 wrapped six around. The primes light up on two vertical lines, then the view changes to thirty around, where eight lines stay lit.
The number line wrapped around a cylinder. 1.1 MB.

Both are far larger than the four scenes above, and that's the next section.

Where it falls short

It is young. I'm on 0.6.1, so I expect things to move.

The SVG route has a cost. File size follows how much moves. The four scenes above come to 1.1, 5.6, 6.2 and 14.5 KB. That's small. The two scenes just above are not: the membership chart is 1.27 MB and the cylinder is 1.1 MB. Those have many moving parts, and the file grows with them. They load fine on a normal connection, but I wouldn't put six of them on one page.

A scene can also be too busy for the browser. This one draws the first 10,000 primes as a spinning cone: 100 layers of 100 dots each.

A spinning cone of the first 10,000 prime numbers, coloured from blue at the top to yellow at the point. In Chrome, black rectangles can flash over parts of it as it turns.
The first 10,000 primes in 3D, first version. 688 KB. Watch for black rectangles.

The file is valid and the scene is correct. The trouble comes at playback. Chrome drops pieces of the picture once about a hundred large groups spin at the same time, so you get black rectangles and flickering dots. It happened in the SVG and in the MP4 I recorded from it. I fixed it by merging each layer's dots into one path and cutting the moving groups to about 40. That version was clean and came out smaller, at 530 KB. The first version is the one above, so you can see it for yourself. Whether you see the glitch depends on your browser and your graphics hardware.

And it's still animation. A scene I can read as code is not a scene that looks good. The first drafts a bot writes are usually correct and a bit dull. I get better results when I render frames to a contact sheet and look at the timing before I believe it.

Should you try it

If you already write TypeScript and you want animations that live in a repo, yes. If you want to drag keyframes around by hand, no. That's a different tool for a different job.

The project is at svjeez.texoport.in, with a gallery of examples and a skill file for coding agents.

The end

Linked from elsewhere

© 2026 davis9001.dev | All rights reserved.