Ink Underline

A hand-dragged ink stroke that draws itself across in a few stop-motion poses

Installation
$ pnpm dlx shadcn@latest add @remocn/ink-underline

Usage

The stroke renders in normal flow — a block of width by thickness + 4 — so it stacks under text in a flex column. Pair it with handwrite and start the drag the moment the writing finishes:

tsx
// src/Root.tsx
import { AbsoluteFill, Composition } from "remotion";
import { Handwrite, handwriteDuration } from "@/components/remocn/handwrite";
import { InkUnderline } from "@/components/remocn/ink-underline";
 
const url = "remocn.dev";
 
const InkUnderlineScene = () => (
  <AbsoluteFill
    style={{
      background: "#f1eee7",
      alignItems: "center",
      justifyContent: "center",
      flexDirection: "column",
      gap: 4,
    }}
  >
    <div style={{ position: "relative", width: 420, height: 96 }}>
      <Handwrite text={url} fontSize={64} />
    </div>
    <InkUnderline width={420} delay={handwriteDuration(url)} />
  </AbsoluteFill>
);
 
export const RemotionRoot = () => (
  <Composition
    id="InkUnderline"
    component={InkUnderlineScene}
    durationInFrames={60}
    fps={30}
    width={1280}
    height={720}
  />
);

Handwrite centres itself inside its nearest positioned ancestor, which is why it sits in a sized position: relative box here. The underline needs no such box — it is a block in the flow.

The wobble in the stroke comes from seed: the same seed always draws the same curve, and changing it gives a different hand. durationSteps is how many stop-motion poses the drag takes, so the real length in frames is durationSteps * step.

Props10

PropTypeDefault
widthrequired
number—

Stroke length in pixels

color
string"#6f7f35"

Ink colour (any valid CSS color)

thickness
number9

Brush width in pixels at full pressure

pressure
number1

Weight the brush lands on, as a fraction of thickness

release
number0.15

Weight the brush lifts off at; below pressure it thins into a dry tail

grain
number1

Edge roughness; breaks the thin tail into dry-brush flecks, 0 gives clean edges

delay
number0

Frames on the local clock before the drag begins

durationSteps
number5

Stop-motion poses the drag takes

seed
string"ink"

Seed for the curve wobble — same seed, same stroke

step
number3

Frames per stop-motion pose — the tempo control

  • widthrequirednumber

    Stroke length in pixels

  • colorstring

    Ink colour (any valid CSS color)

    Default"#6f7f35"

  • thicknessnumber

    Brush width in pixels at full pressure

    Default9

  • pressurenumber

    Weight the brush lands on, as a fraction of thickness

    Default1

  • releasenumber

    Weight the brush lifts off at; below pressure it thins into a dry tail

    Default0.15

  • grainnumber

    Edge roughness; breaks the thin tail into dry-brush flecks, 0 gives clean edges

    Default1

  • delaynumber

    Frames on the local clock before the drag begins

    Default0

  • durationStepsnumber

    Stop-motion poses the drag takes

    Default5

  • seedstring

    Seed for the curve wobble — same seed, same stroke

    Default"ink"

  • stepnumber

    Frames per stop-motion pose — the tempo control

    Default3