UI Components Easy
Text Highlighter
Text where words get highlighted one-by-one with a colored background sweep animation. Each word receives a sequential highlight effect from left to right.
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;
padding: 2rem;
}
.text-highlighter {
max-width: 640px;
font-size: clamp(1.4rem, 3vw, 2.2rem);
font-weight: 600;
line-height: 1.6;
color: rgba(241, 245, 249, 0.3);
cursor: pointer;
user-select: none;
}
.text-highlighter .word {
position: relative;
display: inline-block;
padding: 0 0.1em;
transition: color 0.3s ease;
}
/* The highlight background sweep */
.text-highlighter .word::before {
content: "";
position: absolute;
inset: 0;
background: rgba(167, 139, 250, 0.15);
border-radius: 4px;
transform: scaleX(0);
transform-origin: left;
transition: transform 0.4s cubic-bezier(0.22, 1, 0.36, 1);
z-index: -1;
}
/* Active state */
.text-highlighter .word.highlighted {
color: #f1f5f9;
}
.text-highlighter .word.highlighted::before {
transform: scaleX(1);
}
/* Replay hint */
.highlight-hint {
text-align: center;
margin-top: 2rem;
font-size: 0.75rem;
color: #333;
letter-spacing: 0.1em;
text-transform: uppercase;
}/**
* Text Highlighter
* Wraps each word in a <span>, then sequentially triggers a highlight
* animation on each one. Click to replay.
*/
(function () {
const container = document.querySelector(".text-highlighter");
if (!container) return;
const text =
container.dataset.text ||
"Design is not just what it looks like and feels like. Design is how it works. Every detail matters when crafting remarkable experiences.";
const delayPerWord = parseInt(container.dataset.delay, 10) || 120;
let timeouts = [];
function setup() {
// Clear any previous state
timeouts.forEach(clearTimeout);
timeouts = [];
container.innerHTML = "";
const words = text.split(/\s+/);
words.forEach((word, i) => {
const span = document.createElement("span");
span.classList.add("word");
span.textContent = word;
container.appendChild(span);
// Add a space after each word except the last
if (i < words.length - 1) {
container.appendChild(document.createTextNode(" "));
}
});
// Stagger highlight
const spans = container.querySelectorAll(".word");
spans.forEach((span, i) => {
const tid = setTimeout(() => {
span.classList.add("highlighted");
}, delayPerWord * i);
timeouts.push(tid);
});
}
setup();
// Replay on click
container.addEventListener("click", setup);
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Text Highlighter</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div>
<div
class="text-highlighter"
data-text="Design is not just what it looks like and feels like. Design is how it works. Every detail matters when crafting remarkable experiences."
data-delay="120"
></div>
<p class="highlight-hint">Click to replay</p>
</div>
<script src="script.js"></script>
</body>
</html>import { useState, useEffect, useCallback } from "react";
interface TextHighlighterProps {
text?: string;
delayPerWord?: number;
highlightColor?: string;
}
export default function TextHighlighter({
text = "Design is not just what it looks like and feels like. Design is how it works. Every detail matters when crafting remarkable experiences.",
delayPerWord = 120,
highlightColor = "rgba(167, 139, 250, 0.15)",
}: TextHighlighterProps) {
const words = text.split(/\s+/);
const [highlightedCount, setHighlightedCount] = useState(0);
const [key, setKey] = useState(0);
useEffect(() => {
setHighlightedCount(0);
const timeouts: ReturnType<typeof setTimeout>[] = [];
words.forEach((_, i) => {
const tid = setTimeout(() => {
setHighlightedCount(i + 1);
}, delayPerWord * i);
timeouts.push(tid);
});
return () => timeouts.forEach(clearTimeout);
}, [key, delayPerWord, words.length]);
const replay = useCallback(() => {
setKey((k) => k + 1);
}, []);
return (
<div
style={{
minHeight: "100vh",
background: "#0a0a0a",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "2rem",
}}
>
<div style={{ maxWidth: 640 }}>
<p
onClick={replay}
style={{
fontSize: "clamp(1.4rem, 3vw, 2.2rem)",
fontWeight: 600,
lineHeight: 1.6,
color: "rgba(241,245,249,0.3)",
cursor: "pointer",
userSelect: "none",
}}
>
{words
.map((word, i) => {
const active = i < highlightedCount;
return (
<span
key={`${key}-${i}`}
style={{
position: "relative",
display: "inline-block",
padding: "0 0.1em",
color: active ? "#f1f5f9" : undefined,
transition: "color 0.3s ease",
}}
>
{/* Highlight background */}
<span
style={{
position: "absolute",
inset: 0,
background: highlightColor,
borderRadius: 4,
transform: active ? "scaleX(1)" : "scaleX(0)",
transformOrigin: "left",
transition: "transform 0.4s cubic-bezier(0.22, 1, 0.36, 1)",
zIndex: -1,
}}
/>
{word}
</span>
);
})
.reduce<React.ReactNode[]>((acc, el, i) => {
if (i > 0) acc.push(" ");
acc.push(el);
return acc;
}, [])}
</p>
<p
style={{
textAlign: "center",
marginTop: "2rem",
fontSize: "0.75rem",
color: "#333",
letterSpacing: "0.1em",
textTransform: "uppercase",
}}
>
Click to replay
</p>
</div>
</div>
);
}<script setup>
import { ref, computed, watch, onUnmounted } from "vue";
const props = defineProps({
text: {
type: String,
default:
"Design is not just what it looks like and feels like. Design is how it works. Every detail matters when crafting remarkable experiences.",
},
delayPerWord: {
type: Number,
default: 120,
},
highlightColor: {
type: String,
default: "rgba(167, 139, 250, 0.15)",
},
});
const highlightedCount = ref(0);
const key = ref(0);
let timeouts = [];
const words = computed(() => props.text.split(/\s+/));
function cleanup() {
timeouts.forEach(clearTimeout);
timeouts = [];
}
function startAnimation() {
cleanup();
highlightedCount.value = 0;
words.value.forEach((_, i) => {
const tid = setTimeout(() => {
highlightedCount.value = i + 1;
}, props.delayPerWord * i);
timeouts.push(tid);
});
}
watch([key, () => props.delayPerWord, () => words.value.length], startAnimation, {
immediate: true,
});
function replay() {
key.value += 1;
}
onUnmounted(cleanup);
</script>
<template>
<div class="wrapper">
<div class="inner">
<p class="text" @click="replay">
<template v-for="(word, i) in words" :key="`${key}-${i}`">
<span v-if="i > 0"> </span>
<span
class="word"
:style="{ color: i < highlightedCount ? '#f1f5f9' : 'rgba(241,245,249,0.3)' }"
>
<span
class="highlight-bg"
:style="{
background: highlightColor,
transform: i < highlightedCount ? 'scaleX(1)' : 'scaleX(0)'
}"
></span>
{{ word }}
</span>
</template>
</p>
<p class="hint">Click to replay</p>
</div>
</div>
</template>
<style scoped>
.wrapper {
min-height: 100vh;
background: #0a0a0a;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.inner {
max-width: 640px;
}
.text {
font-size: clamp(1.4rem, 3vw, 2.2rem);
font-weight: 600;
line-height: 1.6;
color: rgba(241, 245, 249, 0.3);
cursor: pointer;
user-select: none;
}
.word {
position: relative;
display: inline-block;
padding: 0 0.1em;
transition: color 0.3s ease;
}
.highlight-bg {
position: absolute;
inset: 0;
border-radius: 4px;
transform-origin: left;
transition: transform 0.4s cubic-bezier(0.22, 1, 0.36, 1);
z-index: -1;
}
.hint {
text-align: center;
margin-top: 2rem;
font-size: 0.75rem;
color: #333;
letter-spacing: 0.1em;
text-transform: uppercase;
}
</style><script>
import { onMount, onDestroy } from "svelte";
export let text =
"Design is not just what it looks like and feels like. Design is how it works. Every detail matters when crafting remarkable experiences.";
export let delayPerWord = 120;
export let highlightColor = "rgba(167, 139, 250, 0.15)";
let highlightedCount = 0;
let key = 0;
let timeouts = [];
$: words = text.split(/\s+/);
$: {
// React to key or delayPerWord changes
key;
delayPerWord;
cleanup();
highlightedCount = 0;
timeouts = [];
words.forEach((_, i) => {
const tid = setTimeout(() => {
highlightedCount = i + 1;
}, delayPerWord * i);
timeouts.push(tid);
});
}
function cleanup() {
timeouts.forEach(clearTimeout);
timeouts = [];
}
function replay() {
key += 1;
}
onDestroy(() => {
cleanup();
});
</script>
<div class="wrapper">
<div class="inner">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<p class="text" on:click={replay}>
{#each words as word, i}
{#if i > 0}{' '}{/if}<span
class="word"
style="color: {i < highlightedCount ? '#f1f5f9' : 'rgba(241,245,249,0.3)'};"
>
<span
class="highlight-bg"
style="background: {highlightColor}; transform: {i < highlightedCount ? 'scaleX(1)' : 'scaleX(0)'};"
></span>
{word}
</span>
{/each}
</p>
<p class="hint">Click to replay</p>
</div>
</div>
<style>
.wrapper {
min-height: 100vh;
background: #0a0a0a;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.inner {
max-width: 640px;
}
.text {
font-size: clamp(1.4rem, 3vw, 2.2rem);
font-weight: 600;
line-height: 1.6;
color: rgba(241, 245, 249, 0.3);
cursor: pointer;
user-select: none;
}
.word {
position: relative;
display: inline-block;
padding: 0 0.1em;
transition: color 0.3s ease;
}
.highlight-bg {
position: absolute;
inset: 0;
border-radius: 4px;
transform-origin: left;
transition: transform 0.4s cubic-bezier(0.22, 1, 0.36, 1);
z-index: -1;
}
.hint {
text-align: center;
margin-top: 2rem;
font-size: 0.75rem;
color: #333;
letter-spacing: 0.1em;
text-transform: uppercase;
}
</style>Text Highlighter
Words in a paragraph are highlighted one-by-one with a colored background sweep that animates from left to right, creating a reading or karaoke-style effect.
How it works
Each word is wrapped in a <span> with a ::before pseudo-element that acts as the highlight background. JavaScript staggers the animation delay on each span so they trigger sequentially.
Features
- Sequential highlight — words light up one after another
- Customizable color — change the highlight hue via CSS variable
- Replay on click — click to restart the animation
- Smooth sweep — background animates from
scaleX(0)toscaleX(1)