Tiny Tells
Cast Color In the Wild Install
Docs
Preview with
Core Play
v0.1.1

Tiny Tells Docs

Tiny Tells is one custom element, <tiny-tell>, that shows what your app is up to with a small animated character. Pick one of six tells with the skin attribute and one of five states with the state attribute. Change the state and the tell springs into the new one.

Start

Get Started Play Bundle

Use

Tells States Accessibility

Customize

Size Color Styling

Integrate

Frameworks and Editors Testing Your App Before It Loads

Reference

API Browser Support How It Works

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.

Play bundle npm CDN Self-Host

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>
Keep the version in the URL until 1.0. That way a breaking change can’t reach your site until you choose to update.

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.

Syncing 3 files…

<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.

Syncing 3 files…

Run It
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

Move your pointer between them, or tap the play one five times. Or Press This

Load it in place of the core bundle:

npm CDN Self-Host
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>
From a CDN, load one bundle, not both. The two CDN files each carry the whole element, so if both load, the first one wins and the console says so. From npm, importing both is fine, since they share one element.

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:

Waiting for your approval to deploy.

Deploy queued.

<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

  1. Put words beside it. A tell is never the only signal.
  2. Hide its name when the words already say it with label="". It works like alt="" on a decorative image. Try it.
  3. 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.
  4. If you change the colors, keep 3:1 against the surface. Check yours.
  5. 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.

    paused
    Moving for more than five seconds? Add a pause. If tells on your page keep moving that long, WCAG asks you to give people a way to stop them.

    A button that pauses every tell on the page:

    Pause Animations
    <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:

    Draft 1,204 words Saved
    .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);
    }
    Set the tokens where the theme switches. A 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.

    Syncing

    Default: state colors

    Syncing

    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 tell CSS 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, like tiny-tell[state="error"]. A tell with no state attribute is working, so match that one with tiny-tell:not([state]) too.

    Upload failed.

    Uploading…

    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.ts types the element and exports TinyTell, TinyTellAttributes, TellSkin, TellState, TellColor, and TellScheme. The play bundle’s types add the dance events to addEventListener.
    • Custom Elements Manifest: dist/custom-elements.json describes 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.customData setting 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:

    npm CDN Self-Host
    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:

    Without the script • working ✓ done ✕ error ! warning
    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.

    Hold Still 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
    Two extras need newer browsers. :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.

    1. 1
    2. 2
    3. 3
    4. 4
    All six tells, one clock. The bar steps once a beat. Start Over

    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

    Switch mid-move and watch the right one.

    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.

    At 32px and under, five of the six switch to a simpler drawing. Flipdot’s grid already reads small.

    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:

    Held still for now. Run All 100

    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

    Tiny Tells Rev 0.1.1 MIT Made by @talbs with

    GitHub npm Docs Issues