Hand Count

A number that counts up in stop-motion poses, rewritten by hand at every pose

Installation
$ pnpm dlx shadcn@latest add @remocn/hand-count

Usage

Every pose the value advances and the digits are written again — same number, different hand. That re-drawing is the whole illusion, and it is what separates this from every other number in the registry: rolling-number and number-wheel glide a mechanical typeface, which is right for a product UI and wrong for a paper scene.

The component fills its parent with position: absolute; inset: 0, so give it a frame and it centres itself inside it.

tsx
// src/Root.tsx
import { AbsoluteFill, Composition, Sequence } from "remotion";
import { HandCount, handCountDuration } from "@/components/remocn/hand-count";
 
const COUNT_AT = 12;
const COUNT_FOR = 14;
const COUNT_ENDS = handCountDuration({
  delay: COUNT_AT,
  durationSteps: COUNT_FOR,
});
 
const PricingScene = () => (
  <AbsoluteFill style={{ background: "#f1eee7" }}>
    <Sequence durationInFrames={COUNT_ENDS + 30}>
      <HandCount
        to={19}
        delay={COUNT_AT}
        durationSteps={COUNT_FOR}
        prefix="$"
        decimals={2}
        suffix="/mo"
      />
    </Sequence>
  </AbsoluteFill>
);
 
export const RemotionRoot = () => (
  <Composition
    id="HandCount"
    component={PricingScene}
    durationInFrames={COUNT_ENDS + 30}
    fps={30}
    width={1280}
    height={720}
  />
);

A year-in-review stat

The affixes are written in the same hand as the digits, so a unit reads as part of the number rather than a label stuck beside it.

tsx
<div style={{ position: "relative", width: 720, height: 220 }}>
  <HandCount to={1284} suffix=" commits" durationSteps={18} fontSize={120} />
</div>

Sizing the scene

handCountDuration returns the frame the number actually settles on, which is not always delay + durationSteps * step. The stop-motion clock saturates on the quantized frame, so an off-grid delay pushes the landing to the next pose boundary — a <Sequence> sized by the naive sum can cut up to step - 1 frames before the number arrives.

tsx
handCountDuration({ delay: 0, durationSteps: 12, step: 3 }); // 36
handCountDuration({ delay: 1, durationSteps: 12, step: 3 }); // 39, not 37

Keep delay a multiple of step and the two agree.

Choosing the ease

in-out is the default and it is the one to keep for a headline number. It opens on a barely-there move, does most of the counting through the middle, and settles gently — so every pose in the run carries a visible digit change.

out front-loads the movement hard: at twelve poses it puts almost a quarter of the count into the very first pose and leaves the last third of the run barely moving. That is right when the number is a supporting detail arriving under something else, and wrong when it is the thing being watched.

A literal spring is deliberately not on offer here. Springs start fast by construction, which is the same problem as out, and the overshoot would show a price climbing past its own figure before falling back to it.

Landing and holding still

The value lands on to exactly — it never stops at 99.7 — and the jitter freezes the moment it does. A headline number that keeps twitching for the rest of the shot is a distraction, not a texture; ambient motion in this kit is paper-wobble's job.

Props15

PropTypeDefault
torequired
number—

Value the count lands on, exactly

from
number0

Value it starts at; above `to` it counts down

delay
number0

Local frame counting starts; keep it a multiple of step

durationSteps
number18

Poses spent counting

decimals
number0

Decimal places; 0 never renders a decimal point

prefix
string""

Written before the number in the same hand

suffix
string""

Written after the number in the same hand

fontSize
number96

Size of the written number

color
string"#26242c"

Ink colour

weight
400 | 500 | 600 | 700700

Font weight

ease
"in-out" | "out" | "linear""in-out"

Shape of the count; `in-out` starts and settles gently, `out` front-loads the movement

align
"left" | "center""center"

Alignment inside the frame it fills

fontFamily
stringCaveat

Override the handwritten face

seed
string"count"

Seed for the per-pose glyph jitter

step
number3

Frames per stop-motion pose

  • torequirednumber

    Value the count lands on, exactly

  • fromnumber

    Value it starts at; above `to` it counts down

    Default0

  • delaynumber

    Local frame counting starts; keep it a multiple of step

    Default0

  • durationStepsnumber

    Poses spent counting

    Default18

  • decimalsnumber

    Decimal places; 0 never renders a decimal point

    Default0

  • prefixstring

    Written before the number in the same hand

    Default""

  • suffixstring

    Written after the number in the same hand

    Default""

  • fontSizenumber

    Size of the written number

    Default96

  • colorstring

    Ink colour

    Default"#26242c"

  • weight400 | 500 | 600 | 700

    Font weight

    Default700

  • ease"in-out" | "out" | "linear"

    Shape of the count; `in-out` starts and settles gently, `out` front-loads the movement

    Default"in-out"

  • align"left" | "center"

    Alignment inside the frame it fills

    Default"center"

  • fontFamilystring

    Override the handwritten face

    DefaultCaveat

  • seedstring

    Seed for the per-pose glyph jitter

    Default"count"

  • stepnumber

    Frames per stop-motion pose

    Default3