data-underline

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.

Getting started

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>
Variants

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"
Opacity

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"
Offset

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"
Replay

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"
Smart defaults

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.

Dynamic

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.

Usage

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.

AttributeValuesNotes
data-underline"" · brush · double · highlightTurns the effect on. Any other value falls back to brush.
data-underline-triggerscroll · load · hover · manualDefault is scroll, or hover on links and buttons.
data-underline-delaymsWait before drawing.
data-underline-durationmsOverride. By default duration scales with stroke length.
data-underline-opacity0–1 or %Per-element opacity. Or put alpha in the color itself: #d9c7ff80.
data-underline-offsetunitless · em · px · remY offset for this element. Positive moves the stroke down. Overrides --underline-offset, which defaults to 0.
data-underline-replay-hovertrue · falseAfter 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-seednumberLock 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