Start
Get Started
Three steps: load it, add a tell, and change its state.
1. Load It
Tiny Tells loads three ways, and each one defines the same <tiny-tell> element, so pick
whichever matches how you build your site. Turn on Play bundle if you want the version
where tells glance at the pointer and dance.
If your app uses a bundler, install the package and import it once, anywhere in your code:
npm i tiny-tells
import 'tiny-tells';
The import registers the element as soon as it runs, and the package ships with TypeScript types. If you
need the class itself, import it by name with
import { TinyTell } from 'tiny-tells'.
If you’d rather skip the build step, add this script tag to your page:
<script type="module" src="https://cdn.jsdelivr.net/npm/tiny-tells@0.1.1/dist/tiny-tells.min.js"></script>
To serve the file yourself, copy dist/tiny-tells.min.js from the package into
your site and load it like any other module. The package also includes
dist/tiny-tells.js, the same code unminified, if you want to read or debug it:
<script type="module" src="/js/tiny-tells.min.js"></script>
2. Add a Tell
Put it next to the words it describes. label="" keeps screen readers from saying the state twice,
and role="status" announces the words when they change.
More on accessibility.
<p>
<tiny-tell skin="dekatron" state="working" label=""></tiny-tell>
<span role="status">Syncing 3 files…</span>
</p>
3. Change Its State
Set state from your code and update the words with it. The tell springs into the new state.
const tell = document.querySelector('tiny-tell');
const words = document.querySelector('[role="status"]');
tell.state = 'done';
words.textContent = 'Synced 3 files.';
Play
Play is an optional second bundle, tiny-tells/play. It’s the same element with the same states,
colors, and API. It adds two things that only happen when someone’s there to see them: a tell glances toward a
nearby pointer, and five quick taps make it dance.
The core bundle is for status and nothing else. Play is for when you want the tells to feel alive. It costs
under 1 KB more, gzipped. Reduced motion and paused turn it off.
Core: keeps to itself
Play: looks your way
Load it in place of the core bundle:
import 'tiny-tells/play';
<script type="module" src="https://cdn.jsdelivr.net/npm/tiny-tells@0.1.1/dist/tiny-tells-play.min.js"></script>
<script type="module" src="/js/tiny-tells-play.min.js"></script>
Use
Tells
Pick one with the skin attribute. An unknown name logs a warning and falls back to Drones.
| Tell | skin |
Who It Is |
|---|
States
Set one with the state attribute. An unknown name logs a warning and falls back to working.
| Tell | state |
Means | Use It |
|---|
Warning means a person has to act, while idle means nobody’s waiting. Whatever the state, pair the tell with
words, and put them in a
role="status" region when the state matters, so screen readers announce the change:
<p>
<tiny-tell state="warning" label=""></tiny-tell>
<span role="status">Waiting for your approval to deploy.</span>
</p>
<p>
<tiny-tell state="idle" label=""></tiny-tell>
<span role="status">Deploy queued.</span>
</p>
Accessibility
A tell is a hint, never the message. The words beside it carry the status, and the tell adds something you can catch at a glance. So Tiny Tells handles everything it can on its own (motion, contrast, high contrast, flashing) and leaves you the one thing it can’t know: what’s happening, in words.
Your Part
- Put words beside it. A tell is never the only signal.
-
Hide its name when the words already say it with
label="". It works likealt=""on a decorative image. Try it. -
If the status changes while someone’s on the page, put the words in a
role="status"region so screen readers announce it. See how. - If you change the colors, keep 3:1 against the surface. Check yours.
- If it moves for more than five seconds, give people a pause button. Copy one.
Handled for You
Labels
Screen readers announce a tell as an image named after its state, like “Working”. Set label to
say something more specific, or set label="" to hide the tell from screen readers when the words
beside it already say the state.
| Tell | Markup | Screen readers hear |
|---|
<tiny-tell state="working"></tiny-tell>
<tiny-tell state="working" label="Uploading photos"></tiny-tell>
<tiny-tell state="working" label=""></tiny-tell>
Pausing
Set the paused attribute to hold a tell on its current frame.
A button that pauses every tell on the page:
<button type="button" id="pause-tells" aria-pressed="false">Pause animations</button>
<script>
document.getElementById('pause-tells').addEventListener('click', event => {
const isPaused = event.currentTarget.getAttribute('aria-pressed') !== 'true';
event.currentTarget.setAttribute('aria-pressed', isPaused);
document.querySelectorAll('tiny-tell').forEach(tell => (tell.paused = isPaused));
});
</script>
Customize
Size
You usually don’t set one. A tell sizes itself to the text beside it, about 1.4 times the font size, and stays between 20 and 48px.
Follow the Text
Leave the size alone and the tell matches whatever text it sits in. Under the hood, that’s
clamp(20px, 1.43em, 48px).
Syncing your files
Syncing your files
Syncing your files
<p>Syncing your files <tiny-tell></tiny-tell></p>
Pin a Size
Set --tell-size when the tell should stay one size no matter the text, like in a toolbar. It
inherits, so set it on the tell or on any container:
.toolbar { --tell-size: 1.5rem; }
Pick a Value
Any CSS length works. Tells are drawn for 20 to 48px and look their best in that range:
tiny-tell { --tell-size: 2rem; }
Color
Set the palette with inherited custom properties, on :root or any container. Any CSS color works,
including light-dark(). Tokens you don’t set keep the built-in palette. Keep 3:1 contrast against
the surface the tell sits on. The built-in palette clears it in light and dark, and the chips below check
yours.
Here’s the CSS for the palette you picked:
Using Another System’s Colors
Declare the tokens on the same selectors your system uses to switch theme. The names below are stand-ins for your system’s own:
data-theme="light"
data-theme="dark"
:root,
[data-theme] {
--tell-color-working: var(--color-accent);
--tell-color-done: var(--color-success);
--tell-color-error: var(--color-danger);
--tell-color-warning: var(--color-warning);
--tell-color-idle: var(--color-neutral);
}
var() resolves where the token is declared, so a token set only on :root keeps
its light value inside a dark region.
Match the Text Color
Set color="current" and the tell draws in the text color around it instead of the state colors,
even inside another component’s slot. Reach for it on a filled button or a badge, where state colors would
clash.
Default: state colors
color="current": follows the text
<tiny-tell></tiny-tell>
<tiny-tell color="current"></tiny-tell>
Match Light or Dark
A tell draws for light or dark surfaces. The scheme attribute picks which: auto (the
default) reads the color-scheme around it, light and dark force one,
and invert flips it for a tell on a badge or a filled button.
<tiny-tell scheme="auto"></tiny-tell>
<tiny-tell scheme="light"></tiny-tell>
<tiny-tell scheme="dark"></tiny-tell>
<tiny-tell scheme="invert"></tiny-tell>
Styling
- The
tellCSS part,::part(tell), is the canvas. -
The custom states
:state(working),:state(done), and so on match the current state. They need Chrome 125, Firefox 126, or Safari 17.4. For older browsers, match the attribute instead, liketiny-tell[state="error"]. A tell with nostateattribute isworking, so match that one withtiny-tell:not([state])too.
tiny-tell:state(error) + .message { color: var(--danger); }
Default: full strength
.muted: steps back
.muted tiny-tell::part(tell) { opacity: 0.6; }
Integrate
Frameworks and Editors
A tell is a plain custom element, so any framework that renders HTML can render one. The package also ships files that describe it to your editor and tools.
-
TypeScript:
dist/tiny-tells.d.tstypes the element and exportsTinyTell,TinyTellAttributes,TellSkin,TellState,TellColor, andTellScheme. The play bundle’s types add the dance events toaddEventListener. -
Custom Elements Manifest:
dist/custom-elements.jsondescribes the element for tools that read the manifest, like Storybook, docs generators, and agents. The package points to it, so most tools find it on their own. -
VS Code: add the custom data file to the
html.customDatasetting to get attribute autocomplete in HTML files. On the CDN or self-hosting, download the same file into your project and point the setting at it.
{
"html.customData": ["./node_modules/tiny-tells/dist/vscode.html-custom-data.json"]
}
React
React 19 handles boolean attributes, so paused={false} leaves the tell running. React 18 writes
every value as an attribute string instead, which turns paused={false} into
paused="false" and pauses the tell anyway. In React 18, leave paused out rather than
setting it to false.
With TypeScript, add this to any .d.ts file so <tiny-tell> type-checks in JSX.
It works with the types for React 18 and 19:
import type { DetailedHTMLProps, HTMLAttributes } from 'react';
import type { TinyTell, TinyTellAttributes } from 'tiny-tells';
declare module 'react' {
namespace JSX {
interface IntrinsicElements {
'tiny-tell': DetailedHTMLProps<HTMLAttributes<TinyTell>, TinyTell> & TinyTellAttributes;
}
}
}
Testing Your App
- Test DOMs (jsdom, happy-dom): tells render inert. Attributes and properties work, nothing draws, and nothing logs, so Jest and Vitest runs stay clean.
- Screenshot tests: emulate reduced motion. Every state then holds one fixed pose, so frames match run to run.
await page.emulateMedia({ reducedMotion: 'reduce' });
Before It Loads
Tiny Tells needs JavaScript, and until the script runs, <tiny-tell> is an empty inline
element. When it loads, the tell appears and can push the words beside it over. The words carry the status, so
a late tell only costs some charm. The shift is the part worth fixing.
Hold Its Space
Load reserve.css before the script. It gives every tell the exact box it will have once it loads,
so nothing moves when it arrives:
import 'tiny-tells/reserve.css';
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tiny-tells@0.1.1/dist/reserve.css">
<link rel="stylesheet" href="/css/reserve.css">
Show a Stand-In
If the script might never arrive, draw something in the reserved space. The state attribute is
already there, so plain CSS can follow it:
tiny-tell:not(:defined) { display: inline-grid; place-items: center; }
tiny-tell:not(:defined)::before { content: "•"; }
tiny-tell:not(:defined)[state="done"]::before { content: "✓"; }
tiny-tell:not(:defined)[state="error"]::before { content: "✕"; }
tiny-tell:not(:defined)[state="warning"]::before { content: "!"; }
Anything you put inside the tag also shows until the element loads, then hides. With
reserve.css it’s clipped to the tell’s box, so keep it to a short word or an icon.
To check from script whether it loaded: await customElements.whenDefined('tiny-tell').
Reference
API
Every attribute is also a property (el.state = 'error'), and unknown values fall back to their
defaults. An unknown skin or state also logs a warning.
| Attribute | Values | Default |
|---|
Events
The core fires no events, since it only shows the state you set. The play bundle fires two around a dance, because a dance is the one change your code doesn’t make. Both bubble and cross shadow roots, so you can listen on the tell or on any element above it.
| Event | Cancelable | Fires |
|---|
A dance lasts two loops, about 6.4 seconds. tell-dance doesn’t fire while a tell is paused, while
reduced motion is on, or while the tell is already dancing. Pausing mid-dance holds it, and
tell-after-dance fires once it finishes after you unpause. A canceled dance never started, so it
gets no tell-after-dance.
Tap it five times, or press Dance.
const tell = document.querySelector('tiny-tell');
const status = document.querySelector('#status');
const holdStill = document.querySelector('#hold-still');
tell.addEventListener('tell-dance', event => {
if (holdStill.checked) {
event.preventDefault();
status.textContent = 'Asked to dance, and held still.';
} else {
status.textContent = 'Dancing…';
}
});
tell.addEventListener('tell-after-dance', event => {
status.textContent = `Back to ${event.target.state}.`;
});
Statics
| Static | Type | Does |
|---|
Custom tells aren’t supported yet, because the internals will keep changing.
Browser Support
Tiny Tells works in these browsers and later:
-
Chrome 99 -
Firefox 112 -
Safari 16.4
:state() selectors, and scheme="auto" following a container’s
color-scheme, need Chrome 125, Firefox 126, or Safari 17.5. Older browsers skip
:state() and follow the system’s light or dark setting instead.
How It Works
Each tell is animated like a character rather than swapped like an icon, and all six follow the same few rules.
One Beat, One Clock
Everything runs on a 0.8s beat. Each loop is four beats, and idle takes six. Every tell on a page shares one clock, so a row of them stays in time, and each one lands its hardest moves on the beat.
Handoffs
When the state changes, the tell doesn’t cut to the next loop. It springs from its current pose into the new one and keeps its speed, so switching halfway through a move still looks deliberate.
Cut: starts the new loop cold
Handoff: springs from where it is
Small on Purpose
Tells drop detail as they shrink. Eyes loses its glow at 32px and under, and Dekatron’s ten cathodes merge into one ring, so each one still reads at 20px.
Drawing
Each tell is one canvas, drawn at your screen’s full resolution. Off-screen tells stop drawing. On a recent laptop, 100 on screen run well past 60 fps, and a CPU four times slower still gets about 40. Measure it on yours:
Tested
Every change runs in Chromium, Firefox, and WebKit. Each tell has to:
- stay inside its live area
- loop without a seam
- never flash more than three times a second
- draw in system colors under forced colors