Keyboard and screen readers: a bookmark
A bookmark that works with the mouse and the keyboard: Tab to it, press Enter or Space, and it saves, with the same animation, the same state machine. The sample comp is Bookmark (folder Interactive examples).
Loading the player…
Try it: click the bookmark, then press Tab until it is focused and press Enter or Space.
How it works
A <canvas> is one picture to the browser: it takes no keyboard focus and a screen reader cannot tell what it does. The state machine only hears the pointer. So the page adds what the canvas lacks, and hands the keyboard to the machine as a click:
- Make it a button.
tabindex="0"lets it take focus,role="button"andaria-labelsay what it is, andaria-pressedsays whether it is on. - Turn Enter and Space into a click.
stateMachinePostClickEvent(x, y)sends the machine the click a mouse would make, at the bookmark's place, so so the Click onBookmarkinteraction runs, exactly as for the mouse. - Keep
aria-pressedtrue to the file. The machine owns the state (saved); the page reads it when it changes.
Post a click, or set the input? For this bookmark both work: the click only toggles saved, so a <button> around the canvas could call stateMachineSetBooleanInput("saved", …) instead (State machines → Accessibility). Posting the click pays off when a click does several things: the music player's Next sets three inputs and fires an event: the keyboard gets all of it without your page repeating the file's logic.
stateMachinePostClickEvent takes canvas pixels, not CSS pixels: on a screen with a device pixel ratio of 2, the point you see at (80, 80) is (160, 160). Scale by canvas.width / animation width, as below. Don't post an event for a real mouse click: the player already hears those, and the toggle would run twice.
| State | Plays | Leaves on |
|---|---|---|
| off | the first frame of save (the outline) | saved is true |
| on | save once (dip, fill, a small burst, the word Saved rises and fades); holds the filled end | saved is false |
| unsaved | unsave once (the fill drains, Removed shows briefly); holds the outline | saved is true |
Click on Bookmark toggles saved. Entering on and unsaved fires a custom event (saved, removed) for the page: for a toast, or to call your API.
In After Effects
- Markers
save(0–29) andunsave(30–44).saveends on the filled pose,unsaveon the outline thatsavestarts from. - The icon is one layer,
Bookmark, with a transparent rectangle in it so a click anywhere on the icon counts. - Build as
.lottiewith the machine in the table.
On a web page
html
<canvas id="bookmark" width="160" height="160" tabindex="0" role="button" aria-label="Bookmark" aria-pressed="false"></canvas>
<script type="module">
// the version these pages use
import { DotLottie } from "https://cdn.jsdelivr.net/npm/@lottiefiles/dotlottie-web@0.81.0/+esm";
const canvas = document.getElementById("bookmark");
const mark = new DotLottie({ canvas, src: "bookmark.lottie" });
mark.addEventListener("load", () => { mark.stateMachineLoad("bookmark"); mark.stateMachineStart(); });
canvas.addEventListener("keydown", (e) => {
if (e.key !== "Enter" && e.key !== " ") return;
e.preventDefault(); // Space would scroll the page
if (e.repeat) return;
const k = canvas.width / 160; // canvas pixels per animation pixel
mark.stateMachinePostClickEvent(80 * k, 80 * k); // the middle of the Bookmark layer
});
mark.addEventListener("stateMachineBooleanInputValueChange", (e) => {
if (e.inputName === "saved") canvas.setAttribute("aria-pressed", String(e.newValue));
});
</script>Give the canvas a visible focus ring in your CSS (canvas:focus-visible { outline: 2px solid; }): a keyboard user has to see where they are.
Variations
- A like, a star, a follow: any toggle: post the click at that layer.
- Arrow keys for a rating or a slider: set the input directly (
stateMachineSetNumericInput) instead of posting clicks. - Reduced motion: when
matchMedia("(prefers-reduced-motion: reduce)")matches, jump to the end withsetFrameinstead of playing the burst.