Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 0 additions & 77 deletions .eslintrc.js

This file was deleted.

6 changes: 5 additions & 1 deletion .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ jobs:

strategy:
matrix:
node-version: [ 18.x, 20.x, 22.x ]
# Matches the engines field. Node 18 reached end of life in April 2025.
node-version: [ 20.x, 22.x, 24.x ]

steps:
- uses: actions/checkout@v4
Expand All @@ -24,4 +25,7 @@ jobs:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm test
- run: npm run build
37 changes: 37 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Deploy demo to GitHub Pages

on:
push:
branches: [ main ]
workflow_dispatch:

# Allow one concurrent deployment, and let a new one supersede a queued one.
concurrency:
group: pages
cancel-in-progress: true

permissions:
contents: read
pages: write
id-token: write

jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22.x
cache: 'npm'
- run: npm ci
- run: npm run build-demo
- uses: actions/configure-pages@v5
- uses: actions/upload-pages-artifact@v3
with:
path: demo-build
- id: deployment
uses: actions/deploy-pages@v4
57 changes: 57 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Publish to npm

on:
# Merging to main never publishes. Pushing a v* tag does, and `npm version`
# is what creates those tags. Releasing stays a separate, deliberate act.
push:
tags: [ 'v*' ]
workflow_dispatch:

# Publishing is irreversible, so never let two runs race each other.
concurrency:
group: publish
cancel-in-progress: false

permissions:
contents: read
# Required for npm Trusted Publishing (OIDC). This is what authenticates to
# npm: there is no token stored anywhere in this repository.
id-token: write

jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22.x
cache: 'npm'
registry-url: 'https://registry.npmjs.org'

# `npm version` writes package.json and cuts the tag in one operation, so
# these cannot disagree. If they do, the tag was made by hand: stop,
# rather than publish a version nobody meant to ship.
- name: Tag must match package.json
if: startsWith(github.ref, 'refs/tags/v')
run: |
TAG="${GITHUB_REF_NAME#v}"
PKG=$(node -p "require('./package.json').version")

if [ "$TAG" != "$PKG" ]; then
echo "::error::Tag v$TAG does not match package.json version $PKG. Nothing published."
exit 1
fi

echo "Publishing $(node -p "require('./package.json').name")@$PKG" >> "$GITHUB_STEP_SUMMARY"

# Node 22 ships npm 10.x; Trusted Publishing needs npm >= 11.5.1.
- name: Use an npm that supports Trusted Publishing
run: npm install -g npm@latest

- run: npm ci

# prepublishOnly runs lint, typecheck, test and build, so a broken build
# cannot reach the registry. --provenance is implied by Trusted
# Publishing; it is spelled out here to keep the intent obvious.
- run: npm publish --provenance --access public
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,12 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# library build output
/dist

# local editor/agent scratch
.claude

# packed tarballs
*.tgz
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fund=false
58 changes: 58 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Changelog

## 2.0.0

A correctness and packaging release. The component worked in the author's demo and was broken or unusable almost everywhere else: it doubled its own animation under React StrictMode, threw on import in any SSR context, rendered invisible white text by default, positioned letters using the wrong coordinate space, and never animated the fade that is its whole reason to exist.

### Breaking

1. **Letters are positioned correctly now, so the animation looks different.** v1 assigned `getBoundingClientRect().x` — a *viewport*-relative coordinate — to the `left` of an absolutely positioned element whose containing block was the component itself. Any instance not flush against the left edge of the window rendered its letters offset by its own inset, and everything drifted when the page scrolled horizontally. It now uses `offsetLeft`/`offsetTop`, which are already relative to the right box.
2. **Vertical movement works.** `top` was never assigned, even though the transition string listed it and `offsetTop` was measured and then thrown away. Wrapped phrases collapsed onto a single line. They now animate between lines.
3. **The component has a real width and height.** All three layers were absolutely positioned, so the element collapsed to zero height and overlapped whatever came after it. The layers are now stacked in a single CSS grid cell sized by the longer phrase.
4. **Appearing and disappearing letters actually fade.** `opacity` was set but was not in the transition list, so they popped instantly.
5. **A letter changes character at `transitionDuration / 2`** instead of a hardcoded 500ms. With the documented `transitionDuration: 4000`, letters used to change identity 3.5 seconds before arriving.
6. **The stylesheet is gone.** The package ships and imports no CSS. The cosmetics it used to force on every consumer — `color: #fff`, `text-transform: uppercase`, `width/height: 100%` — are yours to set. See *Migrating from v1* in the README for a copy-paste block.
7. **Class names are namespaced.** `.word` → `.text-swap-word`, `.letter` → `.text-swap-letter`, `.word-animation` → `.text-swap-word-animation`. `.hidden` is gone; it collided with Tailwind's `.hidden { display: none }`, which would have silently zeroed every measurement.
8. **Deep imports no longer resolve**, and **`main` moved** from `dist/index.js` to `dist/index.cjs`.
9. **Browser floor raised** from roughly Chrome 67 / Safari 11.1 to Chrome 80 / Firefox 74 / Safari 13.1 / Edge 80.
10. **`prefers-reduced-motion: reduce` is now respected**, with no opt-out.
11. **`engines` now requires Node >= 18.**

