Check List

A handwritten checklist that writes itself out, then ticks and strikes through every done item

Installation
$ pnpm dlx shadcn@latest add @remocn/check-list

Usage

The list runs in two phases. First it writes itself out: each row draws its box closed and then has its label written inside, with rows starting itemGap frames apart and free to overlap. Only once the whole list has finished writing does the second phase begin — each done item gets its tick, then a stroke dragged through its label, one item after another closeGap frames apart.

That ordering is the point: you read the full list before anything starts being crossed off.

tsx
// src/Root.tsx
import { AbsoluteFill, Composition, Sequence } from "remotion";
import { CheckList, checkListDuration } from "@/components/remocn/check-list";
 
const items = [
  "Render on your own machine",
  "No watermark, ever",
  "Every component MIT",
  { text: "Ships as source", checked: false },
];
 
const LIST_FROM = 12;
const LIST_DURATION = checkListDuration(items);
 
const CheckListScene = () => (
  <AbsoluteFill
    style={{
      background: "#f1eee7",
      alignItems: "center",
      justifyContent: "center",
    }}
  >
    <Sequence from={LIST_FROM} durationInFrames={LIST_DURATION}>
      <CheckList items={items} width={820} />
    </Sequence>
  </AbsoluteFill>
);
 
export const RemotionRoot = () => (
  <Composition
    id="CheckList"
    component={CheckListScene}
    durationInFrames={LIST_FROM + LIST_DURATION}
    fps={30}
    width={1280}
    height={720}
  />
);

Items that never get ticked

A bare string is shorthand for an item that eventually gets ticked. Pass an object with checked: false and the row keeps its box and its clean label forever — no tick, no stroke through it. That is the "not yet" line in a roadmap or a comparison, and it is skipped when the close phase counts out its turns, so it never leaves a gap in the rhythm.

tsx
<CheckList
  items={["Shipped today", { text: "Landing next", checked: false }]}
  width={620}
/>

Sizing the scene

checkListDuration returns the frame the last stroke ends on. Pass the same timing options you pass the component, and delay if you set it there — the returned frame is absolute and includes it.

tsx
const total = checkListDuration(items, { delay: 12, itemGap: 24, step: 4 });

checkListEnterEnd returns the frame the writing phase finishes on — the moment the first tick lands. Use it to fire something else off the same beat, such as a heading or a stamp arriving once the list is fully written.

tsx
const written = checkListEnterEnd(items);

Both are maxima over the whole list, not the last row's: rows overlap, so an early long label can outlast a later short one, and a list whose longest item is unchecked still has that item decide when the writing phase ends. If nothing is checked there is no close phase, and checkListDuration equals checkListEnterEnd.

Why width is required

Labels are written by handwrite, which centres itself absolutely. Inside an auto-sized cell that collapses to zero width and the text silently vanishes, so the list takes its width explicitly and derives every label cell, box offset, and row from it. The component sets no background and no jitter of its own — wrap it in paper-wobble to put it on the desk.

Props15

PropTypeDefault
itemsrequired
(string | { text: string; checked?: boolean })[]—

Rows to draw; a bare string is an item that gets ticked

widthrequired
number—

List width; the label cell, box offset, and rows derive from it

fontSize
number40

Label size; the box and row height scale from it

color
string"#26242c"

Ink colour of the labels

boxColor
string"#26242c"

Ink colour of the boxes

tickColor
string"#6f7f35"

Ink colour of the ticks and the strokes through done labels

delay
number0

Local frame the first box starts drawing

itemGap
numberstep * 6

Frames between item starts while the list writes itself; rows may overlap

closeGap
numberstep * 3

Frames between one item being ticked and the next, after the list is written

rowGap
numberfontSize * 0.55

Pixels between rows

strokeWidth
number3

Pen width of the box; the tick is one unit heavier

perStep
number1.6

Letters written per pose, forwarded to the labels

weight
400 | 500 | 600 | 700600

Label font weight

seed
string"checklist"

Seed for the per-box corner wobble

step
number3

Frames per stop-motion pose

  • itemsrequired(string | { text: string; checked?: boolean })[]

    Rows to draw; a bare string is an item that gets ticked

  • widthrequirednumber

    List width; the label cell, box offset, and rows derive from it

  • fontSizenumber

    Label size; the box and row height scale from it

    Default40

  • colorstring

    Ink colour of the labels

    Default"#26242c"

  • boxColorstring

    Ink colour of the boxes

    Default"#26242c"

  • tickColorstring

    Ink colour of the ticks and the strokes through done labels

    Default"#6f7f35"

  • delaynumber

    Local frame the first box starts drawing

    Default0

  • itemGapnumber

    Frames between item starts while the list writes itself; rows may overlap

    Defaultstep * 6

  • closeGapnumber

    Frames between one item being ticked and the next, after the list is written

    Defaultstep * 3

  • rowGapnumber

    Pixels between rows

    DefaultfontSize * 0.55

  • strokeWidthnumber

    Pen width of the box; the tick is one unit heavier

    Default3

  • perStepnumber

    Letters written per pose, forwarded to the labels

    Default1.6

  • weight400 | 500 | 600 | 700

    Label font weight

    Default600

  • seedstring

    Seed for the per-box corner wobble

    Default"checklist"

  • stepnumber

    Frames per stop-motion pose

    Default3