@morphatar/core
v0.2.0
Published
Deterministic algorithmic avatar generator. Zero dependencies, raw SVG output.
Maintainers
Readme
@morphatar/core
Zero-dependency, deterministic algorithmic avatar generator.
One seed in, the same SVG out — on every machine, in every runtime, forever.
No Math.random(), no canvas, no network, no images to host.
- ~6 KB gzipped, 0 runtime dependencies, tree-shakeable (
sideEffects: false) - 3 variants —
organicblob with eyes,geometricBauhaus grid,pixelidenticon - Accessible by default — generated colours clear WCAG AA 4.5:1 contrast
- SSR-safe — byte-identical output on server and client, collision-free ids
- Framework-agnostic — returns a plain
<svg>string
Using React? Install @morphatar/react instead.
Install
npm install @morphatar/core
# or
pnpm add @morphatar/core
# or
yarn add @morphatar/coreThe package is ESM-only. Use import in any bundler (Vite, webpack,
Next.js, esbuild, Rollup) or in Node ≥ 18. require() works on Node versions
that support loading ES modules synchronously (20.19+, 22.12+).
Quick start
import { morphatar, morphatarDataUri } from '@morphatar/core';
// A raw <svg> string
const svg = morphatar({ seed: '[email protected]' });
// A data: URI for <img src> or CSS background-image
const uri = morphatarDataUri({ seed: '[email protected]', variant: 'pixel', size: 64 });In the browser (vanilla)
<div id="avatar"></div>
<script type="module">
import { morphatar } from 'https://cdn.jsdelivr.net/npm/@morphatar/core/+esm';
document.getElementById('avatar').innerHTML = morphatar({ seed: 'grace-hopper', size: 96 });
</script>As an <img>
const img = document.createElement('img');
img.src = morphatarDataUri({ seed: user.id, size: 48 });
img.alt = `${user.name}'s avatar`;Served from an HTTP endpoint
Output never changes for the same options, so responses can be cached forever.
// Next.js App Router: app/avatar/[seed]/route.ts
import { morphatar } from '@morphatar/core';
export function GET(_req: Request, { params }: { params: { seed: string } }) {
return new Response(morphatar({ seed: params.seed, size: 128 }), {
headers: {
'Content-Type': 'image/svg+xml',
'Cache-Control': 'public, max-age=31536000, immutable',
},
});
}The same pattern works in Express, Hono, Cloudflare Workers, Deno or Bun — the core only needs a JavaScript runtime.
Options
morphatar(options: MorphatarOptions): string| Option | Type | Default | Description |
| --- | --- | --- | --- |
| seed | string | — (required) | The deterministic seed. Same seed, same avatar. Usually a user id or email. |
| variant | 'organic' \| 'geometric' \| 'pixel' | 'organic' | Shape algorithm. |
| colors | string[] | derived from seed | Shape palette as hex, rgb() or hsl(). Every entry is a shape colour. |
| background | string | per variant | Background fill. 'transparent' / 'none' / '' paint nothing. |
| mask | 'circle' \| 'squircle' \| 'hexagon' \| 'none' | 'squircle' | Clip silhouette. |
| complexity | number (1–10) | 5 | Node count / density. Out-of-range values are clamped. |
| multicolor | boolean | false | Paint with the whole palette instead of one foreground. |
| animation | 'none' \| 'blink' \| 'dart' | 'none' | Idle eye motion, baked in as scoped CSS keyframes. organic only — the grids have no eyes. |
| size | number \| string | '100%' | Width/height. Numbers are px; strings are any CSS length. |
| title | string | 'Avatar' | Accessible name (aria-label). The seed is never written into the markup. |
Background
- organic renders on transparency by default — it is a floating blob.
- geometric and pixel paint the palette's own background by default, because their empty cells are negative space.
morphatar({ seed: 'ada' }); // transparent blob
morphatar({ seed: 'ada', background: '#09090b' }); // painted
morphatar({ seed: 'ada', variant: 'pixel', background: 'transparent' }); // forced offWhatever you pass as background is also the palette's contrast anchor:
generated foregrounds are re-derived to clear 4.5:1 against it. With no
background the library cannot know what your page looks like, so pass your
page's colour when legibility against a specific surface matters. Adding a
background never changes the shapes.
With a transparent organic avatar the mask has nothing to clip — the blob's
own outline is the silhouette.
Colour modes
| Variant | multicolor: false | multicolor: true |
| --- | --- | --- |
| organic | one flat foreground | soft blurred colour field clipped to the blob |
| geometric | every cell shares one colour | each cell picks its own |
| pixel | every pixel shares one colour | each pixel picks its own |
Custom palettes
morphatar({
seed: 'ada',
variant: 'geometric',
multicolor: true,
colors: ['#e4572e', '#1789bb', '#1b1b1b'],
background: '#f4efe4',
});Custom colours are used as given — they are not adjusted for contrast. Use
hex, rgb() or hsl(); named colours (red) render, but the contrast maths
reads them as black.
Other exports
| Export | What it does |
| --- | --- |
| morphatarDataUri(options) | Same as morphatar, encoded as a data:image/svg+xml URI. |
| morphatarPalette(options) | Resolve a seed's { background, foreground[] } without rendering. Useful for theming UI around an avatar. |
| DEFAULTS | The default option values. |
| contrastRatio(a, b), luminance(c), parseColor(c), hslToHex(h, s, l), MIN_CONTRAST | Colour utilities used by the palette. |
| fnv1a, sfc32, createRandom, randInt, pick, shuffle | The deterministic PRNG primitives. |
| createPalette, renderOrganic, renderGeometric, renderPixel, renderEyes, blobPath, maskShape, SQUIRCLE_PATH, EXPRESSIONS | Building blocks, for composing your own renderer. Generators take a RenderContext and return a Drawing ({ defs, layers }). |
All option and return types (MorphatarOptions, Variant, Mask,
Animation, Palette, RenderContext, Drawing, Rand) are exported.
Guarantees
- Deterministic. The same options produce a byte-identical string. Avatars are stable across versions within a major release; a change that alters existing avatars ships as a breaking change.
- Safe to inject.
title,colors,backgroundandsizeare escaped before they reach the markup, so hostile values cannot break out of an attribute. The seed never appears in the output. - No id collisions. Clip paths, filters and keyframes are named from a hash of the options, so many avatars can share one page.
- Respects
prefers-reduced-motion. Animations switch themselves off.
How it works
- Hash — FNV-1a folds the seed into a 32-bit integer.
- Expand — splitmix32 seeds an sfc32 PRNG with 128 bits of state.
- Colour — the palette is drawn first, so changing
variantorcomplexitykeeps a seed's colours. - Draw — the same stream feeds the chosen generator.
- Assemble — shapes, background, mask and keyframes become one
<svg>.
License
MIT
