snapcn
32 components
DocsComponentsTemplatesVideo EditorNewShowcaseMarketplaceRoadmapChangelog

Getting started

IntroductionInstallationAgent skill
GitHub

Star snapcn

Free, open-source components.

Production-ready Remotion animations, transitions and backgrounds — you own the code.

Star on GitHubXFollow

MIT licensed · own your code.

© 2026 snapcn

Loading
snapcn
32 components
DocsComponentsTemplatesVideo EditorNewShowcaseMarketplaceRoadmapChangelog

Getting started

IntroductionInstallationAgent skill
GitHub

Star snapcn

Free, open-source components.

Production-ready Remotion animations, transitions and backgrounds — you own the code.

Star on GitHubXFollow

MIT licensed · own your code.

© 2026 snapcn

DocsComponentsTemplatesVideo EditorNewShowcaseMarketplaceRoadmapChangelog
Getting startedIntroductionInstallationAgent skill

Agent Steps

An agent narrating its own work — a log that writes itself one line at a time, always on the centre line, and then clears until only the answer is left

Installation

pnpm
$ pnpm dlx shadcn@latest add @snapcn/agent-steps

Needs an existing Remotion project and a components.json. Two-minute setup if this is your first snapcn component.

Component code

The exact file shadcn add copies into your project.

Usage

The prompt lands in a pill. Under it the log writes itself: each step arrives running — present tense, ellipsis, a faint globe turning beside it — and later flips to past tense behind a check, carrying whatever number it found. The whole column steps up one row every time a line is added, so the line that is still running is always the one on the centre of the frame.

When the work is done the column swells and clears, the surviving line slides to the middle of the frame, and the answer takes its place in a chip of its own.

// src/Root.tsx
import { Composition } from "remotion";
import { AgentSteps } from "@/components/snap-cn/agent-steps";

const Working = () => (
  <AgentSteps
    query="A 30-second launch video for a Next.js analytics dashboard"
    steps={[
      { running: "Searching the registry…", done: "Searched the registry", icon: "globe" },
      { running: "Reading the docs…", done: "Read 4 component docs", icon: "globe" },
      { running: "Building the timeline…", done: "Set 9 scene timings" },
      { running: "Rendering frames…", done: "Rendered frames" },
    ]}
    result="Rendered 900 frames"
  />
);

export const RemotionRoot = () => (
  <Composition
    id="AgentSteps"
    component={Working}
    durationInFrames={160}
    fps={30}
    width={1280}
    height={720}
  />
);

The last step never finishes

That is the whole structure, not an oversight. Every step but the last one runs for its hold and then flips to done; the last one is still running when the column clears, and the result chip is what it turns into. Write it in the present tense ("Rendering frames…") and put the number in result ("Rendered 900 frames") — the payoff is the moment the sentence completes.

There is no gap to tune between one step finishing and the next appearing: the next line is the acknowledgement that the last one finished, so it enters on the very next frame. That is what the reference does, frame for frame, and a pause between the two reads as a stall.

The column is bottom-anchored, not top-anchored

Rows stack downwards but the column hangs from its last row, which sits on centerY. So the list grows upward out of the frame rather than downward into empty space, and the live line never moves. The travel is one ROW_PITCH per step on a cubic-bezier(.3,.7,.3,1) over 0.308s, and it is additive: if two steps land inside one travel, the column carries both moves at once instead of interpolating between row indices — which is the difference between a list that keeps up with a fast agent and one that stalls and then jumps.

Proportions

Every measurement is a fraction of the composition height, so the whole thing scales as one piece. They come off a 718×398 screen recording, and none of them are eyeballed.

fraction of frame heightmeasured
row pitch0.073829.39px, from six consecutive settle positions agreeing to 0.07px
query pill height0.055322.0px, by the mass of its fill, not by its edges
corner radius0.28 of pill heightnot a pill; a stadium would be 0.500 and sits 3.5px wrong
result pill height0.069127.5px — the answer gets a bigger chip than the question
icon box0.031412.5px, derived from drawn ink, since a lucide glyph does not fill its box
step type0.025410.11px
prompt type0.878 × step typethe prompt is set smaller than the log it starts

The prompt being the smaller type is the one that looks like a mistake and is not. It falls out of a cross-correlation of the reference's column ink against this component's own render under a horizontal scale sweep: three separate step labels peak at 0.882, 0.868 and 0.887, and the prompt at 0.772. The same ratio shows up independently in the two strings' widths. A prompt has to survive one line in its pill; a step label never gets long enough to care.

