Copied!
Navigation and lists

Hover font variants

A text index whose rows flip through serif, mono, outline, wide and italic styles while hovered or focused.

← Library

Element · Vanilla JS + CSSUpdated November 5, 2026

TextElementVanilla JS + CSS

Live preview needs JavaScript. The code below works without any dependencies.

Prompt for your AI assistant

Paste this into Claude, Cursor or ChatGPT. The settings update as you move the controls above.

Build "Hover font variants" for my site using the Enrichr Library element at https://enrichr.dev/elements/hover-font-variants.html.

Read the machine-readable spec and source at https://enrichr.dev/elements/hover-font-variants.json (markup, CSS, JavaScript, parameters).
Requirements: vanilla JavaScript and CSS, no new dependencies; keep the behaviour of the source unchanged; respect prefers-reduced-motion; work on touch screens; keep it accessible.

Settings:
- items: Selected work, Studio, Journal, Contact
- mode: cycle
- interval: 140
- color: #ffffff

Verify: reduced motion shows no movement, it works at 375px wide, there are no console errors, and scrolling stays smooth.

The same spec as JSON, for tools that read it directly: /elements/hover-font-variants.json.

Code

Mount it on an element that contains the markup. It returns an object with update(options) and destroy().

HTML

<ul class="ef-hfv"></ul>

CSS

.ef-hfv { list-style: none; margin: 0; padding: 0; }
.ef-hfv__row { display: flex; align-items: baseline; gap: 1rem; padding: 0.7rem 0; border-bottom: 1px solid currentColor; border-bottom-color: color-mix(in srgb, currentColor 25%, transparent); font-size: clamp(1.6rem, 4.6vw, 3rem); font-weight: 700; line-height: 1.1; cursor: pointer; outline: none; white-space: nowrap; overflow: hidden; }
.ef-hfv__n { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 0.34em; opacity: 0.6; font-weight: 500; }
.ef-hfv__row:focus-visible { outline: 2px solid currentColor; outline-offset: -2px; }
.ef-hfv__row.v-serif .ef-hfv__t { font-family: Georgia, "Times New Roman", serif; font-weight: 400; }
.ef-hfv__row.v-mono .ef-hfv__t { font-family: ui-monospace, Menlo, Consolas, monospace; font-weight: 500; letter-spacing: -0.04em; }
.ef-hfv__row.v-outline .ef-hfv__t { color: transparent; -webkit-text-stroke: 1.5px var(--ef-ink, #fff); }
.ef-hfv__row.v-wide .ef-hfv__t { letter-spacing: 0.22em; text-transform: uppercase; font-size: 0.8em; }
.ef-hfv__row.v-italic .ef-hfv__t { font-family: Georgia, "Times New Roman", serif; font-style: italic; font-weight: 400; }

JavaScript

// Hover font variants: a text index whose rows flip through different type styles (serif, mono, outline, wide, italic) while hovered or focused.
// It uses system font stacks so nothing needs to be downloaded; point the variant classes at your own fonts for a real brand.
// mount(root, { items (comma separated), mode ("cycle" or one of serif, mono, outline, wide, italic), interval (ms between flips), color })
function mount(root, o) {
  var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  var ul = root.querySelector(".ef-hfv");
  var VARS = ["serif", "mono", "outline", "wide", "italic"];
  var opts = {}, rows = [];

  function setVariant(li, name) {
    VARS.forEach(function (v) { li.classList.remove("v-" + v); });
    if (name) li.classList.add("v-" + name);
  }
  function start(li, index) {
    stop(li);
    if (opts.mode !== "cycle") { setVariant(li, opts.mode); return; }
    var i = index % VARS.length;
    setVariant(li, VARS[i]);
    if (reduce) return; // one style on hover, no flicker
    li._t = setInterval(function () { i = (i + 1) % VARS.length; setVariant(li, VARS[i]); }, opts.interval);
  }
  function stop(li) { clearInterval(li._t); li._t = 0; setVariant(li, null); }

  function render() {
    rows.forEach(stop);
    ul.textContent = "";
    ul.style.color = opts.color;
    ul.style.setProperty("--ef-ink", opts.color);
    rows = opts.items.split(",").map(function (s) { return s.trim(); }).filter(Boolean).map(function (label, i) {
      var li = document.createElement("li");
      li.className = "ef-hfv__row";
      li.tabIndex = 0;
      var n = document.createElement("span"); n.className = "ef-hfv__n"; n.textContent = String(i + 1).padStart(2, "0");
      var t = document.createElement("span"); t.className = "ef-hfv__t"; t.textContent = label;
      li.appendChild(n); li.appendChild(t);
      li.addEventListener("mouseenter", function () { start(li, i); });
      li.addEventListener("mouseleave", function () { stop(li); });
      li.addEventListener("focus", function () { start(li, i); });
      li.addEventListener("blur", function () { stop(li); });
      ul.appendChild(li);
      return li;
    });
  }
  function update(next) {
    opts = Object.assign({ items: "Selected work, Studio, Journal, Contact", mode: "cycle", interval: 140, color: "#ffffff" }, opts, next || {});
    render();
  }
  update(o);
  return { update: update, destroy: function () { rows.forEach(stop); } };
}

// Usage
var fx = mount(document.querySelector("#my-root"), {"items":"Selected work, Studio, Journal, Contact","mode":"cycle","interval":140,"color":"#ffffff"});

How it works

Each row is a focusable list item. While the pointer or keyboard focus is on a row, a timer steps through a list of style classes, each of which changes the font stack, weight, spacing or fill of the row's text. When the pointer leaves, the classes are removed and the row returns to its normal style. The styles use system fonts so nothing downloads; replace the variant rules with your own typefaces for a branded version.

Accessibility and performance

Rows are focusable and the flip also runs on keyboard focus. The text stays real text, so the style change does not affect what a screen reader announces. Rapid style flipping can bother some people, so with reduced motion a row shows a single alternate style and does not cycle.

Only classes change, on a timer, and only while a row is hovered. No layout is measured. Row width can change with the style, so give rows a fixed width if neighbours must not move.

SectionNavigation and lists
TechniquesPointer interaction, Text animation
StackVanilla JS, CSS
Works withReact, Next.js, Vanilla
DependenciesNone
Statustested

Find this technique on a real site

Want to see this technique in the wild? Check any site with the free tools, then build your own version from this page.

Check which animation library a site uses · Extract a site's design tokens · Browse site breakdowns

Licence and provenance

Free to use in personal and client projects during the preview. Final licence terms will be published here before any paid tier starts; ask before reselling the code as a collection.

This is original code. The technique is a common one that we studied by measuring award-winning sites with Enrichr; no third-party code, shaders, fonts or images are used.

Marquee tickerEndless horizontal text that pauses on hover, with speed, gap and direction controls. Split text revealA headline whose words rise out of a mask one after another when it scrolls into view. Letter pop introEach letter overshoots its size and settles, staggered left to right, for a bold page intro.

FAQ

Does it load fonts?

No. It uses system font stacks. To use your own typefaces, load them normally and point the variant classes at them.

Can a row link somewhere?

Yes. Put an anchor inside the text span; the effect is on the row, not the link.

Why does it stop cycling with reduced motion?

Fast flickering text can be uncomfortable. People who ask for reduced motion see one alternate style on hover instead.