9 CSS Text Shine Animation Effects — Copy-Paste
Each of these nine css text shine animation effects clips a moving gradient, a glow, or a spinning hue onto plain letters — one line, copy-paste, no image file
01 text-gradient-shine-gradient-text-flow
02 text-gradient-shine-gradient-move
03 text-gradient-shine-neon-glow-text
04 text-gradient-shine-shine-sweep-text
05 text-gradient-shine-glitch-text
06 text-gradient-shine-chrome-metal-text
07 text-gradient-shine-flicker-text
08 text-gradient-shine-outline-glow
09 text-gradient-shine-rainbow-text
Auto-plays · click a tile to jump to its section · all nine in one zip
- 01 Flowing gradient text
- 02 Gradient angle move
- 03 Neon glow text
- 04 Shine sweep text
- 05 Glitch text
- 06 Chrome metal text
- 07 Flicker text
- 08 Outline glow
- 09 Rainbow text cycle
The order below follows what happens to the color, not how flashy an item looks. 01 and 02 move a gradient through the letters — sideways, then rotating. 03 and 04 use light rather than color to make a word stand out: a glow behind it, then a css shine text effect sweeping across it. 05 and 06 treat the letters as a surface that can misbehave or reflect — a broken scanline, then a chrome sheen. 07 and 08 work letter by letter, one flickering and one emptied out to a bare outline. 09 closes the set by spinning the entire hue wheel through a fixed rainbow. Every demo above sits in a small, non-scrollable iframe and loops on its own, since there is nothing in a 480×300 frame to click or hover. The heading text in each demo below is a placeholder — swap the string inside the <h1> for a real headline and every rule keeps working exactly as written. When the surface under that headline is a tile rather than a bare stage, the CSS card hover effects set covers the container, and the same clipped gradient shrunk down to a control lives in the CSS toggle switches demos.
01Flowing gradient text
How it works
A blue-to-cream gradient is clipped to the letters with background-clip: text, then background-position slides back and forth across a background-size set to 300% of the element's own width.
Where it fits
Because the gradient is so much wider than the text, the visible slice keeps changing color as it scrolls, reading as color flowing through the letters rather than a gradient sitting still behind them.

