Design Lounge
Install

Nº 126 of 520

Marker highlight draw

A 58px Newsreader pull-quote where yellow marker and red pen strokes draw on over 2.5s; Replay and hover run the same stroke-dash animation again.

Preview

Open full size
1280 × 800 · 8.0 KB · 115 lines

Use it

Free to use · credit appreciated

1 Once, in your project

npx skills add SusanAcharya/Design-Lounge

2 Then ask

Using the Design Lounge skill, build "Marker highlight draw" (piece text-marker-highlight-draw) in my stack. Match its brief; restyle it onto my product's theme.

No skill? Paste the brief instead. It carries the numbers, tokens and behaviour.

Brief

227 lines

Build brief for a coding agent. Rebuild this piece in the reader’s stack. If they haven’t said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don’t “improve” them.

What it is

A magazine pull-quote for “The Margin” (Issue 31, Autumn 2026). The 58px Newsreader sentence is annotated the way a reader would mark a galley: a yellow highlighter wipe, a red ink circle around wait, a wavy underline, two more highlights, then a −5° Caveat note with a hand-drawn arrow. Every stroke is an SVG path with pathLength="1" and stroke-dashoffset animated 1 → 0. A notebook rule sits 55px from the left. The detail worth copying is that the marks are behind the type (z-index: -1 on the SVG, mix-blend-mode: multiply on highlights) so the serifs stay sharp and the yellow reads as ink on paper, not a box behind a span.

Reference behaviour

  1. First paint: <main id="stage" class="run">. Marks draw in this order (CSS --d delay, then --draw 650ms, except the circle 820ms):
    • 300ms — highlight “your attention.”
    • 900ms — circle around italic wait
    • 1400ms — underline “remember where”
    • 1900ms — highlight “get out”
    • 2250ms — highlight “of the way.”
    • 2500ms — arrow path; 380ms later the two-line note types in over 700ms (clip-path inset 100% → 0, steps(14)).
  2. Hovering any .mk replays that path only: animation: none, force reflow via getBBox(), then draw 650ms (820ms if .ci) cubic-bezier(.65, 0, .35, 1) 0ms backwards.
  3. Replay button: clear inline animation on all paths and .note span, remove .run, force reflow (offsetWidth), add .run again so the full sequence restarts from --d.
  4. Header Subscribe pill inverts on hover (ink fill, paper type). Replay border goes --ink on hover.
  5. Reduced motion: .run paths and note spans have animation: none. Marks are fully visible (dashoffset 0, clip open). Replay/hover still swap classes but there is nothing to watch.

Quote (exact wrapping):

The best tools don’t ask for your attention.
They wait, they remember where
you left off, and then they get out
of the way.

Kicker: “Notes on slow software”. Byline: Ines Okafor · 11 min read · 2 October 2026 · Filed under Craft. Note: “the whole essay,” / “in one word”. Footer legend: Highlight / Pen, “Hover a mark to redraw it”.

Structure

1280 × 800  paper #F2F1EC, vertical rule at x=55–56
.page  grid rows 68px / 1fr / auto, pad 0 56 0 96
┌ header 68, border-bottom 1px ink ──────────────────────────────────────┐
│ The Margin (30px italic)     ESSAYS    ISSUE 31 · AUTUMN 2026  [Subscribe — €48/yr]
├ main grid 1fr 250px, gap 40, align centre, pad-top 8 ───────────────────┤
│ NOTES ON SLOW SOFTWARE ──                                          ↗   │
│ The best tools don’t ask for your attention.      arrow + Caveat note  │
│ They wait, they remember where                    "the whole essay,    │
│ you left off, and then they get out               in one word"         │
│ of the way.                                       −5deg, 30px          │
│ (IO) Ines Okafor · 11 min · 2 October 2026 · Craft                     │
├ footer 22/26 pad, 12px uppercase ───────────────────────────────────────┤
│ ■ Highlight   — Pen     Hover a mark to redraw it          [↻ Replay]  │
└────────────────────────────────────────────────────────────────────────┘
  • .page — three rows. Header flex: .mast, two <span>s, pill <a>.
  • #stage.run — quote column + .note.
  • Each mark: <span class="mk hl|ci|ul" style="--d:…"> wrapping the words, first child an <svg aria-hidden> with one <path pathLength="1">. Circle SVG sits above (z-index: 1) so the stroke rings the italic; highlights and underline sit behind.
  • .meta — 36px #CBD5E8 disc “IO”, name, 3px dots, three facts.
  • Footer: .legend (yellow 18×8 chip, red 18×2 rule), instruction, #replay 40px pill.

