UI Components Easy
Hyper Text
Text that scrambles through random characters before settling on the final text, creating a decode or matrix-style reveal effect. Triggers on hover.
Open in Lab
MCP
css javascript vue svelte
Targets: TS JS HTML React Vue Svelte
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background: #0a0a0a;
color: #f1f5f9;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 3rem;
}
.hyper-text {
font-family: "SF Mono", "Fira Code", "Cascadia Code", "Consolas", monospace;
font-size: clamp(2rem, 6vw, 4.5rem);
font-weight: 700;
letter-spacing: 0.05em;
color: #f1f5f9;
cursor: default;
display: inline-flex;
gap: 0;
}
.hyper-text .char {
display: inline-block;
min-width: 0.6em;
text-align: center;
transition: color 0.15s ease;
}
/* Scrambling state -- dimmer color */
.hyper-text .char.scrambling {
color: #a78bfa;
text-shadow: 0 0 8px rgba(167, 139, 250, 0.5);
}
/* Resolved state -- white with subtle glow */
.hyper-text .char.resolved {
color: #f1f5f9;
text-shadow: 0 0 4px rgba(241, 245, 249, 0.2);
}
/* Label */
.hyper-hint {
font-size: 0.75rem;
color: #333;
letter-spacing: 0.15em;
text-transform: uppercase;
}/**
* Hyper Text
* On hover (or on load), scrambles characters through random glyphs
* before resolving to the target text one character at a time.
*/
(function () {
const container = document.querySelector(".hyper-text");
if (!container) return;
const targetText = container.dataset.text || "STEALTHIS";
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%&*";
const scrambleSpeed = parseInt(container.dataset.speed, 10) || 50;
const resolveDelay = parseInt(container.dataset.resolveDelay, 10) || 80;
let isAnimating = false;
let intervalId = null;
function init() {
container.innerHTML = "";
for (let i = 0; i < targetText.length; i++) {
const span = document.createElement("span");
span.classList.add("char", "resolved");
span.textContent = targetText[i] === " " ? "\u00A0" : targetText[i];
container.appendChild(span);
}
}
function scramble() {
if (isAnimating) return;
isAnimating = true;
const chars = container.querySelectorAll(".char");
const resolvedAt = new Array(chars.length).fill(false);
let resolvedCount = 0;
// Mark all as scrambling
chars.forEach((c) => {
c.classList.remove("resolved");
c.classList.add("scrambling");
});
// Start resolving characters left to right on a stagger
chars.forEach((_, i) => {
setTimeout(
() => {
resolvedAt[i] = true;
resolvedCount++;
},
resolveDelay * (i + 1)
);
});
intervalId = setInterval(() => {
chars.forEach((span, i) => {
if (targetText[i] === " ") {
span.textContent = "\u00A0";
return;
}
if (resolvedAt[i]) {
span.textContent = targetText[i];
span.classList.remove("scrambling");
span.classList.add("resolved");
} else {
span.textContent = alphabet[Math.floor(Math.random() * alphabet.length)];
}
});
if (resolvedCount >= chars.length) {
clearInterval(intervalId);
isAnimating = false;
}
}, scrambleSpeed);
}
init();
// Scramble on load
setTimeout(scramble, 400);
// Scramble on hover
container.addEventListener("mouseenter", scramble);
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hyper Text</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div
class="hyper-text"
data-text="STEALTHIS"
data-speed="50"
data-resolve-delay="80"
></div>
<p class="hyper-hint">Hover to scramble</p>
<script src="script.js"></script>
</body>
</html>import { useState, useEffect, useCallback, useRef } from "react";
interface HyperTextProps {
text?: string;
scrambleSpeed?: number;
resolveDelay?: number;
}
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%&*";
export default function HyperText({
text = "STEALTHIS",
scrambleSpeed = 50,
resolveDelay = 80,
}: HyperTextProps) {
const [displayChars, setDisplayChars] = useState<string[]>(text.split(""));
const [resolvedFlags, setResolvedFlags] = useState<boolean[]>(new Array(text.length).fill(true));
const isAnimating = useRef(false);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const timeoutRefs = useRef<ReturnType<typeof setTimeout>[]>([]);
const scramble = useCallback(() => {
if (isAnimating.current) return;
isAnimating.current = true;
const resolved = new Array(text.length).fill(false);
setResolvedFlags([...resolved]);
// Clear previous timeouts
timeoutRefs.current.forEach(clearTimeout);
timeoutRefs.current = [];
// Stagger resolve
text.split("").forEach((_, i) => {
const tid = setTimeout(
() => {
resolved[i] = true;
setResolvedFlags([...resolved]);
},
resolveDelay * (i + 1)
);
timeoutRefs.current.push(tid);
});
// Scramble loop
if (intervalRef.current) clearInterval(intervalRef.current);
intervalRef.current = setInterval(() => {
const next = text.split("").map((ch, i) => {
if (ch === " ") return "\u00A0";
if (resolved[i]) return ch;
return ALPHABET[Math.floor(Math.random() * ALPHABET.length)];
});
setDisplayChars(next);
if (resolved.every(Boolean)) {
if (intervalRef.current) clearInterval(intervalRef.current);
setDisplayChars(text.split(""));
isAnimating.current = false;
}
}, scrambleSpeed);
}, [text, scrambleSpeed, resolveDelay]);
// Scramble on mount
useEffect(() => {
const tid = setTimeout(scramble, 400);
return () => {
clearTimeout(tid);
if (intervalRef.current) clearInterval(intervalRef.current);
timeoutRefs.current.forEach(clearTimeout);
};
}, [scramble]);
return (
<div
style={{
minHeight: "100vh",
background: "#0a0a0a",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexDirection: "column",
gap: "3rem",
}}
>
<div
onMouseEnter={scramble}
style={{
fontFamily: '"SF Mono", "Fira Code", "Cascadia Code", "Consolas", monospace',
fontSize: "clamp(2rem, 6vw, 4.5rem)",
fontWeight: 700,
letterSpacing: "0.05em",
color: "#f1f5f9",
cursor: "default",
display: "inline-flex",
}}
>
{displayChars.map((ch, i) => (
<span
key={i}
style={{
display: "inline-block",
minWidth: "0.6em",
textAlign: "center",
color: resolvedFlags[i] ? "#f1f5f9" : "#a78bfa",
textShadow: resolvedFlags[i]
? "0 0 4px rgba(241,245,249,0.2)"
: "0 0 8px rgba(167,139,250,0.5)",
transition: "color 0.15s ease",
}}
>
{ch === " " ? "\u00A0" : ch}
</span>
))}
</div>
<p
style={{
fontSize: "0.75rem",
color: "#333",
letterSpacing: "0.15em",
textTransform: "uppercase",
}}
>
Hover to scramble
</p>
</div>
);
}<script setup>
import { ref, onMounted, onUnmounted } from "vue";
const props = defineProps({
text: { type: String, default: "STEALTHIS" },
scrambleSpeed: { type: Number, default: 50 },
resolveDelay: { type: Number, default: 80 },
});
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%&*";
const displayChars = ref(props.text.split(""));
const resolvedFlags = ref(new Array(props.text.length).fill(true));
let isAnimating = false;
let intervalId = null;
let timeoutIds = [];
let mountTimeout = null;
function scramble() {
if (isAnimating) return;
isAnimating = true;
const resolved = new Array(props.text.length).fill(false);
resolvedFlags.value = [...resolved];
timeoutIds.forEach(clearTimeout);
timeoutIds = [];
props.text.split("").forEach((_, i) => {
const tid = setTimeout(
() => {
resolved[i] = true;
resolvedFlags.value = [...resolved];
},
props.resolveDelay * (i + 1)
);
timeoutIds.push(tid);
});
if (intervalId) clearInterval(intervalId);
intervalId = setInterval(() => {
const next = props.text.split("").map((ch, i) => {
if (ch === " ") return "\u00A0";
if (resolved[i]) return ch;
return ALPHABET[Math.floor(Math.random() * ALPHABET.length)];
});
displayChars.value = next;
if (resolved.every(Boolean)) {
if (intervalId) clearInterval(intervalId);
displayChars.value = props.text.split("");
isAnimating = false;
}
}, props.scrambleSpeed);
}
onMounted(() => {
mountTimeout = setTimeout(scramble, 400);
});
onUnmounted(() => {
if (mountTimeout) clearTimeout(mountTimeout);
if (intervalId) clearInterval(intervalId);
timeoutIds.forEach(clearTimeout);
});
</script>
<template>
<div class="hyper-text-wrapper">
<div class="hyper-text" @mouseenter="scramble">
<span
v-for="(ch, i) in displayChars"
:key="i"
class="char"
:style="{
color: resolvedFlags[i] ? '#f1f5f9' : '#a78bfa',
textShadow: resolvedFlags[i] ? '0 0 4px rgba(241,245,249,0.2)' : '0 0 8px rgba(167,139,250,0.5)',
}"
>
{{ ch === ' ' ? '\u00A0' : ch }}
</span>
</div>
<p class="hint">Hover to scramble</p>
</div>
</template>
<style scoped>
.hyper-text-wrapper {
min-height: 100vh;
background: #0a0a0a;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 3rem;
}
.hyper-text {
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
font-size: clamp(2rem, 6vw, 4.5rem);
font-weight: 700;
letter-spacing: 0.05em;
color: #f1f5f9;
cursor: default;
display: inline-flex;
}
.char {
display: inline-block;
min-width: 0.6em;
text-align: center;
transition: color 0.15s ease;
}
.hint {
font-size: 0.75rem;
color: #333;
letter-spacing: 0.15em;
text-transform: uppercase;
margin: 0;
}
</style><script>
import { onMount, onDestroy } from "svelte";
export let text = "STEALTHIS";
export let scrambleSpeed = 50;
export let resolveDelay = 80;
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%&*";
let displayChars = text.split("");
let resolvedFlags = new Array(text.length).fill(true);
let isAnimating = false;
let intervalId = null;
let timeoutIds = [];
let mountTimeout = null;
function scramble() {
if (isAnimating) return;
isAnimating = true;
const resolved = new Array(text.length).fill(false);
resolvedFlags = [...resolved];
// Clear previous timeouts
timeoutIds.forEach(clearTimeout);
timeoutIds = [];
// Stagger resolve
text.split("").forEach((_, i) => {
const tid = setTimeout(
() => {
resolved[i] = true;
resolvedFlags = [...resolved];
},
resolveDelay * (i + 1)
);
timeoutIds.push(tid);
});
// Scramble loop
if (intervalId) clearInterval(intervalId);
intervalId = setInterval(() => {
const next = text.split("").map((ch, i) => {
if (ch === " ") return "\u00A0";
if (resolved[i]) return ch;
return ALPHABET[Math.floor(Math.random() * ALPHABET.length)];
});
displayChars = next;
if (resolved.every(Boolean)) {
if (intervalId) clearInterval(intervalId);
displayChars = text.split("");
isAnimating = false;
}
}, scrambleSpeed);
}
onMount(() => {
mountTimeout = setTimeout(scramble, 400);
});
onDestroy(() => {
if (mountTimeout) clearTimeout(mountTimeout);
if (intervalId) clearInterval(intervalId);
timeoutIds.forEach(clearTimeout);
});
</script>
<div class="hyper-text-wrapper">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="hyper-text" on:mouseenter={scramble}>
{#each displayChars as ch, i}
<span
class="char"
style="color: {resolvedFlags[i] ? '#f1f5f9' : '#a78bfa'}; text-shadow: {resolvedFlags[i] ? '0 0 4px rgba(241,245,249,0.2)' : '0 0 8px rgba(167,139,250,0.5)'};"
>
{ch === ' ' ? '\u00A0' : ch}
</span>
{/each}
</div>
<p class="hint">Hover to scramble</p>
</div>
<style>
.hyper-text-wrapper {
min-height: 100vh;
background: #0a0a0a;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 3rem;
}
.hyper-text {
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
font-size: clamp(2rem, 6vw, 4.5rem);
font-weight: 700;
letter-spacing: 0.05em;
color: #f1f5f9;
cursor: default;
display: inline-flex;
}
.char {
display: inline-block;
min-width: 0.6em;
text-align: center;
transition: color 0.15s ease;
}
.hint {
font-size: 0.75rem;
color: #333;
letter-spacing: 0.15em;
text-transform: uppercase;
margin: 0;
}
</style>Hyper Text
A text effect that scrambles characters through random glyphs before resolving to the final string, producing a decode/matrix-style reveal. Triggers on hover or on initial load.
How it works
Each character slot cycles through random characters from a configurable alphabet at a set interval. Characters are “locked in” from left to right, one at a time, until the full string is revealed.
Features
- Hover trigger — scramble restarts on mouse enter
- Sequential resolve — characters lock in left-to-right
- Configurable speed — control scramble interval and resolve delay
- Monospace styling — clean fixed-width presentation