<div>
<p>Please Hover Below</p>
<p>Mount Everest</p>
<p>Kangchenjunga</p>
<p>Lhotse</p>
<p>Chomo Lonzo</p>
<p>Nanga Parbat</p>
<p>Cho Oyu</p>
</div>
@import url("https://fonts.googleapis.com/css2?family=VT323&display=swap");
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--black: #23232b;
--blue: #2ecee0;
}
html,
body {
display: grid;
place-content: center;
min-height: 100dvh;
font-family: "Montserrat", "Noto Sans TC", sans-serif;
font-optical-sizing: auto;
font-weight: 400;
font-style: normal;
background-color: var(--black);
color: var(--white);
}
a {
text-decoration: none;
}
a,
a:hover,
a:visited {
color: var(--white);
}
ul,
ol {
list-style: none;
}
button {
border: none;
outline: none;
}
div {
--blue-10: color-mix(in srgb, var(--blue), transparent 90%);
--blue-50: color-mix(in srgb, var(--blue), transparent 50%);
padding: 32px 64px;
display: flex;
flex-direction: column;
gap: 8px;
background: linear-gradient(0deg, var(--blue-50), transparent),
linear-gradient(var(--blue-10) 1px, var(--black) 2px) center/100% 3px;
}
p {
font-family: "VT323", monospace;
letter-spacing: 1px;
font-size: 36px;
}
p {
--color: var(--blue);
color: var(--color);
cursor: pointer;
}
const textEls = document.querySelectorAll("p");
textEls.forEach((textEl) => {
splitText(textEl, { charClass: "char" });
textEl.addEventListener("mouseenter", hoverTextAnimation);
});
/**
* @typedef {object} SplitTextOptions
* @property {'words' | 'chars'} type
* @property {string?} wordClass
* @property {string?} charClass
*
* @param {HTMLElement} element
* @param {SplitTextOptions} options
*/
function splitText(element, { type = "chars", wordClass, charClass } = {}) {
const text = element.innerText;
let splitArray;
if (type === "words") {
splitArray = text
.split(" ")
.map((word) => `<span class="${wordClass}">${word}</span>`);
} else if (type === "chars") {
splitArray = text
.split("")
.map((char) => `<span class="${charClass}">${char}</span>`);
} else {
throw new Error("Invalid split type. Use 'words' or 'chars'.");
}
element.innerHTML = splitArray.join(type === "words" ? " " : "");
}
/**
*
* @typedef {object} HoverTextAnimationOptions
* @property {number} charDuration
* @property {number} charDelay
* @property {number} repeatTimes
*
* @param {Event} e
* @param {HoverTextAnimationOptions} options
*/
function hoverTextAnimation(
e,
{ charDelay = 50, charFreq = 200, randomCharRepeats = 2 } = {}
) {
const text = e.target.closest("p");
if (text.classList.contains("is-animated")) return;
const chars = text.querySelectorAll(".char");
text.classList.add("is-animated");
chars.forEach((char, index) => {
createCharAnimation(char, index, {
charDelay,
charFreq,
randomCharRepeats
});
});
const totalDuration =
charFreq * (randomCharRepeats + 1) + chars.length * charDelay;
setTimeout(() => {
text.classList.remove("is-animated");
}, totalDuration);
}
/**
* @param {HTMLElement} char
* @param {number} index
* @param {HoverTextAnimationOptions} options
*/
function createCharAnimation(
char,
index,
{ charDelay, charFreq, randomCharRepeats }
) {
const initText = char.innerText;
const delay = charDelay * index;
// initial animation
char.style.setProperty("opacity", 0);
// first animation
setTimeout(() => {
char.style.setProperty("opacity", 1);
char.style.setProperty("background", "var(--color)");
}, charFreq - charDelay + delay);
for (let i = 1; i <= randomCharRepeats; i++) {
setTimeout(() => {
i == 1 && char.style.setProperty("background", "transparent");
char.innerText = randomLetterAndSymbol();
}, charFreq * i + delay);
}
// last animation
const totalDuration = charFreq * (randomCharRepeats + 1) + delay;
setTimeout(() => {
char.innerText = initText;
}, totalDuration);
}
function randomLetterAndSymbol() {
const chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()";
return chars.charAt(Math.floor(Math.random() * chars.length));
}
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.