Living Gateways to the World's Most Coveted Escapes.
Add one attribute to any word, link or phrase. Every stroke is generated fresh — its own tilt, pressure and ragged edge — then drawn in like a marker.
Two files, then one attribute
Link the stylesheet and the script, then add data-underline to any word, link, or phrase.
<link rel="stylesheet" href="https://hand-drawn-underline-demo-site.pages.dev/underline.css"> <script src="https://hand-drawn-underline-demo-site.pages.dev/underline.js"></script> <span data-underline>Escapes.</span>
Three ways to mark it
The attribute value picks the style. Leave it empty for the default brush stroke.
A slow morning in the Dolomites
data-underline
Nothing feels quite like it
data-underline="double"
Private villas with ocean views
data-underline="highlight"
Always behind the text, at any strength
Ink sits behind every letter in the surrounding text, so a highlight's overshoot never covers the word before it. Set transparency with an 8-digit hex color, or with data-underline-opacity.
An island known for its slow sunsets
style="--underline-color: #ffb38a8c"
Every room opens to the garden
data-underline-opacity="0.45"
Nudge the stroke up or down
A positive --underline-offset moves the stroke down, a negative value lifts it. A unitless number or em is the text's own font size; px and rem are converted. data-underline-offset overrides the variable on one element. The default is 0, and the bar at the bottom sets it for the whole page.
Lifted just above the baseline
data-underline-offset="-0.15em"
The default sits on the baseline
--underline-offset: 0
Dropped further down
data-underline-offset="0.45rem"
Hover a finished stroke to draw it again
After a scroll, load, or manual underline finishes, hovering it — or the link, button, or data-underline-hover parent around it — wipes the stroke away and draws it back in. That replay is on by default. data-underline-replay-hover="false" leaves the finished stroke still. Underlines that already draw on hover are left alone.
Hover this stroke once it lands
Replays by default.
This one stays put
data-underline-replay-hover="false"
Links draw on hover, phrases follow the line
Links get a hover trigger automatically and wipe away when you leave. Long phrases that wrap get one stroke per line, drawn one after the other as if by hand.
Wake up in a Kyoto ryokan, lunch at a cliffside taverna, and end the day on the deck of a gulet.
Hover or tab to the links.
Some places stay with you. The kind of light that makes you forget to check the time is one of them.
Resize the window — the strokes re-flow with the text.
Add the attribute at runtime
A MutationObserver picks up any element that gains (or loses) data-underline — CMS lists, Finsweet attributes, tabs, whatever renders later. Click words below to toggle the attribute live.
From hidden coves in Mallorca to quiet terraces above Lake Como, we curate stays that feel less like hotels and more like belonging somewhere.
Each click sets or removes data-underline on a <span>. Hover a finished stroke to redraw it.
Drop-in for Webflow
Link underline.css and underline.js, then add custom attributes in the Designer. data-underline is the only one you need.
| Attribute | Values | Notes |
|---|---|---|
data-underline | "" · brush · double · highlight | Turns the effect on. Any other value falls back to brush. |
data-underline-trigger | scroll · load · hover · manual | Default is scroll, or hover on links and buttons. |
data-underline-delay | ms | Wait before drawing. |
data-underline-duration | ms | Override. By default duration scales with stroke length. |
data-underline-opacity | 0–1 or % | Per-element opacity. Or put alpha in the color itself: #d9c7ff80. |
data-underline-offset | unitless · em · px · rem | Y offset for this element. Positive moves the stroke down. Overrides --underline-offset, which defaults to 0. |
data-underline-replay-hover | true · false | After a scroll, load, or manual stroke finishes, hover wipes it and draws it again. Default is on. false, 0, off, or no keeps it still. |
data-underline-seed | number | Lock a specific stroke shape. |
data-underline-hover | (on an ancestor) | Hover this parent to draw underlines inside it — e.g. a card. |
/* Style per element, per class, or globally */ .footer-link { --underline-color: #ffc8a6; --underline-thickness: .14; } .tag-mark { --underline-color: #d9c7ff80; } /* 8-digit hex = 50% alpha */ .soft { --underline-opacity: .6; } .lifted { --underline-offset: -0.08em; } /* + down. unitless = em, or px / rem */ // JS API (window.HandUnderline) HandUnderline.draw(el) // draw one (works with trigger="manual") HandUnderline.erase(el) // wipe it away HandUnderline.replay() // replay everything (or pass an element) HandUnderline.refresh() // regenerate after changing size/thickness vars