@usenavii/core
Framework-agnostic engine. Seed in, SVG string out. Pure TypeScript, zero runtime dependencies, ~8 KB gzipped target.
Install
npm i @usenavii/core
# or pnpm / yarn / bun
Functions
createAvatar(seed: string, options?: AvatarOptions): string
random(options?: AvatarOptions): { svg: string; seed: string }
selectAvatar(seed: string, options?: AvatarOptions): AvatarSpec
renderAvatar(spec: AvatarSpec, options?: AvatarOptions): string
renderAvatarInner(spec: AvatarSpec, options?: AvatarOptions): string
renderGroup(seeds: string[], options?: GroupOptions): string
createAvatar is the convenience composition of selectAvatar + renderAvatar. Use the split pair when you want to inspect or mutate the spec between picking and rendering.
renderAvatarInner emits the SVG body without an outer <svg> wrapper — useful when composing multiple avatars into one SVG document (this is how renderGroup works internally).
Random avatars
Navii.random() picks a fresh seed for you and renders the avatar. Returns both the SVG and the chosen seed so you can persist it — saving the seed to the user's profile makes future renders stable.
import { Navii } from '@usenavii/core';
const { svg, seed } = Navii.random({ size: 96 });
// persist the seed so the user's avatar is stable on next visit
await db.users.update(user.id, { naviiSeed: seed });
Use for "spin again" UX, lazy onboarding (assign an avatar before the user picks one), dev/demo seeding. Seed source: crypto.randomUUID() with a Math.random() fallback.
React: stabilize the seed across re-renders with useState:
const [{ seed }] = useState(() => Navii.random());
return <Navii seed={seed} />;
Calling Navii.random() directly inside a render function (without useState/useMemo) gives you a new avatar on every re-render.
Seed helpers
seed(fields: SeedFields, options?: SeedOptions): string
seedFromEmail(email: string): string
normalizeEmail(email: string): string
Navii.seed({ id, email, name, createdAt }) picks the most unique field available: id → email → name + createdAt → name. The email branch is hashed by default with seedFromEmail() so PII never reaches the wire.
import { seed, seedFromEmail } from '@usenavii/core';
const s = seed({ id: user.id, email: user.email, name: user.name });
// id wins if present; otherwise sha256-of-email; otherwise name.
const hashed = seedFromEmail('alice@example.com');
// → "973dfe46…b4e813b" — sha256 hex of normalized email.
Privacy — why hash emails? Raw emails in URLs leak into server access logs, Referer headers, browser history, CDN cache keys, and analytics pixels. seedFromEmail() applies sha256(email.trim().toLowerCase()), so the seed stays stable and two services hashing the same way get the same avatar for the same person.
Migrating off raw-email seeds. If existing avatars are keyed on the plaintext email and changing them would surprise users, pass { hashEmail: false } to keep the old behavior:
seed({ email: user.email }, { hashEmail: false }); // legacy — avoid in new code
AvatarOptions
| Option | Type | Default | Description |
|---|---|---|---|
size | number (px) | 96 | Output canvas size. SVG viewBox is fixed at 100×100; size scales it. |
paletteId | string | seeded | Force a specific palette. Pass any palette id. |
palette | Palette object | — | Runtime/brand palette object (e.g. pulled from Figma variables). Wins over paletteId. No registration in PALETTES required. |
packs | readonly string[] | — | Enable themed packs (premium content). Pack ids resolve against the built-in registry; unknown ids are silently skipped. Their palettes + parts merge into the selection pool, so the same seed renders differently from the base pool. Empty/undefined → base pool only. |
style | StyleHint | — | 'masc' | 'femme' | 'neutral'. Biases seeded picks toward a gender expression. Only takes effect when an enabled pack defines styleHints. Determinism preserved: same seed + same style = same output. |
background | enum or { color: string } | seeded | Override scene fill. Enum form picks from 'none' | 'solid' | 'ring'; object form supplies an exact color (SDK-only — URL form accepts enum only). |
mood | enum | seeded | 'neutral' | 'happy' | 'serious' | 'sleepy' | 'wink'. Overrides seed-derived eyes + mouth with a curated pair. Same seed + mood = byte-identical. Bypasses pack eye/mouth constraints by design. |
title | string | — | Adds role="img" and aria-label. |
animated | boolean | false | Emits inline <style> with idle animations. Honors prefers-reduced-motion. |
tileBg | string | — | Opaque circular fill behind avatar. Any CSS color or 'auto' to use palette accent. |
AvatarSpec
The resolved description of an individual avatar — what selectAvatar returns and what renderAvatar consumes.
interface AvatarSpec {
seed: string;
palette: Palette;
body: BodyShapeId;
eyes: EyeStyleId;
mouth: MouthStyleId;
antenna: AntennaStyleId;
accessory: AccessoryId;
background: BackgroundId;
topper: TopperId;
// Continuous tweaks
hueShift: number; // degrees, signed
bodyScale: number; // 0.92–1.08
eyeGapShift: number; // px (viewBox units), signed
mouthCurveScale: number; // 0.85–1.15
antennaTilt: number; // degrees, signed
}
All *Id types are string unions. Palette is an object: { id, bodyFrom, bodyTo, accent, ink, blush }.
Advanced: composition
The split selectAvatar + renderAvatar lets you override any part programmatically — not just the two the HTTP API exposes.
import { selectAvatar, renderAvatar } from '@usenavii/core';
const base = selectAvatar('alice');
const svg = renderAvatar({ ...base, body: 'tall', eyes: 'star' }, { size: 128 });
GroupOptions
Extends AvatarOptions with these additional fields:
| Option | Type | Default | Description |
|---|---|---|---|
size | number | 64 | Per-tile size. |
overlap | number | 0.3 | Tile overlap fraction (0–0.7). |
max | number | all | Cap tiles; remainder collapses into +N. |
ring | string | #ffffff | Border ring around each tile. |
tileBg | string | #ffffff | Solid fill behind each avatar. |
counterFill | string | #E5E7EB | Background of the +N tile. |
counterInk | string | #374151 | Text color of the +N tile. |
Other exports
createRng(seed)— the PRNG used internally. Returns{ next(), range(min, max), pick(arr) }.cyrb53(string)— fast 53-bit string hash. Used to seed the PRNG.@usenavii/core/partssubpath — exports the part-id arrays (BODY_IDS,EYE_IDS, etc.) andPALETTES.