CharacterMark
A single animated character. Everything else in the library is built on it.
Paint styles
styleId picks the style and variant picks a variant within it. The geometry, the animation and the face are identical across all of them — only the paint changes, so a new state or a new character arrives in every style at once. Each style has a page: Vector, Pixel, Kaomoji. Here is every vector variant:
solid is the default: the body is a flat filled mass and the cute face is white.
outline and glass turn that around. When the body is no longer filled, a white face would vanish, so these two variants draw the face in the ink instead — the same silhouette read from the other side.
The stroke is a constant width in shape space rather than in pixels, so it scales with the character. A stroke pinned to device pixels looks spindly on a large character and swamps a small one.
Faces
face chooses among three faces: the default cute, and eyes and the line face line, which adds a nose and a mouth as strokes at one shared weight. nose only applies to line and picks one of six; omit it for one derived from seed, or pass null for none.
Both props can change on a mounted character: the face eases between styles and the nose crossfades. The root element carries data-face for styling.
When to use it
Use CharacterMark for a character that is on its own — a hero, an empty state, a single avatar in a header. For several characters at once, prefer MarkStageProvider, which shares one animation between every character that looks the same.
Following the pointer
The mapping is not linear. Distance is passed through a square root, which keeps the eyes responsive to small movements near the character while still reaching their limit for anything far away — a linear mapping makes nearby motion feel unresponsive.
Emphasis
Pausing
paused stops the character's subscription to the shared ticker and holds the current frame. It does not reset to a neutral pose, so a character scrolled out of view and back looks like it was waiting rather than like it restarted.
Imperative actions
burst() is suppressed under prefers-reduced-motion, since particles radiating from a centre point are a vestibular trigger.
Following a point the host already knows
followPointer listens for the pointer itself. followTarget takes a point in viewport pixels instead — another element's centre, or a pointer position the host is already tracking for its own reasons — and the engine converts it with the same square-root mapping every frame. gazeTarget wins when both are set.
Ink, eyes and line weight
ink overrides the body paint with a flat colour or a two-stop gradient, for a character that has to match a brand swatch rather than the palette; null hands it back to the theme token. eyeScale is a static multiplier on eye size, applied on top of the emphasis spring. lineWeight scales the nose, the mouth and the HUD dials together, within LINE_WEIGHT_RANGE (0.8–1.25) — one pen per character, so the face and the HUD stay one stroke.
HUD
hud draws a dial in the ring outside the body; null removes it. While a HUD is on, the face is hidden — the body stays a silhouette, the ring is the reading. The spec is the same HudSpec the protocol carries and HudPlate validates, and the dial is stroked at the character's lineWeight.
The spec is compared by value, not identity, so hud={{ kind: 'progress', value }} written inline does not restart the dial on every render.
Knowing when a state has settled
Finite states — spawning, waking — run their course (see STATE_SETTLE_TO) and the engine moves on by itself. onSettle(from, to) fires at that moment, so a host's own idea of the state can follow without keeping a duplicate timer.
The opposite problem — an input that changes faster than the character can follow — is what useMarkSettle is for.
Why it never re-renders
React mounts an empty host element and the engine takes over. Prop changes are pushed into the controller imperatively rather than re-rendering:
Going through the reconciler sixty times a second to set a dozen attributes would cost considerably more than the animation itself. The mount effect deliberately has no dependencies — recreating the controller on a prop change would restart the animation.
API
| Prop | Type | Default | Description |
|---|---|---|---|
antics | boolean | false | Let an unattended character occasionally hop or spin on its own. Default false. |
autoPause | boolean | AutoPauseOptions | false | Stop animating while hidden, scrolled out of view, or under reduced motion. Read at mount. Off by default. |
badgeColor | string | 'var(--aora-accent)' | Corner badge colour, shown in the `notifying` state. |
children | string | number | bigint | boolean | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<AwaitedReactNode> | null | — | — |
className | string | — | — |
color | "black" | "brown" | "red" | "orange" | "yellow" | "green" | "cyan" | "blue" | "violet" | "magenta" | "pink" | "gray" | — | Ink colour from the palette. |
emotionId | string | — | Protocol id, for hosts that speak the wire format. Ignored when `state` is given. |
emphasis | boolean | false | Enlarge and converge the eyes, for hover states. |
entryEffects | boolean | true | Play the one-off flourish some states define on entry. Default true. |
eyeColor | string | — | Colour of the eye cut-outs. Defaults to the surrounding background token. |
eyeScale | number | — | Static multiplier on eye size, on top of the emphasis spring. |
face | "cute" | "eyes" | "line" | — | Face style: `cute` (the default), the plain `eyes`, or `line` — eyes plus a stroked nose and mouth. |
faceTune | Partial<FaceTune> | — | Multipliers on the fitted face box: overall size, eye gap, height, eye width and height. |
followPointer | boolean | false | Follow the pointer automatically. |
followTarget | { x: number; y: number; } | null | null | Eyes track this point, in viewport pixels — another element's centre, or a pointer position the host is already tracking for its own reasons. `gazeTarget` wins when both are set. |
gazeTarget | { x: number; y: number; } | null | null | Eyes track this point. Components in [-1, 1] relative to the centre. |
hud | HudSpec | null | null | A dial drawn in the ring outside the body. `null` removes it. |
ink | InkPaint | null | null | Override the body paint: a flat colour or a two-stop gradient. `null` returns it to the theme token. |
lineWeight | number | — | Multiplier on the shared line weight, 0.8–1.25. Applies to the nose, the mouth and the HUD dials together, so one character can be drawn with a heavier pen without its features disagreeing with each other. |
nose | "slant" | "angular" | "hook" | "long" | "curve" | "bow" | null | — | Nose for the line face. Defaults to one derived from the seed; `null` draws none. |
onSettle | ((from: "idle" | "sleeping" | "drowsy" | "bored" | "waking" | "powering-down" | "listening" | "thinking" | "searching" | "reading" | "writing" | "working" | "loading" | "progress" | "radar" | "orbit" | "dictating" | "uploading" | "sending" | "receiving" | "messaging" | "waiting" | "spawning" | "composing" | "dragging" | "happy" | "excited" | "laughing" | "playful" | "proud" | "celebrate" | "bouncing" | "shy" | "sad" | "angry" | "confused" | "suspicious" | "surprised" | "scared" | "curious" | "humming" | "alerting" | "notifying" | "refusing", to: "idle" | "sleeping" | "drowsy" | "bored" | "waking" | "powering-down" | "listening" | "thinking" | "searching" | "reading" | "writing" | "working" | "loading" | "progress" | "radar" | "orbit" | "dictating" | "uploading" | "sending" | "receiving" | "messaging" | "waiting" | "spawning" | "composing" | "dragging" | "happy" | "excited" | "laughing" | "playful" | "proud" | "celebrate" | "bouncing" | "shy" | "sad" | "angry" | "confused" | "suspicious" | "surprised" | "scared" | "curious" | "humming" | "alerting" | "notifying" | "refusing") => void) | — | Called when a finite state has run its course and the engine has moved on by itself. Lets a host's own idea of the state follow without keeping a duplicate timer. |
paused | boolean | false | Freeze in place. The current frame is held rather than reset. |
poseHome | Partial<PoseHome> | — | Rest pose — the slight head turn held when nothing else is happening. |
preset | "mochi" | "sprout" | "boba" | "dot" | "pip" | "byte" | "momo" | "bao" | "teal" | "tato" | "hexa" | "tri" | "gemma" | "nimbo" | "dew" | "leafy" | "bloom" | "lucky" | "shroom" | "oak" | "pokey" | "nashi" | "pomme" | "berry" | "cheri" | "twinkle" | "luv" | "ember" | "chime" | "jelly" | "beep" | "luna" | "finn" | "inky" | "miso" | "hop" | "bruno" | "chirp" | "waddle" | "oink" | "rusty" | "kiki" | "maru" | "zip" | "fuzz" | "lulu" | — | A named character from the cast. Supplies `shape`, `color`, `face` and `nose` when they are not given. |
ref | Ref<CharacterHandle> | — | — |
seed | string | — | Seed for per-instance randomness so neighbours do not blink in unison. |
shape | "mochi" | "circle" | "blob" | "pebble" | "egg" | "bun" | "tofu" | "pill" | "hex" | "onigiri" | "gem" | "cloud" | "drop" | "leaf" | "flower" | "clover" | "sprout" | "mushroom" | "acorn" | "cactus" | "pear" | "apple" | "strawberry" | "cherry" | "star" | "heart" | "flame" | "bell" | "ghost" | "bot" | "crescent" | "boba" | "fish" | "octopus" | "cat" | "bunny" | "bear" | "chick" | "penguin" | "pig" | "fox" | "bob" | "topknot" | "spiky" | "curly" | "twinbuns" | — | Body shape. |
sizeCss | string | number | — | Any CSS length, taking precedence over `sizePx`. |
sizePx | number | 28 | Rendered size in pixels. Defaults to 28. |
sourceId | string | — | Id for the root SVG group, letting `CharacterMarkMirror` reuse this instance instead of starting its own animation. |
state | "idle" | "sleeping" | "drowsy" | "bored" | "waking" | "powering-down" | "listening" | "thinking" | "searching" | "reading" | "writing" | "working" | "loading" | "progress" | "radar" | "orbit" | "dictating" | "uploading" | "sending" | "receiving" | "messaging" | "waiting" | "spawning" | "composing" | "dragging" | "happy" | "excited" | "laughing" | "playful" | "proud" | "celebrate" | "bouncing" | "shy" | "sad" | "angry" | "confused" | "suspicious" | "surprised" | "scared" | "curious" | "humming" | "alerting" | "notifying" | "refusing" | — | Animation state. |
style | CSSProperties | — | — |
styleId | StyleId | — | The style series: `vector` (default), `pixel` or `kaomoji`. Named `styleId` because `style` is already the inline CSS. Switching it at runtime replaces the renderer and keeps the character's state and motion. |
styleOptions | Record<string, unknown> | — | Parameters of the series, such as `{ grid: 32 }` for `pixel`. Clamped by the engine. |
variant | "solid" | "outline" | "glass" | "sticker" | "paper" | "soft" | "riso" | (string & Record<never, never>) | — | Paint variant within the style series, such as `outline` for `vector`, `dot` for `pixel` or `bubble` for `kaomoji`. Falls back to the surrounding `AppearanceProvider`, then to the series' own default. |
Next
- MarkStageProvider — share engines by appearance
- States — every animation state, grouped
- HudPlate — a figure beside the dial