Decrypting Text

Displays text with a decrypting animation effect, revealing the final content through randomized characters.

decrypting-text-animation-preview.jsx

import DecryptingTextAnimation from "@/components/text-effects/decrypting-text-animation/decrypting-text-animation";

/* keep text on center */
const wrapperStyles = {
  textAlign: "center",
  padding: "24px",
  textWrap: "balance",
};

const DecryptingTextAnimationPreview = () => {
  return (
    <div style={wrapperStyles}>
      <p style={{
        fontFamily: "monospace",
        fontSize: "1.5rem",
      }}>
        Beyond the encrypted noise lives {" "}
        <strong>
          <DecryptingTextAnimation
            text="Pure Awareness"
            speed={50}
          />
        </strong>
      </p>
    </div>
  );
};

export default DecryptingTextAnimationPreview;

Installation

Run the command from your project root directory (the folder that contains package.json).

Terminal / Console

npx mosaicui-cli@latest text-effects/decrypting-text-animation

1. Copy the component file

Create a new file called decrypting-text-animation.jsx in your reusable components folder (for example /src/components/) and paste the following code into it.

decrypting-text-animation.jsx

import { memo, useCallback, useEffect, useMemo, useState, Fragment } from "react";
import styles from "./decrypting-text-animation.module.css";

const random = (n = 1) => {
  return Math.floor(Math.random() * n);
};

const DecryptingTextAnimation = (props) => {
  const {
    text,
    speed = 50,
    charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890!@#$%&*-+?",
    className,
    ...restProps
  } = props;

  const letterVariants = ["outlined", "filled"];

  const getRandomLetterVariant = useCallback(() => {
    return letterVariants[random(letterVariants.length)];
  }, []);

  const getTextMappings = () => {
    return (
      text
        ?.split(" ")
        .filter(Boolean)
        .map(word => (
          word
            .split("")
            .map((letter) => ({
              letter,
              variant: getRandomLetterVariant(),
              index: random(charset.length),
            }))
        ))
    );
  };

  const [currentText, setCurrentText] = useState(text);
  const [textMapping, setTextMapping] = useState(getTextMappings);

  const shuffledCharset = useMemo(() => {
    return charset.split("").sort(() => (
      random(5) - random(5)
    )).join("");
  }, [charset, text]);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setTextMapping((prev) => {
        const areAllDone = prev.flat().every(entry => {
          return entry.letter === shuffledCharset[entry.index];
        });
        if(areAllDone) {
          clearInterval(intervalId);
        }
        return prev.map(word => (
          word.map(entry => {
            const isDone = entry.letter === shuffledCharset[entry.index];
            return {
              ...entry,
              variant: isDone ? "" : getRandomLetterVariant(),
              index: isDone ? (
                entry.index
              ) : (
                (entry.index + 1) % shuffledCharset.length
              )
            };
          })
        ))
      });
    }, speed);
    return () => {
      clearInterval(intervalId);
    };
  }, [shuffledCharset, speed, getRandomLetterVariant]);

  if (currentText !== text) {
    setCurrentText(text);
    setTextMapping(getTextMappings());
  }

  return (
    <span
      {...restProps}
      className={[
        className,
        styles["decrypting-text"],
      ].join(" ")}
    >
      {
        textMapping.map((word, wordIndex, arr) => (
          <Fragment key={`word-${wordIndex}`}>
            <span 
              aria-hidden={true}
              className={styles["word"]}
            >
              {word.map((letter, letterIndex) => {
                return (
                  <span
                    key={`letter-${wordIndex}-${letterIndex}`}
                    aria-hidden={true}
                    className={[
                      styles["letter"],
                      styles[`letter-${letter.variant}`]
                    ].join(" ")}
                  >
                    {shuffledCharset[letter.index]}
                  </span>
                );
              })}
            </span>
            {wordIndex !== (arr.length - 1) && (
              <span aria-hidden={true}>
                &nbsp;
              </span>
            )}
          </Fragment>
        ))
      }
      <span
        className={styles["sr-only"]}
      >
        {currentText}
      </span>
    </span>
  )
};

export default memo(DecryptingTextAnimation);

2. Copy the CSS module file

In the same folder, create a file called decrypting-text-animation.module.css and paste the following CSS.

decrypting-text-animation.module.css

.decrypting-text {
  position: relative;

  .word {
    white-space: nowrap;
    word-break: keep-all;

    .letter {
      display: inline-block;
      font: inherit;
      transition: all 100ms ease-in-out;

      &.letter-filled {
        background: currentColor;
      }

      &.letter-outlined {
        box-shadow: inset 0 0 1px 1px currentColor;
      }
    }
  }

  > .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
    animation: none;
  }
}

Props

Configure the component with the following props:

PropTypeRequiredDefaultDescription
textstringYes-The text content to be decrypted and displayed. All characters must exist in the specified charset. If the text includes characters outside this charset, a custom charset prop must be provided.
speednumberNo50Speed in milliseconds between each decrypting step.
charsetstringNo"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890!@#$%&*-+?"The character set used to generate random decrypting characters.
classNamestringNoOptional class name applied to the root container.
styleReact.CSSPropertiesNoInline styles applied to the root container.