reelcn
GitHubBack to landing

Checklist

Rows that check themselves off with the existing stroke-draw check technique, accelerating pace as the list winds down, and mute once checked.

Checklist, defaults
Checklist, defaults thumbnail, 16:9

Installation

npx shadcn add https://www.reelcn.dev/r/checklist.json

Files land in src/reelcn/ with the items it builds on.

Usage

<Center>
  <Checklist items={[{ text: "Connect your repo" }, { text: "Add a webhook" }, { text: "Ship your first release" }]} />
</Center>

Props

PropTypeDescription
delaynumberFrames to wait before entering.
durationnumberEnter duration in frames. Defaults to 0.6s. Some items reuse duration for an unrelated local timing. See that item's own prop docs.
exitnumber | booleanExit at the end of the parent `<Sequence>`. false keeps it on screen, a number sets the exit length in frames.
motionMotionPersonalityOverride the theme's motion personality.
posterbooleanRenders fully entered, with no exit: for a still frame or thumbnail. Defaults to false.
holdFramesnumberDelays the start of the exit window by this many frames, for a minimum on-screen hold. Defaults to 0.
items*{ text: string; at?: number; }[]at in seconds; omitted entries derive a content-based, accelerating schedule (`checklistSchedule`).
checkColorstring
styleCSSProperties
classNamestring

Built from

Use

  • A features/setup-steps list that completes itself in a product demo
  • Any "here's everything done" beat that shouldn't feel mechanically linear

Avoid

  • A single item. Use plain text with svg-draw's check path directly

Guides

Components take their colors from a theme, their timing from motion props and their layout from the canvas.

Source

registry/items/checklist.tsx
/**
 * @title Checklist
 * @category product
 * @description Rows that check themselves off with the existing stroke-draw check technique, accelerating pace as the list winds down, and mute once checked.
 * @duration data-driven
 * @use A features/setup-steps list that completes itself in a product demo
 * @use Any "here's everything done" beat that shouldn't feel mechanically linear
 * @avoid A single item. Use plain text with `svg-draw`'s check path directly
 * @tags checklist, checkmark, list, tasks, stagger
 * @example
 * <Center>
 *   <Checklist items={[{ text: "Connect your repo" }, { text: "Add a webhook" }, { text: "Ship your first release" }]} />
 * </Center>
 */
import type React from "react";
import {
  checklistSchedule,
  clamp01,
  type MotionProps,
  StrokeOverlay,
  tween,
  useMotion,
  useTheme,
  useViewport,
} from "./core";

export type ChecklistProps = MotionProps & {
  /** `at` in seconds; omitted entries derive a content-based, accelerating schedule (`checklistSchedule`). */
  items: { text: string; at?: number }[];
  checkColor?: string;
  style?: React.CSSProperties;
  className?: string;
};

const ROW_HEIGHT = 52;
const CHECK_SIZE = 22;
const CHECK = "M4 12l5 5L20 6";

export function Checklist({ items, checkColor, style, className, ...motion }: ChecklistProps) {
  const theme = useTheme();
  const { u } = useViewport();
  const m = useMotion(motion);
  const schedule = checklistSchedule(items);
  const check = checkColor ?? theme.colors.success;

  return (
    <div
      className={className}
      style={{ display: "flex", flexDirection: "column", gap: u(4), opacity: m.presence, ...style }}
    >
      {items.map((item, i) => {
        const arrivalFrame = m.delay + (item.at !== undefined ? Math.round(item.at * m.fps) : schedule[i]);
        const drawn = clamp01(
          tween(m.frame, m.fps, { from: arrivalFrame, duration: Math.round(m.fps * 0.35), motion: "smooth" }),
        );
        const settled = clamp01(
          tween(m.frame, m.fps, {
            from: arrivalFrame + Math.round(m.fps * 0.35),
            duration: Math.round(m.fps * 0.4),
            motion: "smooth",
          }),
        );
        return (
          <div key={item.text} style={{ display: "flex", alignItems: "center", gap: u(14), height: u(ROW_HEIGHT) }}>
            <svg aria-hidden="true" width={u(CHECK_SIZE)} height={u(CHECK_SIZE)} viewBox="0 0 24 24">
              <circle cx={12} cy={12} r={10.5} fill="none" stroke={check} strokeWidth={1.6} opacity={drawn} />
              <StrokeOverlay
                d={CHECK}
                kind="vector"
                seed={`checklist-${i}`}
                color={check}
                strokeWidth={2.4}
                drawn={drawn}
                extraProps={{ strokeLinecap: "round", strokeLinejoin: "round" }}
              />
            </svg>
            <span
              style={{
                fontFamily: theme.fonts.body,
                fontSize: u(22),
                color: `color-mix(in srgb, ${theme.colors.muted} ${Math.round(settled * 100)}%, ${theme.colors.foreground})`,
              }}
            >
              {item.text}
            </span>
          </div>
        );
      })}
    </div>
  );
}

On this page