UI Components Easy
Morphing Text
Text that smoothly morphs between different words using an SVG filter blur technique for seamless transitions.
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;
min-height: 100vh;
display: grid;
place-items: center;
background: #0a0a0a;
}
.container {
text-align: center;
padding: 2rem;
}
/* --- Morphing Text --- */
.morph-container {
position: relative;
}
.morph-filters {
position: absolute;
width: 0;
height: 0;
pointer-events: none;
}
.morph-wrapper {
position: relative;
display: inline-block;
filter: url(#morph-blur) contrast(30);
min-height: 1.2em;
}
.morph-text {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
white-space: nowrap;
font-size: clamp(2.5rem, 7vw, 5.5rem);
font-weight: 900;
letter-spacing: -0.03em;
line-height: 1.1;
color: #e8e8e8;
transition: opacity 0.6s ease, filter 0.6s ease;
}
.morph-text--a {
opacity: 1;
}
.morph-text--b {
opacity: 0;
}
/* Active states toggled by JS */
.morph-text--a.morph-out {
opacity: 0;
}
.morph-text--b.morph-in {
opacity: 1;
}
/* Reserve space with invisible text */
.morph-wrapper::after {
content: attr(data-current);
font-size: clamp(2.5rem, 7vw, 5.5rem);
font-weight: 900;
letter-spacing: -0.03em;
visibility: hidden;
white-space: nowrap;
display: inline-block;
}
.subtitle {
margin-top: 1.5rem;
font-size: 1rem;
color: #666;
letter-spacing: 0.01em;
position: relative;
z-index: 1;
}
/* Respect reduced motion */
@media (prefers-reduced-motion: reduce) {
.morph-wrapper {
filter: none;
}
.morph-text {
transition: none;
}
}// Morphing Text — alternates between two text elements with SVG blur morph effect
(function () {
"use strict";
const container = document.querySelector(".morph-container");
if (!container) return;
let texts;
try {
texts = JSON.parse(container.getAttribute("data-texts") || "[]");
} catch {
texts = ["Innovative", "Creative", "Powerful"];
}
if (texts.length < 2) return;
const textA = container.querySelector(".morph-text--a");
const textB = container.querySelector(".morph-text--b");
const wrapper = container.querySelector(".morph-wrapper");
if (!textA || !textB || !wrapper) return;
let index = 0;
let showingA = true;
const MORPH_DURATION = 2000; // ms between morphs
// Respect reduced motion
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (prefersReducedMotion) return;
function morph() {
const nextIndex = (index + 1) % texts.length;
if (showingA) {
// A is showing, prepare B with next text, then cross-fade
textB.textContent = texts[nextIndex];
wrapper.setAttribute("data-current", texts[nextIndex]);
textA.classList.add("morph-out");
textB.classList.add("morph-in");
setTimeout(() => {
textA.classList.remove("morph-out");
textB.classList.remove("morph-in");
// Swap: now B is showing, reset A's opacity
textA.style.opacity = "0";
textB.style.opacity = "1";
showingA = false;
}, 700);
} else {
// B is showing, prepare A with next text, then cross-fade
textA.textContent = texts[nextIndex];
wrapper.setAttribute("data-current", texts[nextIndex]);
textB.style.opacity = "0";
textA.style.opacity = "1";
showingA = true;
}
index = nextIndex;
}
// Set initial state
textA.textContent = texts[0];
wrapper.setAttribute("data-current", texts[0]);
setInterval(morph, MORPH_DURATION);
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Morphing Text</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="container">
<div
class="morph-container"
data-texts='["Innovative","Creative","Powerful","Beautiful","Seamless"]'
>
<svg class="morph-filters" aria-hidden="true">
<defs>
<filter id="morph-blur">
<feGaussianBlur in="SourceGraphic" stdDeviation="8" />
</filter>
</defs>
</svg>
<div class="morph-wrapper">
<span class="morph-text morph-text--a">Innovative</span>
<span class="morph-text morph-text--b">Creative</span>
</div>
</div>
<p class="subtitle">Text morphs smoothly between words via SVG blur</p>
</div>
<script src="script.js"></script>
</body>
</html>import { useEffect, useState, useRef, useCallback } from "react";
import type { CSSProperties } from "react";
interface MorphingTextProps {
texts: string[];
morphDuration?: number;
className?: string;
}
export function MorphingText({ texts, morphDuration = 2000, className = "" }: MorphingTextProps) {
const [index, setIndex] = useState(0);
const [showingA, setShowingA] = useState(true);
const [aText, setAText] = useState(texts[0] || "");
const [bText, setBText] = useState(texts[1] || "");
const [aOpacity, setAOpacity] = useState(1);
const [bOpacity, setBOpacity] = useState(0);
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
useEffect(() => {
if (texts.length < 2) return;
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (prefersReducedMotion) return;
const interval = setInterval(() => {
setIndex((prev) => {
const nextIndex = (prev + 1) % texts.length;
setShowingA((wasA) => {
if (wasA) {
setBText(texts[nextIndex]);
setAOpacity(0);
setBOpacity(1);
} else {
setAText(texts[nextIndex]);
setAOpacity(1);
setBOpacity(0);
}
return !wasA;
});
return nextIndex;
});
}, morphDuration);
return () => {
clearInterval(interval);
if (timeoutRef.current) clearTimeout(timeoutRef.current);
};
}, [texts, morphDuration]);
const wrapperStyle: CSSProperties = {
position: "relative",
display: "inline-block",
filter: "url(#morph-blur-react) contrast(30)",
};
const textBaseStyle: CSSProperties = {
position: "absolute",
left: "50%",
top: "50%",
transform: "translate(-50%, -50%)",
whiteSpace: "nowrap",
fontSize: "clamp(2.5rem, 7vw, 5.5rem)",
fontWeight: 900,
letterSpacing: "-0.03em",
lineHeight: 1.1,
color: "#e8e8e8",
transition: "opacity 0.6s ease",
};
const spacerStyle: CSSProperties = {
...textBaseStyle,
position: "relative",
left: "auto",
top: "auto",
transform: "none",
visibility: "hidden",
};
return (
<>
<svg style={{ position: "absolute", width: 0, height: 0 }} aria-hidden="true">
<defs>
<filter id="morph-blur-react">
<feGaussianBlur in="SourceGraphic" stdDeviation="8" />
</filter>
</defs>
</svg>
<span className={className} style={wrapperStyle}>
{/* Spacer to reserve width */}
<span style={spacerStyle}>{showingA ? aText : bText}</span>
<span style={{ ...textBaseStyle, opacity: aOpacity }}>{aText}</span>
<span style={{ ...textBaseStyle, opacity: bOpacity }}>{bText}</span>
</span>
</>
);
}
// Demo usage
export default function MorphingTextDemo() {
return (
<div
style={{
minHeight: "100vh",
display: "grid",
placeItems: "center",
background: "#0a0a0a",
fontFamily: "system-ui, -apple-system, sans-serif",
textAlign: "center",
padding: "2rem",
}}
>
<div>
<MorphingText texts={["Innovative", "Creative", "Powerful", "Beautiful", "Seamless"]} />
<p
style={{
marginTop: "1.5rem",
color: "#666",
fontSize: "1rem",
position: "relative",
zIndex: 1,
}}
>
Text morphs smoothly between words via SVG blur
</p>
</div>
</div>
);
}<script setup>
import { ref, onMounted, onUnmounted } from "vue";
const props = defineProps({
texts: {
type: Array,
default: () => ["Innovative", "Creative", "Powerful", "Beautiful", "Seamless"],
},
morphDuration: { type: Number, default: 2000 },
});
const showingA = ref(true);
const aText = ref(props.texts[0] || "");
const bText = ref(props.texts[1] || "");
const aOpacity = ref(1);
const bOpacity = ref(0);
let index = 0;
let interval = null;
onMounted(() => {
if (props.texts.length < 2) return;
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (prefersReducedMotion) return;
interval = setInterval(() => {
index = (index + 1) % props.texts.length;
if (showingA.value) {
bText.value = props.texts[index];
aOpacity.value = 0;
bOpacity.value = 1;
} else {
aText.value = props.texts[index];
aOpacity.value = 1;
bOpacity.value = 0;
}
showingA.value = !showingA.value;
}, props.morphDuration);
});
onUnmounted(() => {
if (interval) clearInterval(interval);
});
</script>
<template>
<div class="morph-demo">
<div>
<svg style="position: absolute; width: 0; height: 0;" aria-hidden="true">
<defs>
<filter id="morph-blur-vue">
<feGaussianBlur in="SourceGraphic" stdDeviation="8" />
</filter>
</defs>
</svg>
<span class="morph-wrapper">
<span class="morph-spacer">{{ showingA ? aText : bText }}</span>
<span class="morph-text" :style="{ opacity: aOpacity }">{{ aText }}</span>
<span class="morph-text" :style="{ opacity: bOpacity }">{{ bText }}</span>
</span>
<p class="morph-subtitle">
Text morphs smoothly between words via SVG blur
</p>
</div>
</div>
</template>
<style scoped>
.morph-demo {
min-height: 100vh;
display: grid;
place-items: center;
background: #0a0a0a;
font-family: system-ui, -apple-system, sans-serif;
text-align: center;
padding: 2rem;
}
.morph-wrapper {
position: relative;
display: inline-block;
filter: url(#morph-blur-vue) contrast(30);
}
.morph-spacer {
position: relative;
visibility: hidden;
white-space: nowrap;
font-size: clamp(2.5rem, 7vw, 5.5rem);
font-weight: 900;
letter-spacing: -0.03em;
line-height: 1.1;
color: #e8e8e8;
}
.morph-text {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
white-space: nowrap;
font-size: clamp(2.5rem, 7vw, 5.5rem);
font-weight: 900;
letter-spacing: -0.03em;
line-height: 1.1;
color: #e8e8e8;
transition: opacity 0.6s ease;
}
.morph-subtitle {
margin-top: 1.5rem;
color: #666;
font-size: 1rem;
position: relative;
z-index: 1;
}
</style><script>
import { onMount, onDestroy } from "svelte";
export let texts = ["Innovative", "Creative", "Powerful", "Beautiful", "Seamless"];
export let morphDuration = 2000;
let showingA = true;
let aText = texts[0] || "";
let bText = texts[1] || "";
let aOpacity = 1;
let bOpacity = 0;
let index = 0;
let interval;
onMount(() => {
if (texts.length < 2) return;
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (prefersReducedMotion) return;
interval = setInterval(() => {
index = (index + 1) % texts.length;
if (showingA) {
bText = texts[index];
aOpacity = 0;
bOpacity = 1;
} else {
aText = texts[index];
aOpacity = 1;
bOpacity = 0;
}
showingA = !showingA;
}, morphDuration);
});
onDestroy(() => {
if (interval) clearInterval(interval);
});
const wrapperStyle =
"position: relative; display: inline-block; filter: url(#morph-blur-svelte) contrast(30);";
const textBaseStyle = `
position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
white-space: nowrap; font-size: clamp(2.5rem, 7vw, 5.5rem); font-weight: 900;
letter-spacing: -0.03em; line-height: 1.1; color: #e8e8e8; transition: opacity 0.6s ease;
`;
const spacerStyle = `
position: relative; left: auto; top: auto; transform: none; visibility: hidden;
white-space: nowrap; font-size: clamp(2.5rem, 7vw, 5.5rem); font-weight: 900;
letter-spacing: -0.03em; line-height: 1.1; color: #e8e8e8;
`;
</script>
<div style="min-height: 100vh; display: grid; place-items: center; background: #0a0a0a; font-family: system-ui, -apple-system, sans-serif; text-align: center; padding: 2rem;">
<div>
<svg style="position: absolute; width: 0; height: 0;" aria-hidden="true">
<defs>
<filter id="morph-blur-svelte">
<feGaussianBlur in="SourceGraphic" stdDeviation="8" />
</filter>
</defs>
</svg>
<span style={wrapperStyle}>
<span style={spacerStyle}>{showingA ? aText : bText}</span>
<span style="{textBaseStyle} opacity: {aOpacity};">{aText}</span>
<span style="{textBaseStyle} opacity: {bOpacity};">{bText}</span>
</span>
<p style="margin-top: 1.5rem; color: #666; font-size: 1rem; position: relative; z-index: 1;">
Text morphs smoothly between words via SVG blur
</p>
</div>
</div>Morphing Text
Text that smoothly morphs between different words using a clever SVG filter blur technique. Two overlapping text elements cross-fade while a blur filter makes the transition appear organic and fluid.
How it works
- Two text elements are stacked on top of each other
- An SVG
feGaussianBlurfilter is applied to the container - As one text fades out and the other fades in, the blur makes the letter forms appear to melt and reform
- A high
contrastCSS filter on the parent sharpens the blurred edges back into crisp text - JavaScript alternates between texts on a timer
The blur-morph trick
The key insight: when you blur text that is fading out while another is fading in, the overlapping blurred shapes merge visually. Applying a high contrast filter then snaps the merged blur back into sharp shapes — creating the illusion of one word morphing into another.
When to use it
- Hero sections cycling through product features
- Creative portfolio headings
- Any text that needs premium animated transitions