Text roll link
The label rolls up and a duplicate rolls in from below, per letter with a small stagger.
CSSJavaScriptIntermediate
<nav><a href="#">Projects</a><a href="#">Studio</a><a href="#">Contact</a></nav>
nav{display:flex;gap:36px;flex-wrap:wrap;justify-content:center}
a{color:#fff;text-decoration:none;font-size:clamp(1.6rem,6vw,2.4rem);font-weight:700;letter-spacing:-.02em;display:inline-flex;overflow:hidden;line-height:1.2}
.ch{display:inline-block;position:relative;transition:transform .5s cubic-bezier(.2,.8,.2,1);transition-delay:calc(var(--i) * 22ms)}
.ch::after{content:attr(data-c);position:absolute;left:0;top:100%;color:#f08a5d}
a:hover .ch,a:focus-visible .ch{transform:translateY(-100%)}
document.querySelectorAll('a').forEach(a => {
const text = a.textContent;
a.setAttribute('aria-label', text);
a.innerHTML = [...text].map((c, i) =>
`<span class="ch" style="--i:${i}" data-c="${c}" aria-hidden="true">${c === ' ' ? ' ' : c}</span>`).join('');
});
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Text roll</title>
<style>
*{box-sizing:border-box}
html,body{margin:0;height:100%}
body{display:grid;place-items:center;background:#140807;color:#fffffc;font:16px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif}
nav{display:flex;gap:36px;flex-wrap:wrap;justify-content:center}
a{color:#fff;text-decoration:none;font-size:clamp(1.6rem,6vw,2.4rem);font-weight:700;letter-spacing:-.02em;display:inline-flex;overflow:hidden;line-height:1.2}
.ch{display:inline-block;position:relative;transition:transform .5s cubic-bezier(.2,.8,.2,1);transition-delay:calc(var(--i) * 22ms)}
.ch::after{content:attr(data-c);position:absolute;left:0;top:100%;color:#f08a5d}
a:hover .ch,a:focus-visible .ch{transform:translateY(-100%)}
</style></head>
<body>
<nav><a href="#">Projects</a><a href="#">Studio</a><a href="#">Contact</a></nav>
<script>
document.querySelectorAll('a').forEach(a => {
const text = a.textContent;
a.setAttribute('aria-label', text);
a.innerHTML = [...text].map((c, i) =>
`<span class="ch" style="--i:${i}" data-c="${c}" aria-hidden="true">${c === ' ' ? ' ' : c}</span>`).join('');
});
</script>
</body></html>
Good for
Menu items and header links on bold, editorial sites.
Watch out for
Duplicate the text with a data attribute and hide the copy from screen readers so it is not read twice.
Accessibility
Wrap motion in @media (prefers-reduced-motion: reduce) and switch it off or shorten it for people who ask for less movement.