Accessibility
Labels, reduced motion and contrast — what views do for you and what to set.
A label
Every view exposes one accessible element with an image role. Its name defaults to the FX Spec's name, else the pattern. Set your own (label on the web, accessibilityLabel in SwiftUI, contentDescription on Android) to say what it means in your app: "Assistant is listening", not "listening". Pass an empty label when the visual is purely decorative and the same information is elsewhere on screen.
Reduced motion
Views follow the system setting: prefers-reduced-motion on the web, Reduce Motion on iOS, Remove animations on Android. With it on, a view draws a single still frame. A connected voice is the exception: the visual still reacts to it, gently (up to 30 redraws a second), because the cue carries information. Override with reducedMotion: "always" or "never".
Colour and contrast
Ink visuals mirror their lightness on dark themes, so they keep contrast on either background. With a fixed brand colour (color.mode: "fixed"), check contrast on both themes yourself. Don't rely on colour alone to carry meaning: pair a state change with a label, or with a pattern that changes shape.
Reduced motion:
import { mount } from "@sinua/web";
const canvas = document.querySelector<HTMLCanvasElement>("#orb")!;
// "auto" (default) follows prefers-reduced-motion: a still pose instead of motion.
// With a voice attached it still redraws (up to 30 Hz): the voice cue is information.
export const fx = mount(canvas, { pattern: "working", reducedMotion: "auto" });
// "always" / "never" override the system setting, e.g. from an in-app preference:
fx.update({ reducedMotion: "always" });
The accessibility label:
import { mount } from "@sinua/web";
const canvas = document.querySelector<HTMLCanvasElement>("#orb")!;
// The canvas gets role="img" and this name. Default: the spec's `name`, else the pattern.
export const fx = mount(canvas, { pattern: "listening", label: "Assistant is listening" });
// Purely decorative (text next to it already says it): an empty label hides it.
fx.update({ label: "" });