Animate between two anagram words in React — every letter glides to its new position.
Every letter of the first word is paired with the matching letter of the second, then animated from where it starts to where it ends up. Nothing fades, nothing is faked — the letters you are reading are the letters that move.
- Zero runtime dependencies
- TypeScript types included
- SSR / Next.js App Router safe — no CSS import, no browser globals at module scope
- Respects
prefers-reduced-motion - ESM and CommonJS, React 17, 18 and 19
npm install react-anagram-animationimport Anagram from 'react-anagram-animation';
export default function Hero() {
return <Anagram words={['bad credit', 'debit card']} />;
}There is no stylesheet to import. Style it like any other text:
.anagram-swap {
font-family: 'Open Sans', sans-serif;
font-size: 42px;
font-weight: bold;
color: #fff;
text-transform: uppercase;
}Control the timing with animationOptions. Every value is in milliseconds.
<Anagram
words={['React Anagram Animation', 'Magenta Raincoat Airman']}
animationOptions={{
randomStartMin: 0,
randomStartMax: 3000,
randomReverseMin: 6000,
randomReverseMax: 6000,
loopAnimation: 20000,
waitToStart: 5000,
transitionDuration: 2000,
timingFunction: 'ease-in-out',
}}
/>If the text uses a webfont, name it with fontToObserve so the letters are measured after the font loads rather than before:
<Anagram fontToObserve="Open Sans" />className and style are merged with the component's own, and any other prop — id, data-*, aria-* — is forwarded to the root element.
| Prop | Type | Default | Description |
|---|---|---|---|
words |
[string, string] |
['React Anagram Animation', 'Magenta Raincoat Airman'] |
Exactly two words that are anagrams of each other, including spaces and punctuation. |
animationOptions |
AnimationOptions |
see below | Timing. Any subset; the rest fall back to the defaults. |
fontToObserve |
string |
— | Font family to wait for before measuring. Omit to render immediately. |
All times are in milliseconds. The randomness is what produces the jumbled, staggered effect — set min equal to max to make every letter move in lockstep instead.
| Property | Type | Default | Description |
|---|---|---|---|
randomStartMin |
number |
0 |
Minimum wait before a letter starts moving. |
randomStartMax |
number |
3000 |
Maximum wait before a letter starts moving. Should be >= randomStartMin. |
randomReverseMin |
number |
6000 |
Minimum wait before a letter heads back. |
randomReverseMax |
number |
9000 |
Maximum wait before a letter heads back. Should be >= randomReverseMin. |
loopAnimation |
number |
12000 |
Wait before the next full cycle. Should be >= randomReverseMax + transitionDuration. |
waitToStart |
number |
0 |
Wait before the very first run. |
transitionDuration |
number |
1000 |
How long a letter takes to travel. Should be <= randomReverseMin - randomStartMax. |
timingFunction |
string |
'ease-in-out' |
Any CSS timing function, including cubic-bezier(...). |
import Anagram, { isAnagram, DEFAULT_ANIMATION_OPTIONS } from 'react-anagram-animation';
isAnagram('bad credit', 'debit card'); // true — ignores case, spaces and punctuation
DEFAULT_ANIMATION_OPTIONS.loopAnimation; // 12000CommonJS consumers reach the component through .default:
const Anagram = require('react-anagram-animation').default;The package ships no CSS. Only a handful of structural rules (the ones that make measurement possible) are applied inline; everything visual is yours. Target these class names:
| Class | Element |
|---|---|
.anagram-swap |
Root element. Set font, color, text-transform here. |
.anagram-word |
Each of the three word layers. |
.anagram-word-animation |
The visible, animating layer. |
.anagram-letter |
Every individual letter. |
Types ship with the package; nothing extra to install.
import Anagram, { type AnimationOptions } from 'react-anagram-animation';
const options: AnimationOptions = { transitionDuration: 2000 };
<Anagram words={['bad credit', 'debit card']} animationOptions={options} />;words is typed as a two-element tuple, so a third word is a compile error. If you hoist the array, use as const:
const words = ['bad credit', 'debit card'] as const;The component is safe to import from a server component or any SSR context: it imports no CSS and touches no browser globals at module scope. On the server it renders the first word as real text (good for crawlers), then measures and animates after hydration.
It is a client component, so in the Next.js App Router use it from a file with 'use client'.
prefers-reduced-motion: reduceis respected. The word renders at rest and no timers are scheduled at all. There is no prop to override this — the animation is decorative.- The two hidden measurement copies are
aria-hidden, so a screen reader reads the phrase once.
| Both words are true anagrams | This package. Smaller, and every letter is accounted for — each one travels to a real destination. |
| Any two words or phrases | react-text-swap-animation. Handles unmatched letters by fading them in and out. |
v2 removes the stylesheet that used to ship with the package. It forced color: #fff, text-transform: uppercase and width: 100% on every consumer, which made the component render invisibly on a light background.
To restore the v1 appearance, add this to your own CSS:
.anagram-swap {
color: #fff;
text-transform: uppercase;
text-align: left;
width: 100%;
margin: 0 auto;
padding: 0;
}Other breaking changes:
- Class names are namespaced.
.word→.anagram-word,.letter→.anagram-letter, and.hiddenis gone. (.hiddencollided with Tailwind's.hidden { display: none }.) - No more
import 'react-anagram-animation/dist/components/index.css'— there is no CSS file. - Deep imports are gone.
react-anagram-animation/dist/utilsand friends no longer resolve; use the named exports. mainis nowdist/index.cjs, alongside a real ESM build atdist/index.mjs.- Browser floor is now ~Chrome 80 / Safari 13.1 (was ~Chrome 67), because
core-jswas dropped. prefers-reduced-motionis respected, so some users will see a static word.- Non-anagram input no longer throws. It logs an error and renders the first word.
See CHANGELOG.md for the full list.
npm install
npm start # demo at http://localhost:5173
npm test
npm run lint
npm run build # builds the library into dist/Merging to main never publishes. Only pushing a v* tag does, so docs,
dependency bumps and CI changes can land freely.
1. Check what will actually ship. From a fresh clone, so nothing uncommitted or stale in your working directory can leak into the package:
git clone --depth 1 https://github.com/scottcanoni/react-anagram-animation.git /tmp/verify
cd /tmp/verify && npm ci && npm run build && npm pack --dry-runExpect 8 files: LICENSE, README.md, package.json, and five in dist/.
2. Bump and tag. npm version writes package.json and creates the tag in
one operation, so the two cannot drift apart:
npm version patch # or minor / major
git push --follow-tags # pushing the tag is what publishes3. Wait about two minutes. release.yml checks the tag matches
package.json, then runs npm publish — which runs prepublishOnly first:
lint, typecheck, tests and build. If any of those fail, nothing is published.
4. Verify. npm view lies immediately after a publish: the registry
processes asynchronously and your local npm cache holds a stale packument. Ask
the registry directly instead:
curl -s https://registry.npmjs.org/react-anagram-animation | grep -o '"latest":"[^"]*"'For the same caching reason a local npm install of the new version may fail
with ETARGET; npm install --prefer-online fixes it. Neither affects anyone
else.
Don't unpublish. Point latest back at the last good version and fix forward:
npm dist-tag add react-anagram-animation@1.5.1 latestExisting installs of the bad version are unaffected; new ones resolve to the old version until you publish a fix.
Already configured. Recorded here in case this repo is ever recreated:
- npm Trusted Publishing — npmjs.com → the package → Settings → Trusted
Publisher → GitHub Actions, with repository
scottcanoni/react-anagram-animation, workflow filenamerelease.yml(that exact string, not a path), no environment, and direct publishing allowed. This is what authenticates CI over OIDC: there is no npm token in this repository, nothing to rotate, and nothing to leak. It also produces the provenance attestation npm shows on the package page. - GitHub Pages — repo Settings → Pages → Source: GitHub Actions.
pages.ymlbuilds the demo with Vite and deploys it on every push tomain. Ignore the Jekyll and Static HTML starter cards; neither runs a build step.
These two packages are deliberately separate, but most of their code is the same and it has drifted badly before. When you change one, check whether the other needs the same change.
Intentionally identical: useFonts.js, randomMinMax in utils.js,
eslint.config.js, tsconfig.json, vite.config.lib.js, and the GitHub
workflows.
Intentionally different — do not "fix" these to match:
react-anagram-animation |
react-text-swap-animation |
|
|---|---|---|
| Positioning | Relative delta (dest − src), letters in normal flow |
Absolute coordinates, letters out of flow |
| Why | Letters never change character, so flow is safe, and deltas are immune to where the element sits | A letter changes character mid-flight; in flow that would reflow every letter after it |
| Hidden words | Offset off-screen with left: -1000px |
Not offset — absolute coordinates need all layers to share an origin |
| Layout | Animation layer is in flow and gives the container its size | Single-cell CSS grid; the measurement words give the container its size |
| Timers per letter | 2 | 4 (two extra for the mid-flight character change) |
WTFPL — see LICENSE.
