Skip to content

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…
Live · dotlottie-webDownload .lottie

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:

  1. Make it a button. tabindex="0" lets it take focus, role="button" and aria-label say what it is, and aria-pressed says whether it is on.
  2. 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 on Bookmark interaction runs, exactly as for the mouse.
  3. Keep aria-pressed true 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.

StatePlaysLeaves on
offthe first frame of save (the outline)saved is true
onsave once (dip, fill, a small burst, the word Saved rises and fades); holds the filled endsaved is false
unsavedunsave once (the fill drains, Removed shows briefly); holds the outlinesaved 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 ​

  1. Markers save (0–29) and unsave (30–44). save ends on the filled pose, unsave on the outline that save starts from.
  2. The icon is one layer, Bookmark, with a transparent rectangle in it so a click anywhere on the icon counts.
  3. Build as .lottie with 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 with setFrame instead of playing the burst.

AXO for After Effects