Path geometry (viewBoxes, keep these curves):

  • Highlight: viewBox="0 0 100 10", SVG left −0.1em, width calc(100% + .2em), top .3em, height .66em. Stroke --marker 9, opacity .92, round caps. Path 1: M1 6 C 25 4.6, 60 5.8, 99 4.2. Path get out: M1 5.4 C 30 6.2, 64 4.4, 99 5.6. Path of the way: M1 4.8 C 30 5.8, 70 4.2, 99 5.4.
  • Underline: SVG top .98em, height .26em, full width. Stroke --pen 2.2. M1 5 C 14 2, 26 8, 40 5 S 66 2, 80 5.5 S 94 6, 99 3.
  • Circle: SVG left −0.3em, width calc(100% + .6em), top −.06em, height 1.34em. Stroke --pen 2.4. M62 5 C 24 1, 3 16, 5 32 C 7 50, 46 58, 80 52 C 99 47, 101 22, 86 11 C 72 3, 46 3, 30 9.
  • Arrow: 150×84, M140 80 C 120 40, 70 18, 12 22 M26 10 L 10 22 L 26 34. Stroke 2.2.

preserveAspectRatio="none" on the quote SVGs so they stretch to the word width.

Tokens

:root {
  /* colour — warm paper, navy ink, yellow marker, red pen */
  --paper: #F2F1EC;
  --paper-2: #E8E6DE;
  --ink: #1B2333;
  --ink-2: #4A5163;
  --ink-3: #6E7383;
  --rule: #D3D0C6;
  --marker: #FFD43B;
  --pen: #D9402A;
  --av: #CBD5E8;

  /* type */
  --serif: "Newsreader", Georgia, serif;
  --hand: "Caveat", cursive;

  /* sizes */
  --fs-quote: 58px;
  --fs-body: 17px;
  --fs-label: 12px;
  --fs-hand: 30px;
  --rule-x: 55px;

  /* motion */
  --draw: 650ms;
  --draw-circle: 820ms;
  --write: 700ms;
  --ease-pen: cubic-bezier(.65, 0, .35, 1);
  --ease: cubic-bezier(.2, .7, .2, 1);
}

Typography

RoleFamilySizeWeightLine-heightTrackingCase
BodyNewsreader17px4001.60sentence
MastheadNewsreader30px400 italic1−0.02emtitle
Header metaNewsreader12px5001+0.14emUPPERCASE
SubscribeNewsreader14px50010sentence
KickerNewsreader12px5001+0.14emUPPERCASE
QuoteNewsreader58px4001.16−0.022emsentence
Quote italicNewsreader58px400 italic1.16−0.022emsentence
NoteCaveat30px6001.050sentence
BylineNewsreader15px400/5001.60sentence
Footer / legendNewsreader12px5001+0.14emUPPERCASE
ReplayNewsreader14px50040px h0sentence

Quote uses font-variation-settings: "opsz" 72. Marks are white-space: nowrap so a highlight never wraps mid-stroke.

Motion

ElementTriggerPropertyFrom → ToDurationEasingDelay
Highlight paths.runstroke-dashoffset1 → 0650ms--ease-pen300 / 1900 / 2250
Underline path.runstroke-dashoffset1 → 0650ms--ease-pen1400
Circle path.runstroke-dashoffset1 → 0820ms--ease-pen900
Arrow path.runstroke-dashoffset1 → 0650ms--ease-pen2500
Note lines.runclip-path inset0 100% 0 0 → 0 0 0 0700ms steps(14)—2880
Single markhoversame draw keyframesreplay650 / 820--ease-pen0
Full sequenceReplay clickre-add .runfull timeline———

CSS:

.mk path { stroke-dasharray: 1; stroke-dashoffset: 0; }
.run .mk path, .run .note path {
  animation: draw var(--draw) var(--ease-pen) backwards;
  animation-delay: var(--d);
}
.run .ci path { animation-duration: 820ms; }
@keyframes draw { from { stroke-dashoffset: 1 } to { stroke-dashoffset: 0 } }

backwards keeps dashoffset at 1 during the delay so marks are invisible until their turn.

