shadimage
ratneshc
componentblur-shimmer-textsource: ok · hit

Blur Shimmer Text

A text component that loops phrases with a horizontal blur shimmer.

npx shadcn add "https://ratneshc.com/r/blur-shimmer-text.json"
free

Visual style

glass-blur

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
animationmotion

Files1 · source cached 10/14/2026, 3:35:37 PM

src/registry/components/blur-shimmer-text/blur-shimmer-text.tsx
"use client";

import * as React from "react";
import type { Transition, Variants } from "motion/react";
import { AnimatePresence, motion } from "motion/react";

import { cn } from "@/lib/utils";

type MotionElement =
  | typeof motion.p
  | typeof motion.span
  | typeof motion.code
  | typeof motion.h1
  | typeof motion.h2
  | typeof motion.h3;

export type BlurShimmerTextProps = {
  /** Element to render as.
   * @defaultValue motion.p */
  as?: MotionElement;
  /** Additional class names. */
  className?: string;
  /** Seconds each text is visible before switching.
   * @defaultValue 2 */
  interval?: number;
  /** Blur radius in pixels at the peak of the shimmer effect.
   * @defaultValue 6 */
  blur?: number;
  /** Motion transition for the blur sweep. */
  transition?: Transition;
  /** Motion variants for each character. Overrides the blur prop. */
  variants?: Variants;
  /** List of phrases to loop through. */
  texts: string[];
};

export function BlurShimmerText({
  as = motion.p,
  className,
  interval = 2,
  blur = 6,
  transition = { duration: 0.5 },
  variants,
  texts,
}: BlurShimmerTextProps) {
  const [currentIndex, setCurrentIndex] = React.useState(0);
  const Component = as as typeof motion.p;

  const resolvedVariants: Variants = variants ?? {
    initial: { filter: `blur(${blur}px)`, opacity: 0 },
    animate: { filter: "blur(0px)", opacity: 1 },
    exit: { filter: `blur(${blur}px)`, opacity: 0 },
  };

  React.useEffect(() => {
    if (!texts || texts.length === 0) return;

    const timeout = setInterval(() => {
      setCurrentIndex((prev) => (prev + 1) % texts.length);
    }, interval * 1000);

    return () => clearInterval(timeout);
  }, [texts, interval]);

  if (!texts || texts.length === 0) return null;

  const duration = (transition.duration as number) || 0.6;

  return (
    <div className={cn("inline-grid", className)}>
      {texts.map((text, index) => (
        <span
          key={index}
          className="invisible col-start-1 row-start-1 block whitespace-nowrap"
          aria-hidden="true"
        >
          {text.split("").map((char, i) => (
            <span key={i} className="inline-block whitespace-pre">
              {char}
            </span>
          ))}
        </span>
      ))}
      <AnimatePresence>
        <Component
          key={currentIndex}
          className="col-start-1 row-start-1 block whitespace-nowrap"
          initial="initial"
          animate="animate"
          exit="exit"
        >
          {texts[currentIndex].split("").map((char, i) => {
            const staggerDelay = (i * duration) / texts[currentIndex].length;

            return (
              <motion.span
                key={i}
                variants={resolvedVariants}
                transition={{
                  ...transition,
                  delay: staggerDelay,
                }}
                className="inline-block whitespace-pre"
              >
                {char}
              </motion.span>
            );
          })}
        </Component>
      </AnimatePresence>
    </div>
  );
}

More from ratneshc

Browse all components in this library →