Text Scramble (Load, Scroll, Hover)

Three scramble text patterns in one script: characters randomise on page load, on scroll entry, and on hover. Uses GSAP's ScrambleTextPlugin with SplitText for a per-word scramble effect, with configurable character sets.

GSAPScrambleTextSplitTextScrollTriggerHover

Setup — External Scripts

GSAP CDN
html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"></script>
ScrambleTextPlugin CDN
html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/ScrambleTextPlugin.min.js"></script>
SplitText CDN
html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/SplitText.min.js"></script>
ScrollTrigger CDN
html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/ScrollTrigger.min.js"></script>

Code

index.html
html
<div class="demo-group">
  <!-- Scramble on load -->
  <div class="scramble-section">
    <h1 data-scramble="load" class="scramble-heading">This heading will reveal with a basic scrambling effect on page load</h1>
  </div>

  <!-- Scramble on scroll -->
  <div class="scramble-section u--bg-light">
    <h2 data-scramble="scroll" class="scramble-heading">This heading is triggered by a ScrollTrigger</h2>
  </div>

  <!-- Scramble on scroll — alternate character set -->
  <div class="scramble-section">
    <h2 data-scramble-alt="" data-scramble="scroll" class="scramble-heading">You can control the characters used during scramble</h2>
  </div>

  <!-- Scramble on hover -->
  <div class="scramble-section u--bg-light">
    <h2 class="scramble-heading">And here's how to work with scramble text on hover:</h2>
    <a data-scramble-hover="link" href="#" class="scramble-button">
      <p data-scramble-text="this text can be custom too" data-scramble-hover="target" class="scramble-button-text">How to scramble on hover</p>
    </a>
  </div>
</div>
styles.css
css
.scramble-section {
  grid-column-gap: 2em;
  grid-row-gap: 2em;
  flex-flow: column;
  justify-content: center;
  align-items: center;
  width: 100%;
  min-height: 100vh;
  display: flex;
}

.scramble-section.u--bg-light {
  background-color: #efeeec;
}

.scramble-heading {
  text-align: center;
  letter-spacing: -.03em;
  text-transform: uppercase;
  max-width: 12em;
  margin: 0 auto;
  font-family: RM Mono, Arial, sans-serif;
  font-size: 3em;
  font-weight: 400;
  line-height: .9;
}

.scramble-button {
  color: #131313;
  text-transform: uppercase;
  border: 1px dotted #000;
  border-radius: .3125em;
  padding: .5em 1em;
  font-family: RM Mono, Arial, sans-serif;
  font-size: 1em;
  font-weight: 400;
  text-decoration: none;
}

.scramble-button-text {
  margin: 0;
}
script.js
javascript
gsap.registerPlugin(ScrollTrigger, ScrambleTextPlugin, SplitText)

function initScrambleOnLoad() {
  document.querySelectorAll('[data-scramble="load"]').forEach(target => {
    const split = new SplitText(target, { type: "words, chars", wordsClass: "word", charsClass: "char" });

    gsap.to(split.words, {
      duration: 1.2,
      stagger: 0.01,
      scrambleText: { text: "{original}", chars: 'upperCase', speed: 0.85 },
      onComplete: () => split.revert(),
    });
  });
}

function initScrambleOnScroll() {
  document.querySelectorAll('[data-scramble="scroll"]').forEach(target => {
    const isAlternative = target.hasAttribute("data-scramble-alt");
    const split = new SplitText(target, { type: "words, chars", wordsClass: "word", charsClass: "char" });

    gsap.to(split.words, {
      duration: 1.4,
      stagger: 0.015,
      scrambleText: {
        text: "{original}",
        chars: isAlternative ? '▯|' : 'upperCase',
        speed: 0.95,
      },
      scrollTrigger: { trigger: target, start: "top bottom", once: true },
      onComplete: () => split.revert(),
    });
  });
}

function initScrambleOnHover() {
  document.querySelectorAll('[data-scramble-hover="link"]').forEach(target => {
    const textEl        = target.querySelector('[data-scramble-hover="target"]');
    const originalText  = textEl.textContent;
    const customHoverText = textEl.getAttribute("data-scramble-text");

    new SplitText(textEl, { type: "words, chars", wordsClass: "word", charsClass: "char" });

    target.addEventListener("mouseenter", () => {
      gsap.to(textEl, {
        duration: 1,
        scrambleText: { text: customHoverText || originalText, chars: "◊▯∆|" },
      });
    });

    target.addEventListener("mouseleave", () => {
      gsap.to(textEl, {
        duration: 0.6,
        scrambleText: { text: originalText, speed: 2, chars: "◊▯∆" },
      });
    });
  });
}

document.addEventListener("DOMContentLoaded", () => {
  initScrambleOnLoad();
  initScrambleOnScroll();
  initScrambleOnHover();
});

Notes

  • Splitting text into individual words before applying `scrambleText` gives a nicer per-word scramble cadence than applying it directly to the whole element — each word resolves independently.
  • `split.revert()` is called in `onComplete` to remove all the extra character spans from the DOM after animation, keeping the page lean.
  • The `chars` option in `ScrambleTextPlugin` accepts `'upperCase'`, `'lowerCase'`, `'digits'`, or any custom string of characters (e.g. `'▯|'`, `'◊▯∆'`).
  • Hover scramble reads `data-scramble-text` for a custom target string — if omitted, it rescrambles back to the original text on both enter and leave.
  • The scroll version uses `once: true` so each heading only scrambles the first time it enters the viewport.