States

  • Running (default): .run on #stage. All delays honour --d.
  • Hover a mark: that path retraces from empty. Other marks stay finished.
  • Replay: sequence starts over from delay 0 of the first highlight.
  • Subscribe hover: background --ink, colour --paper.
  • Replay hover: border --ink.
  • Focus-visible: 2px --pen outline, 3px offset (Subscribe, Replay).

Accessibility

  • Decorative SVGs aria-hidden="true". The quote is real text in a <blockquote> — marks do not replace letters.
  • Initials disc aria-hidden. Legend aria-hidden (the footer sentence explains hover).
  • Replay is a real <button type="button"> with visible “Replay” text and a 16px undo icon.
  • Subscribe is a link, 40px-class padding (10 16) plus 1px border, radius 999.
  • Tab order: Subscribe → Replay. Quote is not interactive; hover-redraw is pointer-only (Replay is the keyboard equivalent for the full sequence).
  • Contrast: navy on paper is high; --ink-2 for 12–15px meta; yellow marker is a graphic over black type (multiply), not text colour. Red pen on paper ~4.7:1 at 2.2px — it is decoration around already-readable type.
  • Hit target: Replay 40px. Mark hover uses the word boxes (large display type).

Responsive rules

  • ≥ 1280: quote 58px, main 1fr 250px, page pad 0 56 0 96, rule at 55px.
  • 1024–1279: quote 48px; keep two columns.
  • 768–1023: quote 40px; stack the note under the byline (one column); left pad 64px; rule at 32px.
  • < 640: quote 32px; header pill may wrap; Replay stays 40px. Highlights stay nowrap — if a phrase would overflow, drop quote size before allowing a wrap through a .mk.
  • Reduced motion: no dash or clip animation; every mark is drawn on first paint.

Acceptance checklist

  • Quote is 58px Newsreader / 1.16 / −0.022em / opsz 72, on paper with a 1px rule at x=55.
  • Highlight strokes are 9px #FFD43B, opacity .92, mix-blend-mode: multiply, behind the glyphs.
  • Circle and underline are #D9402A 2.4 / 2.2px and use the paths in Structure.
  • Sequence delays are 300, 900, 1400, 1900, 2250, 2500ms; circle draws in 820ms, others 650ms, easing cubic-bezier(.65,0,.35,1).
  • Note is Caveat 30/600, −5deg, with the 150×84 arrow; words clip in 700ms steps(14) after the arrow starts + 380ms.
  • Hovering a mark retraces only that stroke; Replay retraces the whole timeline.
  • pathLength="1" + stroke-dasharray: 1 (not a huge computed length).
  • Masthead “The Margin”, Issue 31 · Autumn 2026, Subscribe — €48/yr, Ines Okafor, 2 October 2026.
  • Reduced motion shows finished marks with no draw.
  • Focus rings are 2px pen-red on Subscribe and Replay.
  • Highlight spans do not wrap (white-space: nowrap).
  • Circle SVG sits above the italic; highlight SVGs sit behind.

Implementation notes

pathLength="1" so every curve, regardless of pixel length, uses the same 0–1 dash math:

<svg viewBox="0 0 100 10" preserveAspectRatio="none" aria-hidden="true">
  <path pathLength="1" d="M1 6 C 25 4.6, 60 5.8, 99 4.2"/>
</svg>

Restart a single stroke on hover without restarting the page:

m.addEventListener('mouseenter', () => {
  const p = m.querySelector('path'); if (!p) return;
  p.style.animation = 'none';
  void p.getBBox();
  p.style.animation = 'draw ' + (m.classList.contains('ci') ? 820 : 650)
    + 'ms cubic-bezier(.65,0,.35,1) 0ms backwards';
});

Replay the sequence by cycling the class (inline styles from hover must be cleared first):

btn.addEventListener('click', () => {
  stage.querySelectorAll('path, .note span').forEach((p) => { p.style.animation = ''; });
  stage.classList.remove('run');
  void stage.offsetWidth;
  stage.classList.add('run');
});

Common mistakes: a CSS background underline instead of a stretched path (it will be ruler-straight); mix-blend-mode on the text instead of .hl (the type will stain); animating stroke-dashoffset from a JS-measured length while also setting pathLength="1" (the two fight); putting the SVG after the words in DOM without z-index: -1 so the yellow covers serifs; using forwards instead of backwards (marks flash finished during their delay).

Details

Palette
Type
Newsreader · Caveat
Motion
Rich motion
Build
One session
Designed bySusan AcharyaKathmandu

Studied

Susan Acharya

Draw one scene and let it move a little. The words stay calm on top.

All sources