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.jsonUsage
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
- 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.
- 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.
- 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.dampedby default (no overshoot on stroke length). - Triggers.
view(default) fires the draw once via IntersectionObserver.hovertoggles on hover/focus.alwaysdraws immediately on mount. - Reduced motion. When
prefers-reduced-motion: reduceis set, both paths render at full length from the first frame — no draw animation, just a static underline. - Color via
currentColor. The SVG strokes inherit the host text color, defaulting totext-cb-accent.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | required | The text to underline. String children also seed the variant hash. |
trigger | 'view' | 'hover' | 'always' | 'view' | When to draw the squiggle. |
variantIndex | 0 | 1 | 2 | 3 | hash of text | Force a specific stroke signature. |
strokeScale | number | 1 | Multiplier on primary + ghost stroke width. |
transition | Transition | SPRINGS.damped | Override the path-draw spring. |
className | string | — | Merged 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"withpointer-events: none, so it never interferes with selection or hit-testing. - Animation is fully disabled when
prefers-reduced-motion: reduceis set — the squiggle renders statically at full length. - Exposes
data-state="drawn" | "undrawn"anddata-trigger="…"on the root for state-driven styling and tests.
Credits
- Extracted from:
craftingattention(app/src/components/ui/richtext-pencil.tsx).