Skip to content

React usage

The React entry exports four components with one shape. Each renders a single <canvas> and starts its engine in an effect after mount.

SHARK TANK / OPEN WATERLIVE

Click to splash. Move the pointer to scatter the school.

Seed 11, columns 110, and speed 1 are fixed in this demo. The palette and calm controls update the running engine without rebuilding the world.

import {
AsciiDog,
AsciiJellyfish,
AsciiRobots,
AsciiSharks,
} from "ascii-things/react";

AsciiOcean is a back-compat alias for AsciiSharks.

The wrappers include the "use client" directive, so they work in Next.js App Router client components without extra ceremony.

The wrappers render a canvas with:

display: block;
width: 100%;
height: 100%;

Give the parent a height. Or pass width and height explicitly. The canvas has no intrinsic aspect ratio in this release.

// Parent owns the box
<div style={{ height: 420 }}>
<AsciiDog palette="abyss" seed={7} />
</div>
// Canvas owns the box
<AsciiDog palette="abyss" seed={7} height={420} />
// CSS aspect-ratio box
<div style={{ aspectRatio: "16 / 9" }}>
<AsciiDog palette="abyss" seed={7} />
</div>

id, className, style, width, height, and aspectRatio are passed through. The component also adds ascii-things and ascii-<scene> classes, which you can target from CSS.

<AsciiDog palette="abyss" seed={7} aspectRatio="16 / 9" />
<AsciiDog
palette="abyss"
seed={7}
onReady={(scene) => scene.throwBall(600, 360)}
/>
<AsciiSharks
className="hero-scene"
style={{ borderRadius: 4 }}
palette="abyss"
seed={42}
/>

aspectRatio sets the canvas aspect-ratio and switches its height to auto. onReady receives the live SceneHandle after each mount, so you can call throwBall, pause, setOptions, or read getStats without a separate effect.

Some options update the running engine. Others rebuild the world.

Updates live Remounts the scene
palette seed
speed sharks, density, sharkScale
columns robots, count, scale
maxCells jellies
adaptiveQuality dogs, spread
calm
label
paused
fontFamily
onEvent

interactive and maxDpr are read when the engine is constructed. Change them by remounting the component (for example, with a React key), or use the vanilla API if you need to toggle them at runtime.

The wrapper computes a structural key from the remounting props. Changing a roster array literal on every render will remount the scene every time. Hoist the array or memoize it.

// Stable roster: no remount between renders.
const SHARKS = [{ kind: "hammerhead", size: 1 }] as const;
<AsciiSharks sharks={SHARKS} seed={42} />;

Use onEvent for scene-specific moments. The callback receives a discriminated union.

import { AsciiDog } from "ascii-things/react";
<AsciiDog
palette="paper"
seed={7}
label="A live ASCII robot dog test yard"
onEvent={(event) => {
if (event.kind === "nudge") console.log("fetch complete", event.x, event.y);
if (event.kind === "low") console.log("low battery", event.id);
}}
/>;

Event kinds:

Scene Kinds
Sharks eat, splash
Robots dock, undock, job, low
Jellyfish pulse, shock
Dog ball, nudge, charge, low, sit

The React wrappers do not forward a ref or expose an imperative handle in 0.4.0. For pause(), resume(), setOptions(), getStats(), or DogEngine.throwBall(), use the vanilla factory and a canvas ref:

import { useEffect, useRef } from "react";
import { createDog } from "ascii-things/dog";
import type { DogEngine } from "ascii-things/dog";
export function Yard() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const engineRef = useRef<DogEngine | null>(null);
useEffect(() => {
if (!canvasRef.current) return;
const engine = createDog(canvasRef.current, { palette: "abyss", seed: 7 });
engineRef.current = engine;
const timer = window.setInterval(() => {
console.log(engine.getStats());
}, 1000);
return () => {
window.clearInterval(timer);
engine.destroy();
engineRef.current = null;
};
}, []);
return (
<div style={{ height: 420 }}>
<canvas
ref={canvasRef}
style={{ display: "block", width: "100%", height: "100%" }}
aria-hidden="true"
/>
<button type="button" onClick={() => engineRef.current?.pause()}>
Pause
</button>
<button type="button" onClick={() => engineRef.current?.resume()}>
Resume
</button>
</div>
);
}

AsciiDogHero is the exception: it owns a dog engine internally because it needs a live readout and a command input. See UI components.

The engine reacts to prefers-reduced-motion: reduce on its own: it lets the world settle, renders one still frame, and disables pointer actions. You do not need a React hook for the scene itself.

If your own controls should react too, read the media query in an effect, never during render:

import { useEffect, useState } from "react";
const [reduced, setReduced] = useState(false);
useEffect(() => {
const query = window.matchMedia("(prefers-reduced-motion: reduce)");
const update = () => setReduced(query.matches);
update();
query.addEventListener("change", update);
return () => query.removeEventListener("change", update);
}, []);

The docs demos use exactly this pattern.

The wrappers are safe to render on the server. They emit canvas markup and create the engine in useEffect. The canvas is empty until hydration.

  • In Next.js App Router, import them from a client component.
  • In Astro, use client:load, client:visible, or client:only="react".
  • Do not generate a different seed on the server and client if you want hydration parity. A fixed seed is simplest. With no seed, the engine chooses a random world on the client only.

See SSR and hydration for framework-specific notes.

Symptom Cause Fix
Blank canvas Parent has zero height Set a parent height or pass height
Scene restarts on every render Roster or seed prop changes identity Hoist arrays and memoize values
Palette looks wrong Custom palette object is missing tokens, or dark does not match the page Start from PALETTES.<name> and override fields
No animation Reduced motion is enabled, or the tab is hidden Expected behavior; use calm or check the media query
pause() missing on component Wrappers do not expose a handle Use the vanilla engine

The full shared and per-scene option tables are in the API reference.