Why the measurements are mass, not edges

A Gaussian blur moves an edge crossing but conserves an integral. The reference is a compressed recording of 10px type with a soft edge either side of every shape and a faint shadow under the pill, so every distance-between-two-edges came out wrong in a different direction each time. The pill height is therefore measured as the mass of its fill over a column band clear of both the type and the corner radius, and the icon strokes are fitted on ink — the one thing a blurred 12px glyph still reports honestly. Lucide's default stroke-width of 2 puts 34% more ink on the globe than the reference carries, which at this size is the difference between an icon and a dark blob.

The running step's icon is nearly invisible

RUNNING_ICON is 0.17, and that is measured too: the reference's spinner carries 850 of ink where its finished globe carries 5110. A running step is a place in the list, not a thing to look at — the moment it finishes, its icon is what tells you. The globe turns while it runs, and only its meridian turns: spinning the whole glyph would spin the equator with it and read as a wheel rather than a sphere.

The label, on the other hand, is not dimmed while it runs. The reference's running and done labels carry the same ink per glyph.

The outro is two lengths, not one

The column swells to 1.40× and fades, and the surviving row slides to the middle. Both curves are cubic-bezier(.3,0,.3,1), fitted separately at rms 0.017 and 0.015 — and they are deliberately not the same length. The swell finishes in 0.5s, the slide in 0.6s: the row is already at its final size while it is still travelling, so the last thing that moves is the thing you are meant to read.

The slide needs no DOM measurement. The row is wrapped in an element as wide as the column and the pair carry translateX(+50%) and translateX(-50%) — percentages of two different widths — so the row's centre lands on the column's centre whatever the copy is.

The surface is the design system's, not the reference's

The proportions above are measured. The colours are not — they were the reference's brand. The page is theme.background, the pills are theme.card, the log is theme.mutedForeground, the checks and the glint are theme.primary, and the wash in the corner is mixOklch(background, primary, 0.17) — a tint of the page, never a second colour on it. Override any of them per-prop, or the lot through theme.

Props

PropTypeDefaultDescription
query
string"A 30-second launch video for a Next.js analytics dashboard"The prompt, in the pill at the top
steps
AgentStep[] | string—The log, in order. Each entry is { running, done, icon?, hold? }. Also accepts one string for quick edits: "running > done @globe; running > done"
result
string"Rendered 900 frames"The answer the last step turns into
queryHold
number0.683Seconds the prompt holds alone before the first step
stepHold
number0.45Default seconds a step runs, for steps that do not set their own hold
finalHold
number0.283Seconds the last step runs before the column starts to clear. It never flips to its done label — the result replaces it
paperColor
string—Page behind the scene. Defaults to theme.background
glowColor
string—The wash in the corner. Defaults to mixOklch(paper, primary, 0.17)
glowX
number0.858Wash centre, as a fraction of frame width
glowY
number0.854Wash centre, as a fraction of frame height
glowRadius
number0.465Wash radius, as a fraction of composition height. 0 turns it off
pillColor
string—Pill fill. Defaults to theme.card
inkColor
string—Prompt and result ink. Defaults to theme.foreground
stepColor
string—Step ink and the globe. Defaults to theme.mutedForeground
accentColor
string—The completed check and the glint that crosses the result. Defaults to theme.primary
centerY
number0.5The line the running step sits on, as a fraction of frame height
fontFamily
string—Overrides Inter
theme
Partial<SnapCnTheme>—Design-system token overrides — the same shape every snap-cn-ui component takes
mode
"light" | "dark""light"Which default token set to resolve against
speed
number1Global playback multiplier applied to the whole sequence

More in this category

  • Search TypingA search field wider than the shot — it comes forward, types across its left half, then pages to its right half
  • Prompt SendA composer that unrolls from a hairline, writes a brief while the camera cuts in and rides the caret, then cuts back out as a pointer arrives and sends it — with typing that eases in and out instead of ticking
  • Prompt ZoomAn assistant landing screen that offers its suggestions, then cuts hard into the caret — a measured 2.547× push anchored on the text insertion point — where the prompt types itself
  • Answer StreamThe beat after send — a macro shot on the button cuts hard to the answer building itself, while the camera pulls back about a focal point above the frame to keep up with it
  • Answer HighlightA question, an answer that writes itself a word at a time, then a caret lands mid-sentence and drags a selection across the one statement that matters — and settles onto one word inside it