Ribbit turns a string or number into the same generative mark every time. Use the component for an avatar, or use the core to paint Canvas, generate SVG and export media without storing images or making network requests.
Installation
$ bun add ribbit-canvasRibbit has no runtime dependencies. react >= 18 and svelte >= 5 are
optional peer dependencies for their respective adapters.
Components
For an avatar, import the adapter for your UI framework and pass a stable user identifier.
React
import { RibbitAvatar } from "ribbit-canvas/react";
export function Avatar({ user }) {
return <RibbitAvatar seed={user.id} size={40} />;
}
Svelte
The default export has the same avatar defaults.
<script>
import RibbitAvatar from "ribbit-canvas/svelte";
</script>
<RibbitAvatar seed={user.id} size={40} />
Core renderer
Use the core directly when the mark belongs in a canvas composition rather than a component.
import { render } from "ribbit-canvas";
render(canvas, "null-frog", { size: 256, pattern: "dither" });
Props
RibbitAvatar is the small, high-level API for both adapters.
| Prop | Type | Default | Effect |
|---|---|---|---|
seed |
string | number |
required | Stable identity for the mark |
size |
number |
32 |
Displayed square size in CSS pixels |
pattern |
"bars" | "dither" | "glyph" | "maze" | "pulse" | "wave" |
"dither" |
Rendering language |
radius |
number | string |
"9999px" |
Circle, rounded square or square presentation |
palette |
{ background, ramp } |
moss | Overrides the color system |
animated |
boolean |
false |
Evolves the field when visible |
reactive |
boolean |
false |
Cells light up around the cursor |
className, style |
React only | — | Applied to the clipping wrapper |
class |
Svelte only | — | Applied to the canvas |
The component first paints a static frame. When animated is enabled, it
pauses offscreen and respects prefers-reduced-motion.
reactive makes the cells near the pointer climb the tonal ramp. The glow is
folded into the field before it is quantized, so the mark lights up rather than
wearing an overlay, and the geometry of the seed never moves. It is ignored on
touch, under prefers-reduced-motion, and for wave, which has no cell grid.
The two props compose: animated reactive gives a field that both evolves and
answers the cursor.
<RibbitAvatar seed="null-frog" size={96} reactive />
Under the hood this is renderReactive, which takes a pointer in canvas
coordinates. Reach for it directly when you drive your own canvas:
import { renderReactive } from "ribbit-canvas";
canvas.addEventListener("pointermove", (event) => {
const box = canvas.getBoundingClientRect();
renderReactive(ctx, "null-frog", {
size: 256,
pointer: { x: event.clientX - box.left, y: event.clientY - box.top },
});
});
With no pointer it is render, so static exports are untouched and the mark
at rest is exactly the one ribbit generates. glowRadius (default 0.42 of the
shortest side) and glowBoost (default 0.6) tune reach and lift.
Sizes & shapes
radius controls only how an adapter is displayed. It defaults to a circle;
pass a number for a rounded square or 0 for a square.
<RibbitAvatar seed="studio" size={84} />
<RibbitAvatar seed="studio" size={84} radius={18} />
<RibbitAvatar seed="studio" size={84} radius={0} />
For generated files, use the core option shape: "circle". It makes PNG and
SVG corners truly transparent, rather than merely clipping them in CSS.
const avatar = await toBlob("ada", {
preset: "avatar",
size: 512,
shape: "circle",
});
Patterns
| Pattern | Character | Best suited to |
|---|---|---|
dither |
Square Bayer cells over a continuous field | Avatars, pixel art, compact marks |
glyph |
The field sampled as monospace symbols | Technical identities, editorial graphics |
pulse |
Dots that swell and brighten with the field | Soft avatars, halftone textures, hover surfaces |
maze |
Seeded diagonals in the 10 PRINT tradition | Retro identities, texture fills, terminal aesthetics |
bars |
Vertical bars rising with the field | Audio-adjacent brands, dashboards, data-flavored marks |
wave |
Five to nine layered contours | Covers, backgrounds, animated surfaces |
Engine helpers
Use the core when a component is not the right output.
| Helper | Output | Environment |
|---|---|---|
render(target, seed, options?) |
Existing Canvas or 2D context | Browser or Offscreen Canvas |
toSVG(seed, options?) |
Standalone SVG string | Browser or server |
toDataURL(seed, options?) |
PNG data URL | Browser DOM |
toBlob(seed, options?) |
PNG Blob |
Browser DOM |
toWebM(seed, options?) |
Animated WebM Blob |
Browser with MediaRecorder |
The renderer accepts an HTMLCanvasElement, an OffscreenCanvas, or either
2D context. Passing a canvas sets its backing dimensions; passing a context
preserves your transform.
import { render } from "ribbit-canvas";
const width = 320;
const height = 180;
const dpr = Math.min(window.devicePixelRatio, 2);
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
const ctx = canvas.getContext("2d");
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
render(ctx, "brook", { width, height, pattern: "wave" });
width and height override size. On a non-square canvas, cells and field
frequencies stay proportional instead of stretching.
Exports
All export helpers share seed, pattern, shape, palette and t.
import { toBlob, toSVG } from "ribbit-canvas";
const avatar = await toBlob("ada", {
preset: "avatar",
size: 512,
pattern: "glyph",
shape: "circle",
});
const socialCover = toSVG("release-1.4", {
preset: "og",
pattern: "wave",
});
preset: "avatar" is square. preset: "og" always produces a native
1200×630 cover; size, width and height do not change that preset.
Try avatar and OG formats in the playground.
Animation
t: 0 is a static frame. Advance time in seconds to animate the same visual
identity.
render(ctx, "dragonfly", {
size: 256,
pattern: "dither",
t: performance.now() / 1000,
});
Use toWebM when an animation must leave the browser as a file.
import { PALETTES, toWebM } from "ribbit-canvas";
const video = await toWebM("dragonfly", {
preset: "og",
pattern: "wave",
palette: PALETTES.tide,
duration: 5,
fps: 30,
});
Recording happens in real time. Ribbit tries VP9, then VP8, then generic WebM; the helper rejects when the browser cannot encode any supported format.
Palettes
Every renderer accepts a palette with a background and a background-to-foreground
ramp. Ribbit ships four families — moss, tide, ember and mono — each with
a light-mode variant: mossLight, tideLight, emberLight and monoLight.
import { PALETTES, toSVG } from "ribbit-canvas";
const cover = toSVG("launch", {
preset: "og",
pattern: "wave",
palette: PALETTES.ember,
});
Pick a variant at runtime to follow the reader’s color scheme:
const dark = matchMedia("(prefers-color-scheme: dark)").matches;
render(canvas, "launch", { palette: PALETTES[dark ? "tide" : "tideLight"] });
Transparent backgrounds
Set background: null to skip the backdrop. The canvas keeps whatever is behind
it, toSVG omits its backing rect, and toBlob/toWebM keep the alpha channel
(pass matte to flatten onto a color instead).
render(canvas, "launch", {
palette: { ...PALETTES.tide, background: null },
});
Only tone 0 is left unpainted, so how much shows through depends on the pattern:
glyph and maze stay mostly transparent, pulse and bars around half,
wave around 15%, and dither covers nearly the whole surface.
Custom ramps need at least two colors, so a palette override always carries a
ramp.
Changing a palette never changes a seed’s geometry, so the same seed and pattern
retain their composition.
API reference
| Option | Type | Default | Effect |
|---|---|---|---|
size |
number |
200 |
Fallback for both dimensions |
width, height |
number |
size |
Logical dimensions |
pattern |
pattern name | "dither" |
Bars, dither, glyph, maze, pulse or wave renderer |
shape |
"rectangle" | "circle" |
"rectangle" |
Transparent circular crop for core output |
palette |
palette | moss | Background and tonal ramp; background: null is transparent |
t |
number |
0 |
Time in seconds |
Lower-level drawDither, drawGlyph and drawWave paint square contexts
directly. seedFromString hashes strings to unsigned 32-bit seeds, and
toSeed accepts either a string or number.
License
Ribbit is MIT licensed and free to use in personal and commercial projects.