Hover or focus to roll the label up and reveal a duplicate.
export function textRoll(el) {
const _reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
const _duration = _reducedMotion ? 0.01 : 0.35
const text = el.textContent ?? ''
el.setAttribute('aria-label', text)
el.innerHTML = ''
const inner = document.createElement('span')
inner.className = 'k-roll-inner'
inner.append(
...[0, 1].map((i) => {
const row = document.createElement('span')
row.textContent = text
if (i) row.style.color = 'var(--accent)'
return row
}),
)
const roll = document.createElement('span')
roll.className = 'k-roll'
roll.setAttribute('aria-hidden', 'true')
roll.append(inner)
el.append(roll)
const easing = 'linear(0.0000,0.0987,0.1899,0.2740,0.3513,0.4224,0.4874,0.5467,0.6008,0.6499,0.6943,0.7343,0.7703,0.8025,0.8311,0.8566,0.8790,0.8987,0.9159,0.9308,0.9437,0.9546,0.9639,0.9717,0.9781,0.9834,0.9877,0.9910,0.9937,0.9957,0.9972,0.9982,0.9990,0.9994,0.9997,0.9999,1.0000,1.0000,1.0000,1.0000)'
let current = null
const animateTo = (yPercent) => {
const from = getComputedStyle(inner).transform
current?.cancel()
current = inner.animate([{ transform: from }, { transform: `translateY(${yPercent}%)` }], {
duration: _duration * 1000,
easing,
fill: 'forwards',
})
}
const enter = () => animateTo(-50)
const leave = () => animateTo(0)
const ons = [['pointerenter', enter], ['pointerleave', leave], ['focus', enter], ['blur', leave]]
ons.forEach(([e, fn]) => el.addEventListener(e, fn))
return () => {
current?.cancel()
ons.forEach(([e, fn]) => el.removeEventListener(e, fn))
el.textContent = text
}
}
/* CSS */
.k-roll {
display: inline-block;
block-size: 1lh;
overflow: hidden;
line-height: 1;
vertical-align: bottom;
}
.k-roll-inner {
display: block;
}
.k-roll-inner > span {
display: block;
block-size: 1lh;
}