Skip to content

ScrambleText

Reveal short text through changing random characters.

Use it for short technical strings that change: a deploy status, a commit hash, a region code, a label that reacts to hover. The noise says "computed", so keep it to a few words, and don't use it on sentences, prices or numbers people compare. For a number that moves, use Counter.

Installation

npm i @youj/motion

import { ScrambleText } from '@youj/motion'

For an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.

Usage

import { ScrambleText } from '@youj/motion'

export function DeployStatus({ status }: { status: string }) {
  return (
    <span role="status" className="font-mono text-xs uppercase">
      <ScrambleText>{status}</ScrambleText>
    </span>
  )
}

By default it decodes when it mounts and again whenever its text changes, starting from whatever it showed last. A monospace face, or a fixed width such as w-[9ch], keeps the layout still while the glyphs roll.

Examples

Triggers

trigger takes one of mount, view, hover and change, or an array of them.

  • mount plays once, before the first paint.
  • view plays once, when 60% of the element is on screen.
  • hover plays when the pointer enters the element, and when the element itself takes focus.
  • change plays when the text changes. Without it, new text swaps in place.

hover listens on ScrambleText's own element. Render it as the control itself, with as="button", so a keyboard user who tabs to it sees the same decode as the pointer. Focus on a wrapping button or link doesn't reach a child.

Reference

Propschildren, as, trigger… (6)

Props

ScrambleText props
PropType
childrenRequiredstring

Target text. Changing it scrambles from the current text to the new one.

–
asElementType

default 'span'

'span'
triggerScrambleTrigger | ScrambleTrigger[]

When to play.

default ['mount', 'change']

['mount', 'change']
glyphsstring

Characters used while decoding.

default GLYPHS.alnum

GLYPHS.alnum
durationDurationToken
'instant''fast''base''slow''deliberate'

Total time for the last character to resolve.

default 'deliberate'

'deliberate'
tickDurationToken
'instant''fast''base''slow''deliberate'

How often unresolved glyphs re-roll.

default 'instant'

'instant'
onComplete() => void

Called when a decode lands on the text (at once under reduced motion).

–
Other props pass through: extends HTMLAttributes<HTMLElement>.
MotionDecode, glyph re-roll · reduced motion

Motion

Motion tokens
MomentTokens
Decodepositions resolve left to right with jitter; the last one lands at duration
duration.deliberate

560ms

560ms
Glyph re-rollunresolved positions redraw every half tick
duration.instant

80ms

80ms

Reduced motion

The new text replaces the old at once. No glyphs.

Each position resolves at a time between 15% and 100% of duration, later for positions further right, with up to a quarter of the duration in random jitter. That reads as decoding rather than typing. Frames are written straight to the text node, so a decode never re-renders React.

Reduced motion

Text renders as it is and changes in place, instantly. Nothing decodes, on any trigger, and the status in the first preview simply reads Queued, Building, Deploying and Ready in turn.

AccessibilityKeyboard, ARIA, focus

Accessibility

  • The real text is rendered once in a visually hidden span, and the decoding copy is aria-hidden. A screen reader reads the text, never the noise.
  • The hidden copy updates the moment the text changes. To announce a change, put ScrambleText inside a live region, as the status in the first preview does with role="status".
  • hover also plays on keyboard focus when ScrambleText is the focusable element. Give it as="button" rather than wrapping it.
  • The effect is visual only. Keep it to short strings, because noise over a long string reads as broken.