Headings that decode into place: letters cycle through glyphs and lock in like a signal resolving. Five modes, your own glyph sets, real text for screen readers and no layout shift.

  • text
  • heading
  • scramble
  • decode
  • typewriter
  • hover
  • headline
  • typography
  • dom
Preset

Installation

pnpm dlx shadcn@latest add https://kagadmodyaa.vercel.app/r/scramble-text.json

Needs a React + Tailwind project set up with shadcn. Copies the effect into your project, along with lib/effect-runtime.ts, the small WebGL layer it runs on. No other dependencies.

Or from npm

pnpm add @kagadmodyaa/effects
import { ScrambleText } from '@kagadmodyaa/effects/scramble-text'

One package with every free effect, one import path each. No Tailwind or shadcn needed, and no stylesheet to import: the few layout rules an effect needs come with it. The npm package and the command above ship the same source.

Usage

Use it like any other component. Choose a theme with preset; any prop you pass as well overrides the preset.

import { ScrambleText } from '@/components/effects/scramble-text/scramble-text'

export function Hero() {
  return (
    <>
      {/* Decodes when it scrolls into view, once */}
      <ScrambleText as="h1" preset="Editorial" trigger="view" size={72}>
        Meaning finds its place.
      </ScrambleText>

      {/* Typewriter with a caret, in the surrounding font and colour */}
      <ScrambleText as="p" mode="type" font="inherit" color="inherit" accent="#e0562e">
        Built in the open, shipped on Fridays.
      </ScrambleText>

      {/* Hover any word to shuffle it, with your own glyphs */}
      <ScrambleText as="h2" mode="shuffle-on-hover" glyphs="·•◦○" trigger="none">
        Hover these words
      </ScrambleText>
    </>
  )
}

// Imperative replay, and a callback when it has settled:
//   const ref = useRef<ScrambleTextHandle>(null)
//   <ScrambleText ref={ref} onDone={() => console.log('resolved')}>Hello</ScrambleText>
//   <button onClick={() => ref.current?.replay()}>Replay</button>
// The stage above is <ScrambleTextShowcase />, also exported, if you want the same specimen page.
<ScrambleText preset="Editorial" mode={"decode"} />

Presets

  • Terminal
  • Editorial
  • Neon
  • Mono
  • Gold

Props

NameTypeDefaultDescription
preset'Terminal' | 'Editorial' | 'Neon' | 'Mono' | 'Gold'none (the defaults below)A named theme. Any prop you pass as well overrides the preset’s value.
mode'decode' | 'random' | 'wave' | 'type' | 'shuffle-on-hover''decode'How the text resolves: left-to-right sweep, random order, sine wave, typewriter, or per-word shuffle on hover.
glyphs'symbols' | 'binary' | 'latin' | 'blocks''symbols'What unresolved letters cycle through: a named set, or any string of your own characters (the panel lists the named sets).
durationnumber1.4Seconds from the first letter starting to the last one locking in. Range 0.3 to 4 s.
staggernumber0.70 locks every letter together at the end, 1 is a pure one-after-another sweep. Range 0 to 1.
delaynumber0Seconds to wait before it starts. Range 0 to 3 s.
trigger'mount' | 'view' | 'hover' | 'none''mount'When it plays: after mount, on entering the viewport, on hover, or only through replay().
accentstring'#4dffa0'The colour a letter flashes as it locks in. A hex colour.
scrambleColorstring'#2f7a53'Colour of letters that are still scrambling. A hex colour.
colorstring'#d8f2de'Colour of the settled text. A hex colour.
backgroundstring'#060a08'Page colour of the stage and gallery card. The component itself is transparent. A hex colour.
glownumber0.5Soft halo around a freshly locked letter. Range 0 to 1.
font'display' | 'mono' | 'serif' | 'inherit''mono'Typeface of the text; inherit takes it from the parent.
scrambleFont'same' | 'mono''mono'Typeface of the scrambling letters: the text's own, or tidy mono glyphs.
sizenumber88Headline size in px at a 1000px wide stage; the showcase scales it with its width. ScrambleText itself takes any CSS length, or the parent's size. Range 28 to 160 px.
weightnumber500Font weight. Range 300 to 900.
letterSpacingnumber-0.03Letter spacing in em. Range -0.08 to 0.2 em.
classNamestring—Extra class on the root element. The effect fills its parent: give the parent a height.
styleCSSProperties—Inline style on the root element.

Files

What the command adds to your project, and where.

  • components/effects/scramble-text/scramble-text.tsx

    The component.

  • components/effects/scramble-text/text.tsx

    The component.

  • components/effects/scramble-text/showcase.tsx

    The component.

  • components/effects/scramble-text/engine.ts

    The component.

  • components/effects/scramble-text/look.ts

    The component.

  • components/effects/scramble-text/styles.ts

    The component.

  • components/effects/scramble-text/stage-styles.ts

    The component.

  • lib/effect-runtime.ts

    Shared by every shader effect: resizing, pausing, reduced motion, context loss and the no-WebGL fallback. Installed once.

Behaviour

  • The real text is always in the DOM. A visually hidden copy carries it for screen readers (headings also get an aria-label); the letters you see are aria-hidden. Selecting and copying gets the real text.
  • Server and first client render show the settled text, so there is no hydration mismatch. With trigger 'mount' or 'view' the text is held back until it plays (and appears by itself after a moment if scripts never run); with 'hover' or 'none' it simply sits there.
  • Every letter is a cell with the width of its real glyph; the scrambling glyph floats centred inside it. The line, its wrapping and the page never move while a word resolves. Narrow letters scramble through narrow marks.
  • One requestAnimationFrame loop per instance, running only while letters are moving. It changes a data attribute when a letter changes state and a glyph when it swaps: no layout reads, no layout-affecting writes.
  • Modes: decode sweeps left to right behind a soft band of noise, random locks letters in random order, wave follows a sine ripple, type is a typewriter with a blinking caret, and shuffle-on-hover re-scrambles the word under the pointer (or finger) and resolves it again.
  • Triggers: on mount, on entering the viewport (once or every time, with once={false}), on hover, or only through ref.replay(). Changing mode, glyphs, duration, stagger or delay plays it again.
  • Under prefers-reduced-motion the text just shows, replay() resolves immediately and the caret stays out of sight.

See all effects