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.
| Section | Navigation and lists |
|---|---|
| Techniques | Pointer interaction, Text animation |
| Stack | Vanilla JS, CSS |
| Works with | React, Next.js, Vanilla |
| Dependencies | None |
| Status | tested |
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.
More elements
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.