snapcn
23 components
DocsComponentsTemplatesVideo EditorShowcaseMarketplaceRoadmapChangelog

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 EditorShowcaseMarketplaceRoadmapChangelog
Getting startedIntroductionInstallationAgent skill

Status Cycle

A status pill whose label rolls behind a hard clip while its width springs past the target and back, then the field crossfades to a column of chips stepping up from below

Installation

pnpm
$ pnpm dlx shadcn@latest add @snapcn/status-cycle

Component code

The exact file shadcn add copies into your project.

Usage

A prefix, a list of statuses and a list of chips. Everything else has a measured default.

// src/Root.tsx
import { Composition } from "remotion";
import { StatusCycle } from "@/components/snap-cn/status-cycle";

const Cycle = () => (
  <StatusCycle
    prefix="snapcn is"
    statuses={["animating", "transitioning", "rendering a scene", "installed"]}
    chips={["text-reveal", "phone-frame", "answer-stream", "word-flip"]}
  />
);

export const RemotionRoot = () => (
  <Composition
    id="StatusCycle"
    component={Cycle}
    durationInFrames={150}
    fps={30}
    width={1280}
    height={720}
  />
);

durationInFrames has to cover the whole arc: the intro, one statusHold per status, the crossfade, and one chipStagger per chip. The default 150 fits the default content with a hold at the end; add roughly 18 frames per extra status and 8 per extra chip.

How the swap works

Three things happen on the same frame, and keeping them on the same frame is most of the effect.

The label rolls. The outgoing label translates up and out through the pill's top edge while the incoming one arrives from below, both hard-clipped by the pill's rounded box. There is no crossfade — the label's ink never dims — and the travel is one pill height.

The pill's width springs past its target. Not a spring, though: the curve is cubic-bezier(0.65, 0.2, 0.3, 1.5), and the y2 > 1 control value is the overshoot. A real spring cannot make this shape. One frame into an eleven-frame rise a spring released from rest is already 4.1% of the way there; the reference is at 0.65%, six times flatter, and no damping/stiffness pair fixes that without losing the early peak.

The line re-centres. The lockup is centred as a whole, so a widening pill pushes the prefix left and a narrowing one lets it back. The prefix does not re-animate; it only moves.

The label is laid out at its full final width and centred, and the widening pill reveals it from the middle outward — so during a morph you see the middle of the new label with both ends clipped. That is why the width is its own animated quantity and is never derived from a padded text box.

Content of any length

Every width in the scene is measured at runtime behind delayRender() — the prefix's glyph advances, each status label, each chip — so a longer status or a longer chip simply makes a wider box. Nothing is tuned to one sentence. Every other proportion is a fraction of the composition rather than a pixel, so the scene holds its shape at 720p, 1080p or a vertical crop.

Colour

Leave the colour props off and the scene paints from the design system: primary for the first field, background for the second, foreground for the pill and the ink, and a pale tint of the field for the pill's label. Pass any of them to override.

<StatusCycle
  fieldColor="#0f766e"
  pageColor="#f5f5f4"
  chipFills={["#ffffff", "#e7f5f3", "#0f766e"]}
/>

Overriding the motion

Every curve and duration is a measured default on the exported STATUS_CYCLE_MOTION, and motion is merged over it — so you override one without restating the rest.

import { Easing } from "remotion";

<StatusCycle
  motion={{
    // Flatten the overshoot out of the width morph.
    widthEase: Easing.bezier(0.4, 0, 0.2, 1),
    // And give the chips a slower step.
    scrollTau: 0.12,
  }}
/>

Props

PropTypeDefaultDescription
prefix
string"snapcn is"The line that does not change, to the left of the pill
statuses
string[] | string["animating", "transitioning", "rendering a scene", "installed"]Pill labels, in order. Also accepts a comma-separated string, which is what the customizer passes
chips
string[] | string["text-reveal", "phone-frame", "answer-stream", "word-flip", "orbit-gallery"]Act-2 chip labels, in order. Also accepts a comma-separated string
fieldColor
stringtheme.primaryThe field act 1 sits on
pageColor
stringtheme.backgroundThe field act 2 sits on; the crossfade runs between the two
pillColor
stringtheme.foregroundPill fill
pillLabelColor
stringa pale tint of fieldColorPill label ink
prefixColor
stringtheme.foregroundPrefix ink
chipFills
string[] | stringcard, a pale tint of the field, the fieldChip fills, cycled in order. Three of them gives the reference's period-3 rhythm
chipTextColor
stringtheme.foregroundChip ink. One colour on every chip, including the saturated one
chipBorderColor
stringmixOklch(theme.border, theme.foreground, 0.28)Chip hairline. The default is the design system's own recipe for carrying a border token up to a surface this size
fontWeight
number400Act-1 type weight
chipFontWeight
number400Act-2 type weight. Measured 400 — the chips are bigger, not heavier, and bumping this is the usual way to make them look cheap
pillRadius
number0.229 x pill heightPill corner radius in px. The default ratio is very nearly shadcn's own radius token over a 40px control (10/40 = 0.25), so it already sits in the design system. Not a stadium — h/2 fits the reference's edge two orders of magnitude worse
chipRadius
number0.162 x chip heightChip corner radius in px
chipBorderWidth
number0.017 x chip heightChip hairline width in px
pillCenterY
number0.516Pill centre as a fraction of frame height. The reference's lockup rides 1.6% low
motion
Partial<StatusCycleMotion>STATUS_CYCLE_MOTIONCurve and timing overrides: widthEase, widthSeconds, rollEase, rollSeconds, rollTravel, fadeSeconds, exitSeconds, scrollTau, letterSpring, letterStagger. Every default is fitted from frame data, so overriding one is a departure rather than a tweak
fontSize
number8.7% of the composition heightAct-1 type size in px
chipFontSize
number22.2% of the composition heightAct-2 type size in px
fontFamily
stringInterLoaded through @remotion/google-fonts, so the Player and the render agree
introFrames
number24Frames the per-letter intro cascade gets before the first status swap
statusHold
number18Frames from one status swap to the next. Shorter than the 18-frame width morph on purpose — the next swap starts while the last is still settling
chipStagger
number8Frames from one chip's arrival to the next
startAt
number0Frame the scene starts on
speed
number1Playback rate. Below 1 the last chip never arrives
theme
Partial<SnapCnTheme>—Design-system token overrides
mode
"light" | "dark"—Which side of the token set to resolve