<main class="stage">
<h1 class="flow">Summer Sale Starts</h1>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 흐름 한 바퀴가 도는 시간
$easing: linear; // 속도 변화 없이 일정하게 흘러야 끊김이 없다
$color: $subject-blue; // 그러데이션의 기준 색
@include stage($stage-ink);
.flow {
margin: 0;
font-weight: 800; font-size: clamp(28px, 8.6vw, 46px); line-height: 1.2; font-family: inherit;
text-align: center;
background: linear-gradient(90deg, $color, $subject-cream, $color, $subject-cream, $color);
background-size: 300% 100%;
-webkit-background-clip: text; // Chrome·Edge 119 이하, Safari 15.4 이하는 접두사가 있어야 한다
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
animation: flow $duration $easing infinite;
}
@keyframes flow {
0% { background-position: 0% 50%; }
100% { background-position: 100% 50%; }
}
@media (prefers-reduced-motion: reduce) {
.flow { animation: none; background-position: 40% 50%; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.flow {
margin: 0;
font-weight: 800;
font-size: clamp(28px, 8.6vw, 46px);
line-height: 1.2;
font-family: inherit;
text-align: center;
background: linear-gradient(90deg, #2f6df6, #fff7e6, #2f6df6, #fff7e6, #2f6df6);
background-size: 300% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
animation: flow 2s linear infinite;
}
@keyframes flow {
0% {
background-position: 0% 50%;
}
100% {
background-position: 100% 50%;
}
}
@media (prefers-reduced-motion: reduce) {
.flow {
animation: none;
background-position: 40% 50%;
}
}import type { CSSProperties } from "react";
import styles from "./GradientTextFlow.module.scss";
export type GradientTextFlowProps = {
text: string;
/** 흐름 한 바퀴가 도는 시간 (기본 2000ms) */
duration?: number;
easing?: string;
/** 그러데이션 기준 색 */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function GradientTextFlow({
text,
duration = 2000,
easing = "linear",
color = "#2f6df6",
}: GradientTextFlowProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return <h1 className={styles.flow} style={vars}>{text}</h1>;
}01 · Flowing gradient text — Measured area 3.461% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password
.flow {
background: linear-gradient(90deg, $color, $subject-cream, $color, $subject-cream, $color);
background-size: 300% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
animation: flow $duration $easing infinite;
}
@keyframes flow {
0% { background-position: 0% 50%; }
100% { background-position: 100% 50%; }
}
02Gradient angle move
How it works
@property registers --angle as a typed <angle> custom property, so the browser can interpolate it smoothly instead of snapping between values.
Where it fits
That typed variable sits in linear-gradient()'s own angle slot and turns from 0deg to 360deg on a loop, which spins the gradient itself inside the letters rather than moving its position.

<main class="stage">
<h1 class="spin">Premium Membership</h1>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 각도가 한 바퀴(360deg) 도는 시간
$easing: linear;
$color: $subject-ink; // 노란 무대와 대비되는 글자 기준 색
@include stage($stage-yellow);
// 각도 자체를 보간 가능한 값으로 등록해야 gradient() 각도 자리를 keyframes 로 돌릴 수 있다
@property --angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.spin {
margin: 0;
font-weight: 800; font-size: clamp(28px, 8.6vw, 46px); line-height: 1.2; font-family: inherit;
text-align: center;
--angle: 0deg;
background: linear-gradient(var(--angle), $color, $stage-orange, $color);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
animation: rotate-angle $duration $easing infinite;
}
@keyframes rotate-angle {
from { --angle: 0deg; }
to { --angle: 360deg; }
}
@media (prefers-reduced-motion: reduce) {
.spin { animation: none; --angle: 45deg; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
@property --angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.spin {
margin: 0;
font-weight: 800;
font-size: clamp(28px, 8.6vw, 46px);
line-height: 1.2;
font-family: inherit;
text-align: center;
--angle: 0deg;
background: linear-gradient(var(--angle), #17141a, #ff4d1f, #17141a);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
animation: rotate-angle 2s linear infinite;
}
@keyframes rotate-angle {
from {
--angle: 0deg;
}
to {
--angle: 360deg;
}
}
@media (prefers-reduced-motion: reduce) {
.spin {
animation: none;
--angle: 45deg;
}
}import type { CSSProperties } from "react";
import styles from "./GradientAngleMove.module.scss";
export type GradientAngleMoveProps = {
text: string;
/** 각도가 360deg 한 바퀴 도는 시간 (기본 2000ms) */
duration?: number;
easing?: string;
/** 글자 기준 색 */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function GradientAngleMove({
text,
duration = 2000,
easing = "linear",
color = "#17141a",
}: GradientAngleMoveProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return <h1 className={styles.spin} style={vars}>{text}</h1>;
}02 · Gradient angle move — Measured area 3.865% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password
@property --angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.spin {
--angle: 0deg;
background: linear-gradient(var(--angle), $color, $stage-orange, $color);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
animation: rotate-angle $duration $easing infinite;
}
@keyframes rotate-angle {
from { --angle: 0deg; }
to { --angle: 360deg; }
}
03Neon glow text
How the css text glow animation stacks
A second copy of the same letters sits behind the visible text via ::before, inherits the same gradient, and only ever gets filter: blur() applied to it.
Where it fits
The blur radius and opacity swing between a dim and a bright step several times per loop, so the glow reads as breathing rather than a neon tube switching fully off and on.

<main class="stage">
<h1 class="neon" data-text="Live Now">Live Now</h1>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 후광이 커졌다 작아지는 숨쉬기 한 주기
$easing: linear; // 일정한 속도라야 매 프레임이 이전 프레임과 확실히 달라진다
$color: $subject-blue; // 후광 색
@include stage($stage-orange);
.neon {
position: relative;
isolation: isolate; // 새 쌓임 맥락을 만들어 ::before(z-index:-1)가 무대 배경 뒤로 빠지지 않게 한다
margin: 0;
font-weight: 800; font-size: clamp(26px, 8vw, 42px); line-height: 1.2; font-family: inherit;
text-align: center;
background: linear-gradient(90deg, $color, $subject-cream);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
}
// 같은 글자를 뒤에 겹쳐 filter: blur 로만 후광을 만든다 (색은 글자와 같은 그러데이션)
.neon::before {
content: attr(data-text);
position: absolute;
inset: 0;
z-index: -1;
background: inherit;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
filter: blur(8px);
animation: breathe $duration $easing infinite; // on/off 로 꺼졌다 켜지지 않고, 흐림 반경만 숨쉬듯 오간다
}
// 부드러운 단일 곡선 대신 짧게 여러 번 오간다 — 프레임 간 변화폭을 충분히 크게 유지하기 위해서다
@keyframes breathe {
0%, 10% { filter: blur(4px); opacity: .28; }
15%, 25% { filter: blur(20px); opacity: 1; }
30%, 40% { filter: blur(4px); opacity: .28; }
45%, 55% { filter: blur(20px); opacity: 1; }
60%, 70% { filter: blur(4px); opacity: .28; }
75%, 85% { filter: blur(20px); opacity: 1; }
90%, 100% { filter: blur(4px); opacity: .28; }
}
@media (prefers-reduced-motion: reduce) {
.neon::before { animation: none; filter: blur(10px); opacity: .85; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.neon {
position: relative;
isolation: isolate;
margin: 0;
font-weight: 800;
font-size: clamp(26px, 8vw, 42px);
line-height: 1.2;
font-family: inherit;
text-align: center;
background: linear-gradient(90deg, #2f6df6, #fff7e6);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
}
.neon::before {
content: attr(data-text);
position: absolute;
inset: 0;
z-index: -1;
background: inherit;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
filter: blur(8px);
animation: breathe 2s linear infinite;
}
@keyframes breathe {
0%, 10% {
filter: blur(4px);
opacity: 0.28;
}
15%, 25% {
filter: blur(20px);
opacity: 1;
}
30%, 40% {
filter: blur(4px);
opacity: 0.28;
}
45%, 55% {
filter: blur(20px);
opacity: 1;
}
60%, 70% {
filter: blur(4px);
opacity: 0.28;
}
75%, 85% {
filter: blur(20px);
opacity: 1;
}
90%, 100% {
filter: blur(4px);
opacity: 0.28;
}
}
@media (prefers-reduced-motion: reduce) {
.neon::before {
animation: none;
filter: blur(10px);
opacity: 0.85;
}
}import type { CSSProperties } from "react";
import styles from "./NeonGlowText.module.scss";
export type NeonGlowTextProps = {
text: string;
/** 후광이 숨쉬는 한 주기 (기본 2000ms) */
duration?: number;
easing?: string;
/** 후광 색 */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function NeonGlowText({
text,
duration = 2000,
easing = "linear",
color = "#2f6df6",
}: NeonGlowTextProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<h1 className={styles.neon} style={vars} data-text={text}>
{text}
</h1>
);
}03 · Neon glow text — Measured area 10.16% · moving frames 12/23Original by GODRICH · SCSS and React ship in the zip · See the password
.neon {
position: relative;
isolation: isolate;
background: linear-gradient(90deg, $color, $subject-cream);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.neon::before {
content: attr(data-text);
position: absolute;
inset: 0;
z-index: -1;
background: inherit;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
filter: blur(8px);
animation: breathe $duration linear infinite;
}
@keyframes breathe {
0%, 10% { filter: blur(4px); opacity: .28; }
15%, 25% { filter: blur(20px); opacity: 1; }
}
04Shine sweep text
How it works
The same word is stacked twice: the bottom copy stays a flat solid color, and only the top copy is clipped to a narrow, bright diagonal band with background-clip: text.
Where it fits
background-position carries that band across the letters once per loop, so light appears to graze a metal surface rather than glow evenly.

<main class="stage">
<h1 class="sheen">
<span class="sheen__base">Gold Members Only</span>
<span class="sheen__light" aria-hidden="true">Gold Members Only</span>
</h1>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 빛 띠가 한 번 훑고 돌아오는 시간
$easing: $ease-out;
$color: $subject-ink; // 바탕 글자 색 (금속처럼 진하게)
@include stage($stage-paper);
.sheen {
position: relative;
margin: 0;
font-weight: 800; font-size: clamp(26px, 8vw, 42px); line-height: 1.2; font-family: inherit;
text-align: center;
}
.sheen__base, .sheen__light {
display: block;
grid-area: 1 / 1;
}
.sheen { display: grid; } // 두 겹을 같은 자리에 포갠다
.sheen__base { color: $color; }
.sheen__light {
background: linear-gradient(100deg, transparent 42%, rgba($stage-paper, .95) 50%, transparent 58%);
background-size: 260% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
animation: sweep $duration $easing infinite;
}
@keyframes sweep {
0% { background-position: 140% 50%; }
60% { background-position: -40% 50%; }
100% { background-position: -40% 50%; }
}
@media (prefers-reduced-motion: reduce) {
.sheen__light { animation: none; background-position: 50% 50%; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.sheen {
position: relative;
margin: 0;
font-weight: 800;
font-size: clamp(26px, 8vw, 42px);
line-height: 1.2;
font-family: inherit;
text-align: center;
}
.sheen__base, .sheen__light {
display: block;
grid-area: 1/1;
}
.sheen {
display: grid;
}
.sheen__base {
color: #17141a;
}
.sheen__light {
background: linear-gradient(100deg, transparent 42%, rgba(255, 255, 255, 0.95) 50%, transparent 58%);
background-size: 260% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
animation: sweep 2s ease-out infinite;
}
@keyframes sweep {
0% {
background-position: 140% 50%;
}
60% {
background-position: -40% 50%;
}
100% {
background-position: -40% 50%;
}
}
@media (prefers-reduced-motion: reduce) {
.sheen__light {
animation: none;
background-position: 50% 50%;
}
}import type { CSSProperties } from "react";
import styles from "./ShineSweepText.module.scss";
export type ShineSweepTextProps = {
text: string;
/** 빛 띠가 한 번 훑고 돌아오는 시간 (기본 2000ms) */
duration?: number;
easing?: string;
/** 바탕 글자 색 */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function ShineSweepText({
text,
duration = 2000,
easing = "ease-out",
color = "#17141a",
}: ShineSweepTextProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<h1 className={styles.sheen} style={vars}>
<span className={styles.base}>{text}</span>
<span className={styles.light} aria-hidden="true">{text}</span>
</h1>
);
}04 · Shine sweep text — Measured area 2.915% · moving frames 6/23Original by GODRICH · SCSS and React ship in the zip · See the password
.light {
background: linear-gradient(100deg, transparent 42%, rgba($stage-paper, .95) 50%, transparent 58%);
background-size: 260% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
animation: sweep $duration $easing infinite;
}
@keyframes sweep {
0% { background-position: 140% 50%; }
60% { background-position: -40% 50%; }
}
05Glitch text
How it works
Two color-shifted copies sit in mix-blend-mode: screen, each carrying a striped mask-image whose position jumps on steps() timing instead of easing, which breaks the letters into scanlines that snap rather than slide.
Where it fits
A short filter: hue-rotate spike fires at the same instant to shift the color, with no clip-path slicing involved anywhere.

<main class="stage">
<h1 class="glitch" data-text="System Error">
<span class="glitch__base">System Error</span>
<span class="glitch__r" aria-hidden="true">System Error</span>
<span class="glitch__b" aria-hidden="true">System Error</span>
</h1>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 스캔라인이 어긋났다 돌아오는 한 주기
$easing: steps(2, jump-none); // 부드럽게 잇지 않고 뚝뚝 끊겨야 글리치답다
$color: $error; // 어긋나는 레이어 한쪽 색
@include stage($stage-ink);
.glitch {
position: relative;
margin: 0;
font-weight: 800; font-size: clamp(26px, 8.2vw, 42px); line-height: 1.2; font-family: inherit;
text-align: center;
}
.glitch__base, .glitch__r, .glitch__b {
display: block;
grid-area: 1 / 1;
}
.glitch { display: grid; }
.glitch__base {
background: linear-gradient(90deg, $color, $subject-blue);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
animation: hue-jump $duration $easing infinite;
}
// clip-path 로 조각내 밀어내는 대신, 가로줄 mask 를 뚝뚝 끊어 옮겨 스캔라인이 깨지는 것처럼 보이게 한다
.glitch__r, .glitch__b {
position: absolute; inset: 0;
mix-blend-mode: screen;
-webkit-mask-image: repeating-linear-gradient(to bottom, #000 0 3px, transparent 3px 7px);
mask-image: repeating-linear-gradient(to bottom, #000 0 3px, transparent 3px 7px);
}
.glitch__r { color: $color; animation: mask-jump-r $duration $easing infinite; }
.glitch__b { color: $subject-blue; animation: mask-jump-b $duration $easing infinite; }
@keyframes mask-jump-r {
0%, 40%, 100% { -webkit-mask-position: 0 0; mask-position: 0 0; opacity: 0; }
45%, 55% { -webkit-mask-position: 0 3px; mask-position: 0 3px; opacity: .9; }
}
@keyframes mask-jump-b {
0%, 60%, 100% { -webkit-mask-position: 0 0; mask-position: 0 0; opacity: 0; }
65%, 75% { -webkit-mask-position: 0 -3px; mask-position: 0 -3px; opacity: .9; }
}
@keyframes hue-jump {
0%, 48%, 52%, 100% { filter: hue-rotate(0deg); }
50% { filter: hue-rotate(35deg); }
}
@media (prefers-reduced-motion: reduce) {
.glitch__base { animation: none; }
.glitch__r, .glitch__b { animation: none; opacity: 0; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.glitch {
position: relative;
margin: 0;
font-weight: 800;
font-size: clamp(26px, 8.2vw, 42px);
line-height: 1.2;
font-family: inherit;
text-align: center;
}
.glitch__base, .glitch__r, .glitch__b {
display: block;
grid-area: 1/1;
}
.glitch {
display: grid;
}
.glitch__base {
background: linear-gradient(90deg, #c2361a, #2f6df6);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
animation: hue-jump 2s steps(2, jump-none) infinite;
}
.glitch__r, .glitch__b {
position: absolute;
inset: 0;
mix-blend-mode: screen;
-webkit-mask-image: repeating-linear-gradient(to bottom, #000 0 3px, transparent 3px 7px);
mask-image: repeating-linear-gradient(to bottom, #000 0 3px, transparent 3px 7px);
}
.glitch__r {
color: #c2361a;
animation: mask-jump-r 2s steps(2, jump-none) infinite;
}
.glitch__b {
color: #2f6df6;
animation: mask-jump-b 2s steps(2, jump-none) infinite;
}
@keyframes mask-jump-r {
0%, 40%, 100% {
-webkit-mask-position: 0 0;
mask-position: 0 0;
opacity: 0;
}
45%, 55% {
-webkit-mask-position: 0 3px;
mask-position: 0 3px;
opacity: 0.9;
}
}
@keyframes mask-jump-b {
0%, 60%, 100% {
-webkit-mask-position: 0 0;
mask-position: 0 0;
opacity: 0;
}
65%, 75% {
-webkit-mask-position: 0 -3px;
mask-position: 0 -3px;
opacity: 0.9;
}
}
@keyframes hue-jump {
0%, 48%, 52%, 100% {
filter: hue-rotate(0deg);
}
50% {
filter: hue-rotate(35deg);
}
}
@media (prefers-reduced-motion: reduce) {
.glitch__base {
animation: none;
}
.glitch__r, .glitch__b {
animation: none;
opacity: 0;
}
}import type { CSSProperties } from "react";
import styles from "./GlitchText.module.scss";
export type GlitchTextProps = {
text: string;
/** 스캔라인이 어긋났다 돌아오는 한 주기 (기본 2000ms) */
duration?: number;
easing?: string;
/** 어긋나는 레이어 한쪽 색 */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function GlitchText({
text,
duration = 2000,
easing = "steps(2, jump-none)",
color = "#c2361a",
}: GlitchTextProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<h1 className={styles.glitch} style={vars}>
<span className={styles.base}>{text}</span>
<span className={styles.r} aria-hidden="true">{text}</span>
<span className={styles.b} aria-hidden="true">{text}</span>
</h1>
);
}05 · Glitch text — Measured area 2.345% · moving frames 6/23Original by GODRICH · SCSS and React ship in the zip · See the password
.r, .b {
position: absolute;
inset: 0;
mix-blend-mode: screen;
-webkit-mask-image: repeating-linear-gradient(to bottom, #000 0 3px, transparent 3px 7px);
mask-image: repeating-linear-gradient(to bottom, #000 0 3px, transparent 3px 7px);
animation: mask-jump-r $duration steps(1) infinite;
}
@keyframes mask-jump-r {
0%, 40%, 100% { -webkit-mask-position: 0 0; mask-position: 0 0; opacity: 0; }
45%, 55% { -webkit-mask-position: 0 3px; mask-position: 0 3px; opacity: .9; }
}
.tint {
animation: hue-jab $duration steps(1) infinite;
}
@keyframes hue-jab {
0%, 44%, 100% { filter: hue-rotate(0deg); }
47% { filter: hue-rotate(35deg); }
}
06Chrome metal text
How it works
Dark, white, and cream stops alternate at tight intervals inside one gradient, clipped to the letters the same way as item 01, but background-position here eases to a stop at the end of each loop instead of sliding at item 01's constant linear speed.
Where it fits
The tight alternation is what sells the metal — a wide, soft gradient would just look like a colored glow instead of a reflective surface.

<main class="stage">
<h1 class="chrome">Racing Champion</h1>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 반사광이 표면을 한 번 훑는 시간
$easing: $ease-out;
$color: $subject-ink; // 가장 어두운 밴드 색
@include stage($stage-yellow);
.chrome {
margin: 0;
font-weight: 900; font-size: clamp(28px, 8.8vw, 46px); line-height: 1.2; font-family: inherit;
text-align: center;
background: linear-gradient(
100deg,
$color 0%, $stage-paper 12%, $color 24%,
$subject-cream 40%, $stage-paper 52%, $color 64%,
$stage-paper 80%, $color 100%
);
background-size: 220% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
animation: reflect $duration $easing infinite;
}
@keyframes reflect {
0% { background-position: 100% 50%; }
100% { background-position: -20% 50%; }
}
@media (prefers-reduced-motion: reduce) {
.chrome { animation: none; background-position: 40% 50%; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.chrome {
margin: 0;
font-weight: 900;
font-size: clamp(28px, 8.8vw, 46px);
line-height: 1.2;
font-family: inherit;
text-align: center;
background: linear-gradient(100deg, #17141a 0%, #ffffff 12%, #17141a 24%, #fff7e6 40%, #ffffff 52%, #17141a 64%, #ffffff 80%, #17141a 100%);
background-size: 220% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
animation: reflect 2s ease-out infinite;
}
@keyframes reflect {
0% {
background-position: 100% 50%;
}
100% {
background-position: -20% 50%;
}
}
@media (prefers-reduced-motion: reduce) {
.chrome {
animation: none;
background-position: 40% 50%;
}
}import type { CSSProperties } from "react";
import styles from "./ChromeMetalText.module.scss";
export type ChromeMetalTextProps = {
text: string;
/** 반사광이 표면을 한 번 훑는 시간 (기본 2000ms) */
duration?: number;
easing?: string;
/** 가장 어두운 밴드 색 */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function ChromeMetalText({
text,
duration = 2000,
easing = "ease-out",
color = "#17141a",
}: ChromeMetalTextProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return <h1 className={styles.chrome} style={vars}>{text}</h1>;
}06 · Chrome metal text — Measured area 3.683% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password
.chrome {
background: linear-gradient(
100deg,
$color 0%, $stage-paper 12%, $color 24%,
$subject-cream 40%, $stage-paper 52%, $color 64%,
$stage-paper 80%, $color 100%
);
background-size: 220% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
animation: reflect $duration $easing infinite;
}
@keyframes reflect {
0% { background-position: 0% 50%; }
100% { background-position: 100% 50%; }
}
07Flicker text
How it works
Each letter is wrapped in its own span, and a --i custom property multiplied inside animation-delay gives every letter a different starting point on the same short opacity keyframes.
Where it fits
Because the delay lives on the letter, not a shared parent, the whole word never dims at once — it flickers letter by letter, closer to an old marquee sign than a single neon tube.

<main class="stage">
<h1 class="marquee">
<span style="--i:0">Li</span><span style="--i:1">v</span><span style="--i:2">e</span><span class="marquee__gap" aria-hidden="true"></span><span style="--i:3">Sta</span><span style="--i:4">rt</span>
</h1>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 전구 하나가 어두워졌다 돌아오는 한 주기
$easing: linear;
$color: $subject-cream; // 전구 색
@include stage($stage-orange);
.marquee {
margin: 0;
font-weight: 800; font-size: clamp(28px, 9vw, 46px); line-height: 1.2; font-family: inherit;
text-align: center;
}
.marquee span {
display: inline-block;
color: $color;
animation: bulb $duration $easing infinite;
animation-delay: calc(var(--i, 0) * 120ms); // 글자마다 지연이 달라 하나씩 따로 깜빡인다
}
.marquee__gap { width: .3em; }
@keyframes bulb {
0%, 40%, 100% { opacity: 1; }
47% { opacity: .22; } // 통째로 꺼지는 네온과 달리 글자 하나만 잠깐 어두워진다
54% { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
.marquee span { animation: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.marquee {
margin: 0;
font-weight: 800;
font-size: clamp(28px, 9vw, 46px);
line-height: 1.2;
font-family: inherit;
text-align: center;
}
.marquee span {
display: inline-block;
color: #fff7e6;
animation: bulb 2s linear infinite;
animation-delay: calc(var(--i, 0) * 120ms);
}
.marquee__gap {
width: 0.3em;
}
@keyframes bulb {
0%, 40%, 100% {
opacity: 1;
}
47% {
opacity: 0.22;
}
54% {
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.marquee span {
animation: none;
}
}import type { CSSProperties } from "react";
import styles from "./FlickerText.module.scss";
export type FlickerTextProps = {
/** 글자를 하나씩 나눠 넣는다 (공백은 별도 span 으로) */
letters: string[];
/** 전구 하나가 어두워졌다 돌아오는 한 주기 (기본 2000ms) */
duration?: number;
easing?: string;
/** 전구 색 */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
type LetterVars = CSSProperties & { "--i"?: number };
export default function FlickerText({
letters,
duration = 2000,
easing = "linear",
color = "#fff7e6",
}: FlickerTextProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<h1 className={styles.marquee} style={vars}>
{letters.map((ch, i) =>
ch === " " ? (
<span key={i} className={styles.gap} aria-hidden="true" />
) : (
<span key={i} style={{ "--i": i } as LetterVars}>{ch}</span>
)
)}
</h1>
);
}07 · Flicker text — Measured area 2.881% · moving frames 10/23Original by GODRICH · SCSS and React ship in the zip · See the password
.marquee span {
display: inline-block;
color: $color;
animation: bulb $duration $easing infinite;
animation-delay: calc(var(--i, 0) * 120ms);
}
@keyframes bulb {
0%, 40%, 100% { opacity: 1; }
47% { opacity: .22; }
54% { opacity: 1; }
}
08Outline glow
How it works
color: transparent empties the letter fill entirely, -webkit-text-stroke draws a thin outline in its place, and filter: drop-shadow() is the only thing left animating — its radius widens and narrows so light spreads outward from the stroke and pulls back in.
Where it fits
Nothing about the fill or the stroke width itself ever changes.

<main class="stage">
<h1 class="outline">Try It Free</h1>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 빛이 커졌다 작아지는 숨쉬기 한 주기
$easing: $ease-spring;
$color: $subject-blue; // 윤곽선·빛 색
@include stage($stage-paper);
.outline {
margin: 0;
font-weight: 900; font-size: clamp(28px, 8.8vw, 46px); line-height: 1.2; font-family: inherit;
text-align: center;
color: transparent; // 속을 비워 윤곽선만 남긴다
-webkit-text-stroke: 2px $color;
text-stroke: 2px $color;
animation: glow $duration $easing infinite;
}
@keyframes glow {
0%, 100% { filter: drop-shadow(0 0 3px rgba($color, .5)); }
50% { filter: drop-shadow(0 0 14px rgba($color, .9)) drop-shadow(0 0 26px rgba($color, .5)); }
}
@media (prefers-reduced-motion: reduce) {
.outline { animation: none; filter: drop-shadow(0 0 10px rgba($color, .7)); }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.outline {
margin: 0;
font-weight: 900;
font-size: clamp(28px, 8.8vw, 46px);
line-height: 1.2;
font-family: inherit;
text-align: center;
color: transparent;
-webkit-text-stroke: 2px #2f6df6;
text-stroke: 2px #2f6df6;
animation: glow 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes glow {
0%, 100% {
filter: drop-shadow(0 0 3px rgba(47, 109, 246, 0.5));
}
50% {
filter: drop-shadow(0 0 14px rgba(47, 109, 246, 0.9)) drop-shadow(0 0 26px rgba(47, 109, 246, 0.5));
}
}
@media (prefers-reduced-motion: reduce) {
.outline {
animation: none;
filter: drop-shadow(0 0 10px rgba(47, 109, 246, 0.7));
}
}import type { CSSProperties } from "react";
import styles from "./OutlineGlowText.module.scss";
export type OutlineGlowTextProps = {
text: string;
/** 빛이 커졌다 작아지는 숨쉬기 한 주기 (기본 2000ms) */
duration?: number;
easing?: string;
/** 윤곽선·빛 색 */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function OutlineGlowText({
text,
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#2f6df6",
}: OutlineGlowTextProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return <h1 className={styles.outline} style={vars}>{text}</h1>;
}08 · Outline glow — Measured area 15.976% · moving frames 7/23Original by GODRICH · SCSS and React ship in the zip · See the password
.outline {
color: transparent;
-webkit-text-stroke: 2px $color;
animation: glow $duration $easing infinite;
}
@keyframes glow {
0%, 100% { filter: drop-shadow(0 0 3px rgba($color, .5)); }
50% {
filter: drop-shadow(0 0 14px rgba($color, .9))
drop-shadow(0 0 26px rgba($color, .5));
}
}
09Rainbow text cycle
How it works
A fixed multicolor gradient is clipped to the letters and never moves — no background-position, no angle change.
Where it fits
Instead filter: hue-rotate turns continuously from 0deg to 360deg, so the whole color wheel cycles through the same static band of letters rather than the gradient itself sliding anywhere.

<main class="stage">
<h1 class="rainbow">The Festival Begins</h1>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 색상환이 360도 한 바퀴 도는 시간
$easing: linear;
$color: $subject-blue; // 그러데이션의 시작 색
@include stage($stage-ink);
.rainbow {
margin: 0;
font-weight: 800; font-size: clamp(28px, 8.6vw, 46px); line-height: 1.2; font-family: inherit;
text-align: center;
background: linear-gradient(90deg, $color, $stage-yellow, $stage-orange, $error, $subject-cream, $color);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
animation: hue-cycle $duration $easing infinite; // 위치가 아니라 색상환 자체를 돌린다
}
@keyframes hue-cycle {
from { filter: hue-rotate(0deg); }
to { filter: hue-rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
.rainbow { animation: none; filter: hue-rotate(0deg); }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.rainbow {
margin: 0;
font-weight: 800;
font-size: clamp(28px, 8.6vw, 46px);
line-height: 1.2;
font-family: inherit;
text-align: center;
background: linear-gradient(90deg, #2f6df6, #ffd23f, #ff4d1f, #c2361a, #fff7e6, #2f6df6);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
animation: hue-cycle 2s linear infinite;
}
@keyframes hue-cycle {
from {
filter: hue-rotate(0deg);
}
to {
filter: hue-rotate(360deg);
}
}
@media (prefers-reduced-motion: reduce) {
.rainbow {
animation: none;
filter: hue-rotate(0deg);
}
}import type { CSSProperties } from "react";
import styles from "./RainbowText.module.scss";
export type RainbowTextProps = {
text: string;
/** 색상환이 360도 한 바퀴 도는 시간 (기본 2000ms) */
duration?: number;
easing?: string;
/** 그러데이션 시작 색 */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function RainbowText({
text,
duration = 2000,
easing = "linear",
color = "#2f6df6",
}: RainbowTextProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return <h1 className={styles.rainbow} style={vars}>{text}</h1>;
}09 · Rainbow text cycle — Measured area 4.147% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password
.rainbow {
background: linear-gradient(90deg, $color, $stage-yellow, $stage-orange, $error, $subject-cream, $color);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
animation: hue-cycle $duration linear infinite;
}
@keyframes hue-cycle {
from { filter: hue-rotate(0deg); }
to { filter: hue-rotate(360deg); }
}
Where it breaks — prefixes, @property, and a glow that vanishes
The glow that vanishes
Item 03's glow layer is the trap this set actually shipped with once: drop the isolation: isolate line from .neon and the ::before copy doesn't just look dimmer, it disappears completely. That ::before carries z-index: -1 so it paints behind the visible letters, but a negative z-index only stays inside its own parent's stacking context when that parent actually opens one. Without position: relative plus isolation: isolate on .neon, the pseudo-element's stacking context folds into whatever ancestor owns one first — here, the .stage backdrop — and the glow ends up painted below the entire demo background instead of just behind its own letters. MDN's isolation reference covers how a new stacking context keeps a layer's z-order scoped to its own parent.
Why the -webkit- lines stay
The seven items built on background-clip: text (01, 02, 03, 04, 05, 06, and 09) still carry the -webkit- line for a real reason, not habit. Checking MDN's compatibility data for background-clip shows Chrome and Edge only accepted the unprefixed text value starting at version 120, while Safari picked it up earlier, at version 14 — before those versions, -webkit-background-clip: text was the only spelling either engine understood. Firefox has supported it unprefixed since version 49, with MDN's own compatibility notes flagging some edge cases around background-attachment: fixed that these nine demos never touch. Item 05's mask-image carries the same doubling for the same reason: MDN's compatibility data for mask-image puts unprefixed support at Chrome and Edge 120, Safari 15.4, and Firefox 53, so -webkit-mask-image and -webkit-mask-position stay next to the standard lines.
What @property needs
Item 02's @property block asks for more: caniuse's @property table lists support from Chrome and Edge 85, Safari 16.4, and Firefox only as recently as version 128. On anything older, the <angle> custom property just fails to register, --angle stops interpolating between keyframes, and the gradient holds still at whatever angle it started instead of rotating — the text still reads fine, it just never spins. Item 06's chrome sheen scaled up to full sections behaves the same way in the Y2K chrome holographic kit, where the still metal gradient is the baseline and the rotation is the bonus. Every file in this set, unpacked and already measured after render, sits behind 5hzhmkcb typed exactly as it appears right here, nothing added and nothing trimmed off either end.
Accessibility
Where each one parks
All nine turn off their looping animation under prefers-reduced-motion: reduce, and each settles on its own fixed value rather than one shared default. 01, 02, and 06 park their moving gradient or angle at a mid-cycle position — background-position: 40% 50% for 01 and 06, --angle: 45deg for 02 — chosen so the letters stay fully colored instead of sitting at either extreme. 04's shine band settles at background-position: 50% 50%, off to the side of its sweep, so no stray highlight freezes across the letters. 09 works differently: it resets hue-rotate all the way back to 0deg, the exact start of its loop, rather than stopping partway around the color wheel. 03 and 08 both settle on a fixed, clearly visible glow instead of their dimmest step — blur(10px) at .85 opacity for 03, a single 14px drop-shadow for 08 — so neither reads as broken or half-rendered. 05 takes the opposite approach: its two color-shifted mask layers drop straight to opacity: 0, so reduced motion doesn't freeze the glitch mid-jump, it removes the distortion entirely and leaves the plain base gradient underneath. 07's flicker stops outright and every letter locks to full opacity together, closing the one gap where reduced motion could otherwise leave part of a word invisible.
Contrast for low vision
Because 03 and 08 lean on filter: blur() or stacked drop-shadow() for contrast rather than a flat text color, pairing either with a plain, solid-color fallback matters more than usual for low-vision readers on a washed-out monitor. MDN's prefers-reduced-motion page documents the operating-system toggle behind that media query.
More CSS-only effects like these live on the CSS category hub, and what this site does and doesn't sell is on the about page.
FAQ
Do I still need the -webkit- prefix for a css text shine animation built on background-clip?
Yes, for now. Chrome and Edge only dropped the requirement at version 120 and Firefox has a handful of documented edge cases even where it works unprefixed, so leaving -webkit-background-clip: text next to the standard line costs nothing and keeps older installs from showing solid black text instead of a gradient.
Why did my copy of item 03's glow disappear instead of just looking dimmer?
isolation: isolate on .neon is doing real work there, not decoration — without it, the z-index: -1 glow layer stacks behind the demo's entire background rather than just behind its own letters. Add position: relative; isolation: isolate; to whatever element wraps the glowing text and the layer comes back.
Can several css gradient text animation layers run on the same headline at once?
Technically, but it costs more than it's worth. Stacking background-clip: text with filter: blur() or a mask-image piles up extra layers the browser has to repaint every frame, and that shows up first as stutter on lower-end phones. One effect per headline holds up better than layering two or three for a busier look. To keep the extra color without the extra text layer, move it off the letters and onto the frame with the CSS border animations set instead.









