webdevpuneet.com

Cursor-Follow Underline Draw — Link Hover Snippet

Cursor-Follow Underline Draw · Text Animation · Plain HTML, CSS & JS · Live preview

Copy & paste

Source Code

<div class="lu-stage">
  <p class="lu-hint">Move your cursor across a link — the underline draws in from wherever you entered</p>
  <nav class="lu-nav">
    <a href="#" class="lu-link">Home</a>
    <a href="#" class="lu-link">Products</a>
    <a href="#" class="lu-link">Documentation</a>
    <a href="#" class="lu-link">Pricing</a>
    <a href="#" class="lu-link">Contact</a>
  </nav>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Underline split into two pseudo-elements, each anchored to a shared CSS custom property
mouseenter reads e.clientX against getBoundingClientRect for the true entry x-position
Both halves grow to the link exact edges at the same instant, from any entry point
Symmetric entry (link center) draws outward in both directions simultaneously
cubic-bezier(0.22,1,0.36,1) ease-out gives the draw a fast, decisive feel
mouseleave retracts both halves back to zero width with the same easing
No JS animation loop — CSS transition handles all interpolation
Export as HTML file, React JSX, or React + Tailwind CSS
Mobile (375px), Tablet (768px), Desktop device preview buttons
Live split-pane editor — preview updates as you type

About this UI Snippet

Cursor-Follow Underline Draw — Split Pseudo-Element Underline Anchored to mouseenter X

Screenshot of the Cursor-Follow Underline Draw snippet rendered live

Most animated link underlines grow from a fixed edge — always left-to-right, regardless of where the cursor entered. This effect instead draws the underline outward from the exact horizontal point where the pointer crossed into the link, so a link entered from the right visibly grows rightward-to-leftward and one entered dead-center draws in both directions symmetrically. It reads as far more "aware" of the cursor than a standard animated underline.

Splitting the underline into two independent halves

Instead of one pseudo-element, the link gets two: ::before anchored to grow toward the left edge, and ::after anchored to grow toward the right edge. Both halves share a single anchor point — a CSS custom property --lu-x — using right: calc(100% - var(--lu-x)) on ::before and left: var(--lu-x) on ::after. Whatever --lu-x is set to, both halves start from that exact pixel and grow away from it in opposite directions.

Reading the entry point from mouseenter

The mouseenter listener receives the native MouseEvent, from which e.clientX - rect.left gives the cursor's x-offset within the link's own getBoundingClientRect() box — not the viewport, the link itself. That offset becomes entryX, converted to a percentage (--lu-x) for positioning the anchor, and also used directly in pixels to compute exactly how far each half must travel: --lu-left-w is the distance to the left edge, --lu-right-w is the distance to the right edge.

Why explicit widths instead of a flat 50% on each half

An early, simpler version of this effect just set both halves to width: 50% on hover, which only looks correct when the entry point happens to be the exact center of the link — asymmetric entry points would produce two unequal-looking bars. Setting --lu-left-w and --lu-right-w to the actual pixel distances measured from the real entry point makes both halves finish growing at exactly the same instant, at exactly the link's true edges, no matter where the cursor came in.

The easing

Both halves transition their width with cubic-bezier(0.22, 1, 0.36, 1) — a snappy ease-out that front-loads the motion, so the line appears to shoot outward from the cursor rather than crawl. mouseleave resets both custom properties to 0px, and the same transition eases the retreat back to nothing (from whichever edges they'd reached).

Applying it to your own nav

Any <a> with position: relative and padding-bottom for the underline's vertical offset can use this — the JS attaches to any element matching .lu-link and requires no other markup. This pairs well as a drop-in replacement anywhere a static text-decoration: underline currently sits, for a nav bar, a footer link list, or inline links inside body copy.

Step by step

How to Use

  1. 1
    Hover a link from different sidesMove your cursor into each nav link from the left, right, or middle — notice the underline always draws outward from your actual entry point.
  2. 2
    Move the cursor outLeave the link and the underline retracts back to nothing using the same easing curve.
  3. 3
    Change the underline color or thicknessEdit the background and height on the .lu-link::before / ::after rules in the CSS panel.
  4. 4
    Adjust the draw speedChange the 0.3s duration or the cubic-bezier easing on the transition: width rule.
  5. 5
    Apply to your own nav linksAdd class lu-link to any anchor with position: relative — the JS listener attaches automatically via querySelectorAll.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a React + Tailwind version.

Real-world uses

Common Use Cases

Primary site navigation links
Replace a static underline or color-change hover with this cursor-aware draw for a nav bar that feels more responsive to how users actually move their mouse.
ARTICLE
Inline links inside long-form body copy
Apply to inline text links in articles or docs so the underline draws precisely from where a reader hovers, rather than a generic left-to-right sweep.
Learn mouseenter coordinate math
A clean example of converting a raw MouseEvent into an element-relative offset with getBoundingClientRect — a technique used across dozens of cursor-driven effects.
Footer link lists and sitemaps
A subtle but noticeably more polished hover state for dense footer link columns compared to a flat color change.
Portfolio and agency site link styling
Pairs well with a signature site-wide interaction language alongside effects like the magnetic button.
Design system link component
Standardize this as the default link hover treatment across a design system for a consistently premium feel on every text link.

Got questions?

Frequently Asked Questions

The mouseenter event carries e.clientX, the cursor position in viewport coordinates. Subtracting rect.left (from the link's getBoundingClientRect) converts that to a position relative to the link itself, which becomes the anchor point for both underline halves.

A single underline can only grow from one fixed edge. Splitting it into a left-growing ::before and a right-growing ::after, both anchored at the same entry point, lets the line expand in both directions at once from wherever the cursor actually entered.

A flat 50% only looks symmetric when the entry point happens to be dead-center. Computing the real distance from the entry point to each edge (--lu-left-w and --lu-right-w) makes both halves finish exactly at the link's true edges regardless of where the cursor came in.

It is designed for single-line inline-block links. For links that wrap across multiple lines, getBoundingClientRect returns the bounding box of the whole wrapped element, which can make the underline position look off on the second line — keep wrapped links to a plain hover state instead.

Edit the height and background properties on the .lu-link::before and ::after rules in the CSS panel — both halves should stay in sync since they share the same visual style.

Yes. Click "JSX" for a React component. Attach an onMouseEnter handler that reads e.clientX against the target's getBoundingClientRect and calls element.style.setProperty for the three custom properties, plus onMouseLeave to reset them.

Design your website