Skip to main content

Cipher Text Reveal

Text that decodes itself: characters reveal left to right while a short window of cipher glyphs churns just ahead of the reveal edge, like a lock finding its combination. One interval drives reveal, tail resolution, and completion; the full text stays in an sr-only span for screen readers, and a ref handle exposes start/reset for sequencing lines.

TextReactTailwind CSS
CSSTailwind

Manual

Create a file and paste the following code into it.

cipher-text-reveal.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
"use client";

import { type Ref, useCallback, useEffect, useImperativeHandle, useState } from "react";

/* ------------------------------------------------------------------ */
/*  CipherTextReveal                                                   */
/* ------------------------------------------------------------------ */

interface CipherTextRevealProps {
  text: string;
  /** ms between reveal ticks [Optional, default: 50] */
  speed?: number;
  /** Scrambled chars running ahead of the revealed text [Optional, default: 2] */
  cipherCount?: number;
  /** Glyph pool for the scramble [Optional] */
  characters?: string;
  className?: string;
  /** Class for the still-scrambled portion [Optional] */
  cipherClassName?: string;
  /** Start on mount [Optional, default: true] */
  autoStart?: boolean;
  delay?: number;
  onComplete?: () => void;
  ref?: Ref<CipherTextRevealHandle>;
}

export interface CipherTextRevealHandle {
  start: () => void;
  reset: () => void;
}

/**
 * Text that decodes itself: characters reveal left to right while a short
 * window of cipher glyphs churns just ahead of the reveal edge, like a lock
 * finding its combination. Exposes start/reset through a ref handle and
 * keeps the full text in an sr-only span for screen readers.
 * @param {string} text - Final text [Required]
 * @param {number} cipherCount - Churning glyphs ahead of the edge [Optional, default: 2]
 */
export function CipherTextReveal({
  text,
  speed = 50,
  cipherCount = 2,
  characters = "abcdefghijklmnopqrstuvwxyz!@#$%^&*()_+",
  className = "",
  cipherClassName = "",
  autoStart = true,
  delay = 0,
  onComplete,
  ref,
}: CipherTextRevealProps) {
  const [display, setDisplay] = useState("");
  const [running, setRunning] = useState(false);
  const [visible, setVisible] = useState(0);
  const [tailTick, setTailTick] = useState(0);

  const start = useCallback(() => {
    setRunning(true);
    setVisible(0);
    setTailTick(0);
  }, []);

  const reset = useCallback(() => {
    setRunning(false);
    setVisible(0);
    setTailTick(0);
    setDisplay("");
  }, []);

  useImperativeHandle(ref, () => ({ start, reset }), [start, reset]);

  useEffect(() => {
    if (!autoStart) return;
    if (delay > 0) {
      const t = setTimeout(start, delay);
      return () => clearTimeout(t);
    }
    start();
  }, [autoStart, delay, start]);

  useEffect(() => {
    if (!running) return;
    const interval = setInterval(() => {
      if (visible < text.length) {
        setVisible((v) => v + 1);
      } else if (tailTick < cipherCount) {
        setTailTick((t) => t + 1);
      } else {
        clearInterval(interval);
        setRunning(false);
        onComplete?.();
      }

      const remaining = Math.max(0, text.length - visible);
      const churn = Math.min(remaining, cipherCount);
      const cipher = Array.from(
        { length: churn },
        () => characters[Math.floor(Math.random() * characters.length)],
      ).join("");
      setDisplay(text.slice(0, visible) + cipher);
    }, speed);
    return () => clearInterval(interval);
  }, [running, text, visible, tailTick, cipherCount, characters, speed, onComplete]);

  const revealed = display.slice(0, visible);
  const cipher = display.slice(visible);

  return (
    <>
      <span className="sr-only">{text}</span>
      <span className="inline-block whitespace-pre-wrap" aria-hidden="true">
        <span className={className}>{revealed}</span>
        <span className={cipherClassName}>{cipher}</span>
      </span>
    </>
  );
}

Update the import paths to match your project setup.

Similar components

Spotlight Quotes

Prism Reveal Text

Char Spring Morph

Snap Scroll Headlines

Install via CLI

Resource details

PublishedJuly 17, 2026
CategoryText
ReactTailwind CSS