Shiny Text Effect
Original · freeshiny sweep text
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/txfx-shiny.json"use client";
import { useRef, useState, type CSSProperties, type KeyboardEvent } from "react";
import { motion, useReducedMotion } from "motion/react";
type MaterialId = "gold" | "chrome" | "aurora" | "ember";
interface Material {
id: MaterialId;
label: string;
swatch: string;
hint: string;
}
const MATERIALS: readonly Material[] = [
{
id: "gold",
label: "Gold",
swatch: "linear-gradient(135deg,#b45309 0%,#fde68a 100%)",
hint: "Warm amber leaf",
},
{
id: "chrome",
label: "Chrome",
swatch: "linear-gradient(135deg,#334155 0%,#cbd5e1 100%)",
hint: "Brushed metal",
},
{
id: "aurora",
label: "Aurora",
swatch: "linear-gradient(135deg,#4338ca 0%,#22d3ee 100%)",
hint: "Indigo to ice",
},
{
id: "ember",
label: "Ember",
swatch: "linear-gradient(135deg,#be123c 0%,#f59e0b 100%)",
hint: "Rose to flame",
},
] as const;
const STYLES = `
.txfx-shiny-text {
background-image: linear-gradient(
100deg,
var(--txfx-a) 0%,
var(--txfx-a) 36%,
var(--txfx-b) 50%,
var(--txfx-a) 64%,
var(--txfx-a) 100%
);
background-size: 220% 100%;
background-position: 130% center;
background-repeat: no-repeat;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
color: transparent;
}
.txfx-shiny-static { background-position: 50% center; }
.txfx-shiny-text[data-material="gold"] { --txfx-a: #b45309; --txfx-b: #fef3c7; }
.txfx-shiny-text[data-material="chrome"] { --txfx-a: #334155; --txfx-b: #94a3b8; }
.txfx-shiny-text[data-material="aurora"] { --txfx-a: #4338ca; --txfx-b: #22d3ee; }
.txfx-shiny-text[data-material="ember"] { --txfx-a: #be123c; --txfx-b: #f59e0b; }
@media (prefers-color-scheme: dark) {
.txfx-shiny-text[data-material="gold"] { --txfx-a: #f59e0b; --txfx-b: #fffbeb; }
.txfx-shiny-text[data-material="chrome"] { --txfx-a: #cbd5e1; --txfx-b: #ffffff; }
.txfx-shiny-text[data-material="aurora"] { --txfx-a: #818cf8; --txfx-b: #67e8f9; }
.txfx-shiny-text[data-material="ember"] { --txfx-a: #fb7185; --txfx-b: #fde68a; }
}
.dark .txfx-shiny-text[data-material="gold"] { --txfx-a: #f59e0b; --txfx-b: #fffbeb; }
.dark .txfx-shiny-text[data-material="chrome"] { --txfx-a: #cbd5e1; --txfx-b: #ffffff; }
.dark .txfx-shiny-text[data-material="aurora"] { --txfx-a: #818cf8; --txfx-b: #67e8f9; }
.dark .txfx-shiny-text[data-material="ember"] { --txfx-a: #fb7185; --txfx-b: #fde68a; }
@keyframes txfxShinySweep {
from { background-position: 220% center; }
to { background-position: -120% center; }
}
@media (prefers-reduced-motion: reduce) {
.txfx-shiny-text {
animation: none !important;
background-position: 50% center !important;
}
}
`;
function SparkIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" className="h-3.5 w-3.5">
<path
d="M12 2.5l1.7 5.1a3 3 0 0 0 1.9 1.9l5.1 1.7-5.1 1.7a3 3 0 0 0-1.9 1.9L12 20l-1.7-5.2a3 3 0 0 0-1.9-1.9L3.3 11.2l5.1-1.7a3 3 0 0 0 1.9-1.9L12 2.5z"
fill="currentColor"
/>
</svg>
);
}
function ReplayIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" className="h-4 w-4">
<path
d="M3.5 12a8.5 8.5 0 1 0 2.6-6.1M6 3.4V8h4.6"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export default function TxfxShiny() {
const prefersReduced = useReducedMotion();
const [text, setText] = useState("Built to catch the light");
const [material, setMaterial] = useState<MaterialId>("aurora");
const [auto, setAuto] = useState(true);
const [duration, setDuration] = useState(2.8);
const [replayKey, setReplayKey] = useState(0);
const radioRefs = useRef<Array<HTMLButtonElement | null>>([]);
const activeMaterial =
MATERIALS.find((m) => m.id === material) ?? MATERIALS[0];
const shown = text.trim().length > 0 ? text : "Your words here";
const headingStyle = {
"--txfx-dur": `${duration}s`,
animation: prefersReduced
? undefined
: `txfxShinySweep var(--txfx-dur) ${
auto ? "linear infinite" : "cubic-bezier(0.22,1,0.36,1) 1 both"
}`,
} as CSSProperties;
function onRadioKey(e: KeyboardEvent<HTMLButtonElement>, index: number) {
const last = MATERIALS.length - 1;
let next = index;
if (e.key === "ArrowRight" || e.key === "ArrowDown")
next = index === last ? 0 : index + 1;
else if (e.key === "ArrowLeft" || e.key === "ArrowUp")
next = index === 0 ? last : index - 1;
else if (e.key === "Home") next = 0;
else if (e.key === "End") next = last;
else return;
e.preventDefault();
setMaterial(MATERIALS[next].id);
radioRefs.current[next]?.focus();
}
const focusRing =
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950";
return (
<section className="relative w-full overflow-hidden bg-slate-50 px-5 py-20 text-slate-900 sm:px-8 sm:py-28 dark:bg-slate-950 dark:text-slate-100">
<style>{STYLES}</style>
{/* atmosphere */}
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 opacity-70 [background:radial-gradient(60%_50%_at_50%_-5%,rgba(99,102,241,0.16),transparent_70%),radial-gradient(45%_40%_at_85%_15%,rgba(56,189,248,0.12),transparent_70%)]"
/>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-slate-300 to-transparent dark:via-slate-700"
/>
<div className="relative mx-auto max-w-4xl">
<motion.div
initial={prefersReduced ? false : { opacity: 0, y: 14 }}
whileInView={prefersReduced ? undefined : { opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.4 }}
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
>
<span className="inline-flex items-center gap-1.5 rounded-full border border-slate-200 bg-white/70 px-3 py-1 text-xs font-medium uppercase tracking-[0.18em] text-indigo-600 backdrop-blur dark:border-slate-800 dark:bg-slate-900/60 dark:text-indigo-300">
<SparkIcon />
Text effect · Shiny sweep
</span>
</motion.div>
{/* Preview stage */}
<div className="relative mt-8 overflow-hidden rounded-3xl border border-slate-200 bg-white px-6 py-16 shadow-[0_1px_0_rgba(255,255,255,0.6)_inset,0_30px_60px_-30px_rgba(15,23,42,0.35)] sm:px-12 sm:py-24 dark:border-slate-800 dark:bg-slate-900 dark:shadow-[0_1px_0_rgba(255,255,255,0.04)_inset,0_40px_80px_-40px_rgba(0,0,0,0.7)]">
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 opacity-60 [background-image:linear-gradient(to_right,rgba(100,116,139,0.08)_1px,transparent_1px),linear-gradient(to_bottom,rgba(100,116,139,0.08)_1px,transparent_1px)] [background-size:44px_44px] [mask-image:radial-gradient(ellipse_at_center,black_35%,transparent_75%)]"
/>
<h2
key={replayKey}
data-material={material}
style={headingStyle}
className={`txfx-shiny-text relative text-center text-4xl font-black leading-[1.05] tracking-tight sm:text-6xl md:text-7xl ${
prefersReduced ? "txfx-shiny-static" : ""
}`}
>
{shown}
</h2>
<p className="relative mx-auto mt-6 max-w-md text-center text-sm text-slate-500 dark:text-slate-400">
A metallic light sweep, clipped straight to your letters. Tune the
finish, cadence, and shimmer below.
</p>
</div>
{/* Controls */}
<div className="mt-6 grid gap-4 rounded-3xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900 sm:p-8">
{/* Text input */}
<div className="grid gap-2">
<label
htmlFor="txfx-text"
className="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400"
>
Display text
</label>
<input
id="txfx-text"
type="text"
value={text}
maxLength={34}
onChange={(e) => setText(e.target.value)}
placeholder="Type your headline…"
className={`w-full rounded-xl border border-slate-300 bg-slate-50 px-4 py-2.5 text-base text-slate-900 placeholder:text-slate-400 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100 dark:placeholder:text-slate-500 ${focusRing}`}
/>
</div>
{/* Finish radiogroup */}
<div className="grid gap-2">
<span
id="txfx-finish-label"
className="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400"
>
Finish
</span>
<div
role="radiogroup"
aria-labelledby="txfx-finish-label"
className="grid grid-cols-2 gap-2 sm:grid-cols-4"
>
{MATERIALS.map((m, i) => {
const selected = m.id === material;
return (
<button
key={m.id}
ref={(el) => {
radioRefs.current[i] = el;
}}
type="button"
role="radio"
aria-checked={selected}
tabIndex={selected ? 0 : -1}
onClick={() => setMaterial(m.id)}
onKeyDown={(e) => onRadioKey(e, i)}
className={`group flex items-center gap-2.5 rounded-xl border px-3 py-2.5 text-left transition-colors ${focusRing} ${
selected
? "border-indigo-500 bg-indigo-50 dark:border-indigo-400 dark:bg-indigo-500/10"
: "border-slate-200 bg-slate-50 hover:border-slate-300 dark:border-slate-800 dark:bg-slate-950 dark:hover:border-slate-700"
}`}
>
<span
aria-hidden="true"
style={{ backgroundImage: m.swatch }}
className="h-6 w-6 shrink-0 rounded-full ring-1 ring-inset ring-black/10 dark:ring-white/10"
/>
<span className="min-w-0">
<span
className={`block text-sm font-semibold ${
selected
? "text-indigo-700 dark:text-indigo-200"
: "text-slate-700 dark:text-slate-200"
}`}
>
{m.label}
</span>
<span className="block truncate text-xs text-slate-400 dark:text-slate-500">
{m.hint}
</span>
</span>
</button>
);
})}
</div>
</div>
<div className="grid gap-4 sm:grid-cols-[1fr_auto] sm:items-end">
{/* Speed */}
<div className="grid gap-2">
<label
htmlFor="txfx-speed"
className="flex items-baseline justify-between text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400"
>
<span>Sweep cadence</span>
<span className="font-mono text-[11px] normal-case tracking-normal text-slate-400 dark:text-slate-500">
{duration.toFixed(1)}s / pass
</span>
</label>
<input
id="txfx-speed"
type="range"
min={1.2}
max={6}
step={0.1}
value={duration}
onChange={(e) => setDuration(Number(e.target.value))}
aria-valuetext={`${duration.toFixed(1)} seconds per sweep`}
className={`w-full cursor-pointer accent-indigo-500 dark:accent-indigo-400 ${focusRing} rounded-full`}
/>
<div className="flex justify-between text-[11px] text-slate-400 dark:text-slate-500">
<span>Fast</span>
<span>Slow</span>
</div>
</div>
{/* Auto switch + replay */}
<div className="flex items-center gap-3">
<button
type="button"
role="switch"
aria-checked={auto}
aria-label="Auto shimmer"
onClick={() => setAuto((v) => !v)}
className={`inline-flex h-9 w-16 shrink-0 items-center rounded-full border px-1 transition-colors ${focusRing} ${
auto
? "border-indigo-500 bg-indigo-500 dark:border-indigo-400 dark:bg-indigo-500"
: "border-slate-300 bg-slate-200 dark:border-slate-700 dark:bg-slate-800"
}`}
>
<span
className={`h-7 w-7 rounded-full bg-white shadow-sm transition-transform duration-200 ${
auto ? "translate-x-7" : "translate-x-0"
}`}
/>
</button>
<button
type="button"
onClick={() => setReplayKey((k) => k + 1)}
disabled={auto}
title={
auto
? "Turn off auto shimmer to trigger a single sweep"
: "Play one sweep"
}
className={`inline-flex items-center gap-2 rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 transition-colors hover:border-slate-400 disabled:cursor-not-allowed disabled:opacity-45 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-200 dark:hover:border-slate-600 ${focusRing}`}
>
<ReplayIcon />
Sweep once
</button>
</div>
</div>
<p
aria-live="polite"
className="text-xs text-slate-400 dark:text-slate-500"
>
Finish: {activeMaterial.label} ·{" "}
{prefersReduced
? "Reduced motion — static gleam"
: auto
? "Auto shimmer on"
: "Manual sweep"}{" "}
· {duration.toFixed(1)}s
</p>
</div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Aurora Gradient Headline
OriginalAn animated headline whose living multi-colour gradient drifts across every letter while the words blur and stagger into place on scroll.

Character Reveal Heading
OriginalA cinematic heading that blurs and lifts in one character at a time with a staggered, scroll-triggered reveal.

Shimmer Sweep Headline
OriginalA headline with a polished light sheen that glides endlessly across the letters and races faster on hover.

Rotating Words Headline
OriginalA headline whose final word flips through a curated set with a blurred vertical roll while the rest of the line stays perfectly still.

Gradient Animate Text Effect
Originalanimated gradient text

Typing Text Effect
Originaltypewriter text with caret

Glitch Text Effect
Originalglitch text effect

Wave Text Effect
Originalwavy letter animation

Reveal Mask Text Effect
Originalmask reveal heading

Outline Text Effect
Originaloutline-to-fill text

3D Text Effect
Originallayered 3D text

Scramble Text Effect
Originalscramble/decode text

