UI Components Easy
Flip Text
Words that flip and rotate vertically to cycle through different words with smooth CSS perspective 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;
}
/* --- Flip Text --- */
.flip-heading {
font-size: clamp(2rem, 5vw, 4rem);
font-weight: 800;
letter-spacing: -0.02em;
line-height: 1.2;
color: #e0e0e0;
}
.flip-text {
--flip-duration: 2.5s;
display: inline-block;
position: relative;
height: 1.2em;
overflow: hidden;
vertical-align: bottom;
perspective: 600px;
}
.flip-word {
display: inline-block;
color: transparent;
background: linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6);
-webkit-background-clip: text;
background-clip: text;
animation: flip-in var(--flip-duration) ease forwards;
transform-origin: center bottom;
backface-visibility: hidden;
}
@keyframes flip-in {
0% {
transform: rotateX(90deg);
opacity: 0;
}
15% {
transform: rotateX(0deg);
opacity: 1;
}
80% {
transform: rotateX(0deg);
opacity: 1;
}
100% {
transform: rotateX(-90deg);
opacity: 0;
}
}
.subtitle {
margin-top: 1.5rem;
font-size: 1rem;
color: #666;
letter-spacing: 0.01em;
}
/* Respect reduced motion */
@media (prefers-reduced-motion: reduce) {
.flip-word {
animation: none;
transform: rotateX(0deg);
opacity: 1;
}
}// Flip Text — cycles through words with a vertical flip animation
(function () {
"use strict";
const el = document.querySelector(".flip-text");
if (!el) return;
let words;
try {
words = JSON.parse(el.getAttribute("data-words") || "[]");
} catch {
words = ["amazing", "beautiful", "fast"];
}
if (words.length === 0) return;
let index = 0;
const wordEl = el.querySelector(".flip-word");
if (!wordEl) return;
// Read duration from CSS custom property
const style = getComputedStyle(el);
const durationStr = style.getPropertyValue("--flip-duration").trim() || "2.5s";
const duration = parseFloat(durationStr) * 1000;
// Respect reduced motion
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (prefersReducedMotion) return;
function nextWord() {
index = (index + 1) % words.length;
wordEl.textContent = words[index];
// Restart animation
wordEl.style.animation = "none";
wordEl.offsetHeight; // trigger reflow
wordEl.style.animation = "";
}
setInterval(nextWord, duration);
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Flip Text</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="container">
<h1 class="flip-heading">
We build
<span
class="flip-text"
data-words='["amazing","beautiful","fast","modern","stunning"]'
>
<span class="flip-word">amazing</span>
</span>
products
</h1>
<p class="subtitle">Words flip vertically to cycle through a list</p>
</div>
<script src="script.js"></script>
</body>
</html>import { useEffect, useState, useCallback } from "react";
import type { CSSProperties } from "react";
interface FlipTextProps {
words: string[];
duration?: number;
className?: string;
}
export function FlipText({ words, duration = 2500, className = "" }: FlipTextProps) {
const [index, setIndex] = useState(0);
const [animKey, setAnimKey] = useState(0);
useEffect(() => {
if (words.length < 2) return;
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (prefersReducedMotion) return;
const interval = setInterval(() => {
setIndex((prev) => (prev + 1) % words.length);
setAnimKey((prev) => prev + 1);
}, duration);
return () => clearInterval(interval);
}, [words, duration]);
const containerStyle: CSSProperties = {
display: "inline-block",
position: "relative",
height: "1.2em",
overflow: "hidden",
verticalAlign: "bottom",
perspective: "600px",
};
const wordStyle: CSSProperties = {
display: "inline-block",
backgroundImage: "linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)",
WebkitBackgroundClip: "text",
backgroundClip: "text",
color: "transparent",
animation: `flip-in ${duration}ms ease forwards`,
transformOrigin: "center bottom",
backfaceVisibility: "hidden",
};
return (
<>
<style>{`
@keyframes flip-in {
0% { transform: rotateX(90deg); opacity: 0; }
15% { transform: rotateX(0deg); opacity: 1; }
80% { transform: rotateX(0deg); opacity: 1; }
100% { transform: rotateX(-90deg); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.flip-word-react { animation: none !important; transform: rotateX(0deg) !important; opacity: 1 !important; }
}
`}</style>
<span className={className} style={containerStyle}>
<span key={animKey} className="flip-word-react" style={wordStyle}>
{words[index]}
</span>
</span>
</>
);
}
// Demo usage
export default function FlipTextDemo() {
return (
<div
style={{
minHeight: "100vh",
display: "grid",
placeItems: "center",
background: "#0a0a0a",
fontFamily: "system-ui, -apple-system, sans-serif",
textAlign: "center",
padding: "2rem",
}}
>
<div>
<h1
style={{
fontSize: "clamp(2rem, 5vw, 4rem)",
fontWeight: 800,
letterSpacing: "-0.02em",
lineHeight: 1.2,
color: "#e0e0e0",
}}
>
We build <FlipText words={["amazing", "beautiful", "fast", "modern", "stunning"]} />{" "}
products
</h1>
<p style={{ marginTop: "1.5rem", color: "#666", fontSize: "1rem" }}>
Words flip vertically to cycle through a list
</p>
</div>
</div>
);
}<script setup>
import { ref, onMounted, onUnmounted } from "vue";
const props = defineProps({
words: { type: Array, default: () => ["amazing", "beautiful", "fast", "modern", "stunning"] },
duration: { type: Number, default: 2500 },
});
const index = ref(0);
const animKey = ref(0);
let interval;
onMounted(() => {
if (props.words.length < 2) return;
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (prefersReducedMotion) return;
interval = setInterval(() => {
index.value = (index.value + 1) % props.words.length;
animKey.value += 1;
}, props.duration);
});
onUnmounted(() => {
if (interval) clearInterval(interval);
});
</script>
<template>
<div class="flip-demo">
<div>
<h1 class="flip-heading">
We build
<span class="flip-container">
<span
:key="animKey"
class="flip-word"
:style="{ animationDuration: duration + 'ms' }"
>
{{ words[index] }}
</span>
</span>
products
</h1>
<p class="flip-subtitle">Words flip vertically to cycle through a list</p>
</div>
</div>
</template>
<style scoped>
@keyframes flip-in {
0% { transform: rotateX(90deg); opacity: 0; }
15% { transform: rotateX(0deg); opacity: 1; }
80% { transform: rotateX(0deg); opacity: 1; }
100% { transform: rotateX(-90deg); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.flip-word { animation: none !important; transform: rotateX(0deg) !important; opacity: 1 !important; }
}
.flip-demo {
min-height: 100vh;
display: grid;
place-items: center;
background: #0a0a0a;
font-family: system-ui, -apple-system, sans-serif;
text-align: center;
padding: 2rem;
}
.flip-heading {
font-size: clamp(2rem, 5vw, 4rem);
font-weight: 800;
letter-spacing: -0.02em;
line-height: 1.2;
color: #e0e0e0;
}
.flip-container {
display: inline-block;
position: relative;
height: 1.2em;
overflow: hidden;
vertical-align: bottom;
perspective: 600px;
}
.flip-word {
display: inline-block;
background-image: linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
animation: flip-in ease forwards;
transform-origin: center bottom;
backface-visibility: hidden;
}
.flip-subtitle {
margin-top: 1.5rem;
color: #666;
font-size: 1rem;
}
</style><script>
import { onMount, onDestroy } from "svelte";
export let words = ["amazing", "beautiful", "fast", "modern", "stunning"];
export let duration = 2500;
let index = 0;
let animKey = 0;
let interval;
onMount(() => {
if (words.length < 2) return;
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (prefersReducedMotion) return;
interval = setInterval(() => {
index = (index + 1) % words.length;
animKey += 1;
}, duration);
});
onDestroy(() => {
if (interval) clearInterval(interval);
});
</script>
<svelte:head>
<style>
@keyframes flip-in {
0% { transform: rotateX(90deg); opacity: 0; }
15% { transform: rotateX(0deg); opacity: 1; }
80% { transform: rotateX(0deg); opacity: 1; }
100% { transform: rotateX(-90deg); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.flip-word-svelte { animation: none !important; transform: rotateX(0deg) !important; opacity: 1 !important; }
}
</style>
</svelte:head>
<div class="flip-demo">
<div>
<h1 class="flip-heading">
We build
<span class="flip-container">
{#key animKey}
<span class="flip-word flip-word-svelte" style="animation-duration: {duration}ms;">
{words[index]}
</span>
{/key}
</span>
products
</h1>
<p class="flip-subtitle">Words flip vertically to cycle through a list</p>
</div>
</div>
<style>
.flip-demo {
min-height: 100vh;
display: grid;
place-items: center;
background: #0a0a0a;
font-family: system-ui, -apple-system, sans-serif;
text-align: center;
padding: 2rem;
}
.flip-heading {
font-size: clamp(2rem, 5vw, 4rem);
font-weight: 800;
letter-spacing: -0.02em;
line-height: 1.2;
color: #e0e0e0;
}
.flip-container {
display: inline-block;
position: relative;
height: 1.2em;
overflow: hidden;
vertical-align: bottom;
perspective: 600px;
}
.flip-word {
display: inline-block;
background-image: linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
animation: flip-in ease forwards;
transform-origin: center bottom;
backface-visibility: hidden;
}
.flip-subtitle {
margin-top: 1.5rem;
color: #666;
font-size: 1rem;
}
</style>Flip Text
Cycles through an array of words with a vertical flip animation. Each word rotates in from below, holds, then flips out upward to reveal the next word.
How it works
- A container with
overflow: hiddenandperspectiveclips the text during rotation @keyframes fliprotates each word along the X-axis from90deg(hidden) through0deg(visible) and back to-90deg(exit)- JavaScript cycles through the word list, updating the displayed word on each animation iteration
transform-origin: center bottomensures the flip feels natural
Customization
- Pass any array of words via the
data-wordsattribute (HTML) orwordsprop (React) --flip-durationcontrols total time per word (default:2.5s)- Works inline with static text: “We build
[amazing|beautiful|fast]products”
When to use it
- Hero taglines with rotating keywords
- Feature descriptions cycling through benefits
- Any text that needs to communicate multiple concepts in one space