Web InnoventixFreeCode

Shiny Text Effect

Original · free

shiny sweep text

byWeb InnoventixReact + Tailwind
txfxshinytext-effects
Open

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
txfx-shiny.tsx
"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

motion

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 quote

Similar components

Browse all →