Pencil Emphasis

A hand-drawn pencil squiggle that scribbles in beneath inline text. It sits between bold and italic in tone — a deliberate editorial marker for the one word in a sentence you want the reader to actually pause on.

Some words deserve a second glance.

Customize
Trigger
view
Stroke
1.0x
Signature
S-wave

Installation

npx shadcn@latest add https://craftbits.dev/r/pencil-emphasis.json

Usage

import { PencilEmphasis } from "@craft-bits/core";
 
<p>
  This is the <PencilEmphasis>key idea</PencilEmphasis> of the paragraph.
</p>

Trigger the draw on hover, or force it to draw on mount:

<PencilEmphasis trigger="hover">hover me</PencilEmphasis>
<PencilEmphasis trigger="always">drawn already</PencilEmphasis>

Understanding the component

  1. Four stroke signatures. Each draw is one of four hand-crafted SVG paths — gentle S-wave, quick jag, single deep dip, or flat-with-curl flourish. Every variant has its own widths, opacities, and bottom offset so they read as different pencil moods.
  2. Stable per-word variant. A tiny sum-of-char-codes hash picks a variant for each piece of text, so the same word always gets the same squiggle but neighbours on the page get different signatures.
  3. Primary + ghost stroke. The primary path draws first; a lighter ghost path layers on top to mimic a real pencil's second pass. Both paths share the draw transition — SPRINGS.damped by default (no overshoot on stroke length).
  4. Triggers. view (default) fires the draw once via IntersectionObserver. hover toggles on hover/focus. always draws immediately on mount.
  5. Reduced motion. When prefers-reduced-motion: reduce is set, both paths render at full length from the first frame — no draw animation, just a static underline.
  6. Color via currentColor. The SVG strokes inherit the host text color, defaulting to text-cb-accent.

Props

PropTypeDefaultDescription
childrenReactNoderequiredThe text to underline. String children also seed the variant hash.
trigger'view' | 'hover' | 'always''view'When to draw the squiggle.
variantIndex0 | 1 | 2 | 3hash of textForce a specific stroke signature.
strokeScalenumber1Multiplier on primary + ghost stroke width.
transitionTransitionSPRINGS.dampedOverride the path-draw spring.
classNamestringMerged onto the root span.

Accessibility

  • The original children stay in the DOM exactly as written — screen readers, search, and selection all behave as if the squiggle weren't there.
  • The decorative SVG is aria-hidden="true" with pointer-events: none, so it never interferes with selection or hit-testing.
  • Animation is fully disabled when prefers-reduced-motion: reduce is set — the squiggle renders statically at full length.
  • Exposes data-state="drawn" | "undrawn" and data-trigger="…" on the root for state-driven styling and tests.

Credits

  • Extracted from: craftingattention (app/src/components/ui/richtext-pencil.tsx).