Shuffle

A cyberpunk-inspired text reveal with random character decryption effect.

Overview

A text animation simulating a decryption process by cycling through random characters before settling on the final string. Perfect for futuristic UIs, data visualizations, or impactful headlines.


Features

  • Rapid random character cycling
  • Configurable reveal speed
  • Zero external dependencies
  • Auto-play on mount
  • Monospace font optimized
  • Hover effect built-in

Preview

Live Preview

ZENBLOCKS


Installation

Install via the ZenBlocks CLI to automatically handle dependencies:

npx shadcn@latest add https://zenblocks-three.vercel.app/r/shuffle.json

Method 2: Manual

  1. Install Dependencies

    No external dependencies required.

  2. Copy the Source Code

    Copy the code below into components/zenblocks/shuffle.tsx.

Click to expand source
"use client";

import React, { useState, useEffect, useRef } from "react";
import { cn } from "@/lib/utils";

interface ShuffleProps extends React.HTMLAttributes<HTMLSpanElement> {
  text: string;
  duration?: number;
  delay?: number;
}

export function Shuffle({
  text = "Shuffle",
  duration = 0.5,
  className,
  ...props
}: ShuffleProps) {
  const [displayText, setDisplayText] = useState(text);
  const [isHovered, setIsHovered] = useState(false);
  const intervalRef = useRef<NodeJS.Timeout | null>(null);

  const originalText = useRef(text);

  useEffect(() => {
    originalText.current = text;
    setDisplayText(text);
  }, [text]);

  const startShuffle = () => {
    if (intervalRef.current) clearInterval(intervalRef.current);

    let iteration = 0;
    const steps = duration * 30;
    const increment = text.length / steps;

    intervalRef.current = setInterval(() => {
      setDisplayText((prev) =>
        originalText.current
          .split("")
          .map((letter, index) => {
            if (index < iteration) {
              return originalText.current[index];
            }
            return String.fromCharCode(65 + Math.floor(Math.random() * 26));
          })
          .join("")
      );

      if (iteration >= originalText.current.length) {
        if (intervalRef.current) clearInterval(intervalRef.current);
        setDisplayText(originalText.current);
      }

      iteration += increment;
    }, 30);
  };

  const handleMouseEnter = (e: React.MouseEvent<HTMLSpanElement>) => {
    setIsHovered(true);
    startShuffle();
    props.onMouseEnter?.(e);
  };

  useEffect(() => {
    startShuffle();
    return () => {
      if (intervalRef.current) clearInterval(intervalRef.current);
    }
  }, []);

  return (
    <span
      className={cn("inline-block whitespace-nowrap", className)}
      onMouseEnter={handleMouseEnter}
      {...props}
    >
      {displayText}
    </span>
  );
}

Usage

import { Shuffle } from "@/components/zenblocks/shuffle";

<h1>
  <Shuffle text="ACCESS GRANTED" />
</h1>

Props

PropTypeDefaultDescription
textstring-Final string to display
durationnumber1.0Animation time in seconds
classNamestring-Styling for text span

Accessibility

  • Recommended to detect prefers-reduced-motion and skip animation
  • Use aria-label on parent to announce final text immediately
  • Ensure sufficient contrast ratios
  • Avoid for critical information that needs instant readability

Customization

Typography

Works best with monospaced fonts to prevent layout jitter during character changes.

<Shuffle 
  className="font-mono tracking-widest" 
  text="SYSTEM_READY" 
/>

Duration

<Shuffle text="LOADING" duration={2.0} />

Motion Behavior

  • Cycle: Replaces characters every 30-50ms
  • Reveal: Linearly increases index of "solved" characters left to right
  • Character set: Random uppercase letters (A-Z)

Performance Notes

  • Uses setInterval for animation loop
  • Cleaned up on unmount
  • If switching text rapidly, use unique key prop to force re-mount

Examples

Badge

<div className="badge">
  <span className="dot" />
  <Shuffle text="v2.0 Live" />
</div>

Hero Headline

<h1 className="text-6xl font-bold">
  <Shuffle text="WELCOME" duration={1.5} />
</h1>

Notes

  • Avoid using for long paragraphs (shifting text can move layout)
  • Best for short, impactful phrases
  • Character width changes can cause jitter with non-monospace fonts
  • Component is client-side only