baffle-ts
v1.0.0
Published
A tiny dependency-free TypeScript library for obfuscating and revealing DOM text. A modern TypeScript successor to the archived baffle.js.
Maintainers
Readme
baffle-ts targets DOM text, obfuscates it with configurable characters, and reveals it over time. It is dependency-free, framework-agnostic, typed, and ships ESM, CommonJS, UMD, minified UMD, and TypeScript declarations.
import { baffle } from 'baffle-ts';
const title = baffle('.headline', {
characters: 'abcdefghijklmnopqrstuvwxyz0123456789',
speed: 45,
});
title.start();
await title.reveal(900);- Dependency-free runtime - no framework or utility dependencies.
- Tiny bundle - Bundlewatch checks every JavaScript output.
- Framework-agnostic - pass a selector,
Element,NodeList, or any element collection. - Typed API - TypeScript declarations are included.
- Multiple builds - ESM, CommonJS, UMD, and minified UMD.
Credit
baffle-ts is a modern TypeScript rewrite inspired by baffle.js by Cam Wiegert, which has been archived since 2020. The original is MIT licensed and its copyright notice is retained in LICENSE.
This is a from-scratch implementation, not a fork. It keeps the shape of baffle's API because that API was good, while modernising the internals and the build. See Differences from baffle.js before migrating.
Getting Started
Installation
npm install baffle-tsLive demo
The demo is hosted on GitHub Pages:
andreasnicolaou.github.io/baffle-ts
You can also serve docs/ with any static file server and open docs/index.html. The demo loads baffle-ts from the CDN, so no local build is required.
Browser Usage
<h1 data-headline>baffle-ts</h1>
<script src="https://unpkg.com/baffle-ts@latest/dist/index.umd.min.js"></script>
<script>
const title = Baffle.baffle('[data-headline]');
title.start();
title.reveal(900);
</script>API
const instance = baffle(target, options);target can be a CSS selector, a single Element, or an iterable/array-like collection of elements.
baffle('.headline');
baffle(document.querySelector('.headline')!);
baffle(document.querySelectorAll('.headline'));baffle() is the convenience factory and returns a Baffle instance. You can also construct the class directly when that better fits your code:
import { Baffle } from 'baffle-ts';
const instance = new Baffle('.headline', { speed: 45 });Framework integration
baffle-ts works with React, Vue, Angular, Svelte, and other browser frameworks because it operates on real DOM elements rather than framework-specific components. Create an instance only after the element has mounted, and call destroy() when the owning component unmounts.
For server-side rendering, initialize baffle-ts on the client. Selector targets require document; passing an existing Element is safe in environments where a DOM element is available.
The library updates textContent directly, so let it own the animated element's text while an effect is active. Avoid rendering competing text into that same element from the framework until the effect has finished or been destroyed.
Options
characters: string or array of characters used while text is obfuscated.exclude: string or array of characters that should not be replaced.speed: minimum milliseconds between obfuscation frames.random: custom random number function, useful for deterministic tests.respectReducedMotion: honourprefers-reduced-motion. Defaults totrue.
Instance Methods
start(): repeatedly obfuscates unrevealed text.stop(): stops active animation without revealing.once(): performs one obfuscation frame.reveal(duration?, delay?): reveals text over time and resolves with the instance when the reveal finishes or is stopped.set(options): updates animation options, including mid-animation.text(valueOrResolver): replaces the managed text.refresh(): reads the current DOM text back into the instance.destroy(): stops animation and restores the managed text.
Waiting for a reveal
reveal() returns a real promise, so code can wait for the reveal to settle:
// Configure with the usual chain, then reveal
const title = baffle('.headline')
.start()
.set({ speed: 100 })
.text(() => 'Hi dad!');
// Await completion before starting the next action
await title.reveal(900);
showTheNextThing();
// It is a genuine promise, so it composes
await Promise.all([title.reveal(400), subtitle.reveal(600)]);Calling stop(), start(), or a new reveal interrupts an active reveal() call and resolves its promise with the instance.
Accessibility
When the user has prefers-reduced-motion: reduce set, baffle-ts skips obfuscation entirely and leaves text readable: start() and once() render the real text, and reveal() restores it and resolves immediately. Scrambled-but-static text would be less readable than the animation it replaces, so nothing is obfuscated at all.
Opt out with respectReducedMotion: false. Outside a browser, or where matchMedia is unavailable, animation runs normally.
Rendering
The animation loop is driven by requestAnimationFrame, so it stays in step with the display, avoids interval drift, and pauses automatically in background tabs. speed throttles how often characters churn. Where frames are unavailable, it falls back to setTimeout.
Differences from baffle.js
All six baffle.js methods keep their names and argument shapes, so most code moves across unchanged. The remaining differences are defaults and additions:
| | baffle.js 0.3.6 | baffle-ts |
| --------------------------- | ----------------------------------- | -------------------------------- |
| reveal() default duration | 0 | 600 |
| reveal() return value | instance | Promise<Baffle> |
| Default characters | Aa…Zz~!@#$%^&*()-+=[]{}\|;:,./<>? | A–Z a–z 0–9 #%&*+-=?@ |
| Default exclude | [' '] | ' \t\n\r' |
| exclude semantics | replaces the default | appends to the default |
| text() argument | function only | string or function |
| text() resolver signature | (currentText) | (currentText, element, index) |
| Animation loop | setInterval | requestAnimationFrame |
| Reduced motion | not handled | obfuscation skipped by default |
| Extra methods | — | refresh(), destroy() |
| Extra options | — | random, respectReducedMotion |
Migrating from baffle.js
Most method calls move across unchanged. reveal() now returns a promise, so retain the instance when later code needs to configure or stop it. The other changes that alter behaviour are the defaults:
// Restore baffle.js's character set and instant reveal
const b = baffle('.headline', {
characters: 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz~!@#$%^&*()-+=[]{}|;:,./<>?',
});
await b.reveal(0);Whitespace beyond the space character (\t, \n, \r) is excluded by default, and anything you pass to exclude is added to that set rather than replacing it.
Users with prefers-reduced-motion: reduce will see no obfuscation at all. If you need the previous unconditional behaviour, pass respectReducedMotion: false.
Development
npm install
npm run check
npm run test:coverageThe check script runs ESLint, Prettier validation, Vitest, the Rollup build, and Bundlewatch. test:coverage runs the same test suite with V8 coverage enabled and enforces 100% statements, lines, and functions, plus at least 95% branch coverage.
License
baffle-ts is licensed under the MIT License (c) Andreas Nicolaou, incorporating the MIT-licensed copyright notice of baffle.js (c) Cam Wiegert.