### Fixed

- **Animation no longer doubles under React 18/19 StrictMode.** The effect installed a self-recursive `setTimeout` chain with no cleanup, so StrictMode's double invocation left two permanently out-of-phase loops. The repo's own demo is wrapped in StrictMode, so this was visible in development the whole time.
- **The animation loop no longer outlives the component**, and changing props no longer stacks another loop on top. This package schedules four timers per letter per cycle, so the leak accumulated twice as fast as its sibling's.
- **The component no longer restarts on every single render.** `Loader` built a fresh `words` array literal on each render and the child depended on it, so the effect tore down and restarted unconditionally — and consumer memoisation could not help. The child now takes two string props.
- **Passing a longer phrase no longer crashes.** Per-letter refs were created once at mount, so a later, longer `words` threw `Cannot read properties of undefined (reading 'current')`.
- **Works under SSR and React Server Components.** The built output used to `require('./index.css')` from CommonJS, which throws in Node. It now renders the first phrase as real text on the server.
- **Letters no longer pop in a frame late.** Measurement moved to `useLayoutEffect`.
- **Positions are re-measured on resize**, orientation change, or a late webfont swap.
- **A consumer `className` no longer replaces the component's own**, which used to break every documented styling hook. `className` is merged and `style` is combined with the structural styles.
- **`fontToObserve` fixes:** the hook re-ran its effect on every render; omitting the prop called `document.fonts.load('16px "undefined"')`; a rejected font load left the component rendering `null` forever with no explanation.
- **Unpairable input no longer throws** and take down the consumer's React tree; it logs once and renders the first phrase.
- **Screen readers read the phrase once**, not three times.
- Two comments that described the opposite of what the code did, and a `findIndex` parameter named `srcIndex` that was actually a destination index.

### Added

- **TypeScript types**, hand-written and shipped in the package.
- **ESM build** (`dist/index.mjs`) alongside CommonJS, with an `exports` map, `sideEffects: false` and sourcemaps.
- **`DEFAULT_ANIMATION_OPTIONS`** is now exported from the entry point.
- **Rest props are forwarded** to the root element.
- **A test suite** of 19 tests. Every fix in this release was mutation-tested: each one was individually reverted and a test caught it.
- **CI that actually enforces things** — lint, typecheck, test and build on Node 20, 22 and 24. Previously CI ran only `npm run build`, and the linter had never run.
- **A LICENSE file.** `package.json` declared WTFPL but no license text was published.
- **A permanent hosted demo** on GitHub Pages, replacing a hand-maintained CodeSandbox link.
- **npm provenance** on publish.
- **A "keeping in sync" section** in the README documenting which files are meant to match `react-anagram-animation` and which are deliberately different.

### Removed

- **`core-js`** — the only runtime dependency, and unnecessary. **The package now has zero runtime dependencies.**
- **Babel**, `sass`, `cross-env` and `rimraf` from the toolchain. The library is built by Vite.
- A fake `uuidv4` that was used only for React keys.
- Dead `rect` measurements that were captured and never read, and dead files in the published tarball.

## 1.5.1 and earlier

See the [commit history](https://github.com/scottcanoni/react-text-swap-animation/commits/main).
16 changes: 16 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004

Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>

Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.

DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

0. You just DO WHAT THE FUCK YOU WANT TO.


Copyright (C) 2021 Scott Canoni
Loading
Loading