Implement DiceBear Core
This guide explains how to implement DiceBear Core in any programming language. A correct implementation produces byte-identical SVGs to the JavaScript and PHP reference implementations for the same seed and style definition.
Architecture overview
Avatar(definition, options)
│
├── Style Parse and validate the definition JSON
├── Options Resolve options using the PRNG
└── Renderer Generate SVG from resolved style + options
│
├── Prng Deterministic random number generator
│ ├── Fnv1a FNV-1a 32-bit hash
│ └── Mulberry32 Stateful PRNG
│
└── SVG outputThe core is intentionally minimal. It takes a style definition and user options, resolves randomizable values through a deterministic PRNG, and renders an SVG string.
PRNG contract
The PRNG is the interoperability surface. If your PRNG produces the same outputs as the reference for the same inputs, your implementation will produce identical SVGs. Get this right first.
FNV-1a 32-bit hash
FNV-1a converts a string into a 32-bit unsigned integer. DiceBear iterates over UTF-16 code units (not bytes, not code points).
offset_basis = 0x811c9dc5
prime = 0x01000193
function fnv1a_hash(input: string) -> uint32:
hash = offset_basis
for each UTF-16 code unit c in input:
hash = hash XOR c
hash = hash * prime (32-bit multiply, discard overflow)
return hash as unsigned 32-bitIn JavaScript, input.charCodeAt(i) returns UTF-16 code units. In PHP, mb_ord(mb_substr($input, $i, 1)) returns code points — for Basic Multilingual Plane characters (which covers all ASCII and most practical text), code units and code points are identical.
Reference (JS):
js
static hash(input) {
let hash = 0x811c9dc5;
for (let i = 0; i < input.length; i++) {
hash ^= input.charCodeAt(i);
hash = Math.imul(hash, 0x01000193);
}
return hash >>> 0;
}Mulberry32
Mulberry32 is a stateful PRNG that converts a 32-bit seed into a sequence of pseudo-random numbers. The implementation must match Tommy Ettinger's C reference exactly.
function mulberry32_next(state) -> (uint32, new_state):
state = (state + 0x6D2B79F5) as signed 32-bit
z = state
z = (z XOR (z >>> 15)) * (z OR 1) (32-bit multiply)
z = z XOR (z + ((z XOR (z >>> 7)) * (z OR 61))) (32-bit multiply)
return (z XOR (z >>> 14)) as unsigned 32-bit
function mulberry32_next_float(state) -> (float, new_state):
(value, new_state) = mulberry32_next(state)
return value / 2^32Reference (JS):
js
next() {
const z = (this.#state = (this.#state + 0x6d2b79f5) | 0);
let t = Math.imul(z ^ (z >>> 15), z | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0);
}
nextFloat() {
return this.next() / 4294967296; // 2^32
}Key details:
| 0forces 32-bit signed integer (handles overflow)>>> 0converts back to unsigned 32-bitMath.imulperforms 32-bit integer multiplicationnextFloat()returns a value in[0, 1)by dividing by2^32- The state is stateful — it advances with each call to
next()
Key-based value generation
DiceBear does not call the PRNG sequentially. Instead, each random decision uses a key to derive an independent value. This makes the output independent of call order.
function getValue(seed: string, key: string) -> float:
hash = fnv1a_hash(seed + ":" + key)
prng = new Mulberry32(hash)
return prng.nextFloat()For example, getValue("alice", "eyesVariant") always returns the same float, regardless of whether getValue("alice", "mouthVariant") was called before or after.
Selection methods
All selection methods sort their inputs by string representation using UTF-16 code unit comparison (JavaScript's default .sort() order) before operating on them. This ensures cross-language determinism.
pick(key, items) -> item | undefined
Selects one item from an array.
function pick(seed, key, items):
if items is empty: return undefined
sorted = sort items by string representation (UTF-16 code unit order)
if sorted has 1 item: return sorted[0]
index = floor(getValue(seed, key) * length(sorted))
return sorted[index]weightedPick(key, entries) -> item | undefined
Selects from [item, weight] tuples, respecting weights.
function weightedPick(seed, key, entries):
if entries is empty: return undefined
sorted = sort entries by item string representation
totalWeight = sum of all weights
if totalWeight == 0: return pick(seed, key, items from entries)
threshold = getValue(seed, key) * totalWeight
cumulative = 0
for each (item, weight) in sorted:
cumulative += weight
if cumulative > threshold: return item
return last itembool(key, likelihood) -> boolean
Returns true with probability likelihood / 100.
function bool(seed, key, likelihood = 50):
return getValue(seed, key) * 100 < likelihoodfloat(key, values) -> number | undefined
Returns a float within a range, rounded to 4 decimal places.
function float(seed, key, values):
if values is empty: return undefined
if values has 1 item: return values[0]
min = minimum(values)
max = maximum(values)
raw = min + getValue(seed, key) * (max - min)
return round(raw * 10000) / 10000integer(key, values) -> number | undefined
Returns an integer within a range (inclusive).
function integer(seed, key, values):
if values is empty: return undefined
if values has 1 item: return values[0]
min = minimum(values)
max = maximum(values)
return floor(getValue(seed, key) * (max - min + 1)) + minshuffle(key, items) -> items[]
Fisher-Yates shuffle using a stateful Mulberry32 instance (not key-based).
function shuffle(seed, key, items):
sorted = sort items by string representation
result = copy of sorted
prng = new Mulberry32(fnv1a_hash(seed + ":" + key))
for i from length(result) - 1 down to 1:
j = floor(prng.nextFloat() * (i + 1))
swap result[i] and result[j]
return resultNote: shuffle is the only method that uses a stateful PRNG instance directly (calling nextFloat() multiple times). All other methods call getValue() which creates a fresh PRNG for each key.
Options resolution
The Options class resolves raw user options into concrete values used by the renderer. Each resolution uses the PRNG with a specific key.
Seed
The seed defaults to '' (empty string) if not provided.
Core options
| Option | PRNG key | Resolution |
|---|---|---|
flip | flip | pick from ['none', 'horizontal', 'vertical', 'both'] |
rotate | rotate | float from range, default 0 |
scale | scale | float from range, default 1 |
borderRadius | borderRadius | float from range, default 0 |
translateX | translateX | float from range, default 0 |
translateY | translateY | float from range, default 0 |
fontFamily | fontFamily | pick from array, default 'system-ui' |
fontWeight | fontWeight | pick from array, default 400 |
Component options
For each component (e.g. eyes):
| Option | PRNG key | Resolution |
|---|---|---|
eyesProbability | eyesProbability | bool with likelihood, default 100 |
eyesVariant | eyesVariant | weightedPick from variants |
eyesRotate | eyesRotate | float from range or component default |
eyesTranslateX | eyesTranslateX | float from range or component default |
eyesTranslateY | eyesTranslateY | float from range or component default |
If the probability check fails, the component is not rendered (variant returns undefined).
Color options
For each color group (e.g. skin):
- Get candidate colors from user option (
skinColor) or style definition - Normalize all colors to hex
- Determine number of stops:
1if fill issolid, else fromskinColorFillStops(PRNG integer, default2) - Apply constraints from the style definition:
contrastTo: Sort candidates by WCAG 2.1 contrast ratio (descending) against the referenced color — do not shufflenotEqualTo: Filter out colors already selected for referenced groups
- If no
contrastToconstraint: shuffle candidates - Slice to number of stops
The PRNG key for color selection is {name}Color (e.g. skinColor).
SVG rendering pipeline
The renderer walks the element tree and generates an SVG string. The transformations are applied in a specific order — getting this wrong will produce different output.
1. Background
If a backgroundColor is set, render a <rect> covering the full canvas as the first element.
2. Element tree
Walk the canvas.elements array recursively:
element: Render as SVG tag with attributes. Resolve color references and variable references in attribute values.text: Render as escaped text content. Resolve variable references.component: Look up the selected variant (from options resolution). If the component is visible, render the variant's elements. Apply component-specific transforms (translate, rotate) as a wrapping<g>element.
3. Transform order
Wrap the rendered body in nested <g> elements in this order (outermost first):
- Border radius — clip path with rounded rectangle
- Translate —
translate(dx, dy)wheredx = (translateX / 100) * canvas.width,dy = (translateY / 100) * canvas.height - Rotate —
rotate(angle, cx, cy)around canvas center - Flip — depends on mode:
none: no transformhorizontal:translate(width, 0) scale(-1, 1)vertical:translate(0, height) scale(1, -1)both:translate(width, height) scale(-1, -1)
- Scale —
translate(cx, cy) scale(s) translate(-cx, -cy)wherecx = width / 2,cy = height / 2
4. SVG root element
The root <svg> element includes:
xmlns="http://www.w3.org/2000/svg"viewBox="0 0 {width} {height}"widthandheightattributes (ifsizeoption is set)- Global
attributesfrom the style definition role="img"andaria-label="{title}"iftitleis setaria-hidden="true"if notitleis set
If a title is set, include a <title> element as the first child.
Include any <defs> (gradients, clip paths) after the title.
Include a license comment from meta before the body content.
5. ID randomization
When idRandomization is true, append a random suffix to all id attributes and update all references (url(#...), href="#..."). This prevents ID collisions when multiple avatars are embedded in the same HTML document.
Gradient rendering
When a color group has multiple stops (fill is linear or radial):
- Create a
<linearGradient>or<radialGradient>in<defs> - Calculate stop offsets:
round(i / (count - 1) * 100)% - Apply gradient rotation via
gradientTransform="rotate(angle, 0.5, 0.5)" - Reference via
url(#gradient-id)in fill attributes - Gradient ID format:
{colorName}-color-{seedHash}whereseedHashis the FNV-1a hex hash of the seed (8 chars, zero-padded)
Initials extraction
The initial and initials variables are derived from the seed:
- Strip
@...suffix (for email addresses) - Remove apostrophe-like characters (
` ´ ' ʼ) - Match Unicode letter sequences (
\p{L}[\p{L}\p{M}]*) - If one word: take first 1-2 characters (respecting combining marks)
- If multiple words: take first character of first and last word
- Convert to uppercase
Testing your implementation
The most reliable way to verify your implementation:
- Generate an avatar with both your implementation and the JS reference using the same seed and style definition
- Compare the SVG output byte-for-byte
bash
# Generate reference SVGs with the CLI
dicebear initials ./reference --seed "Alice" --count 1
dicebear lorelei ./reference --seed "Alice" --count 1
dicebear avataaars ./reference --seed "Alice" --count 1Start with the initials style (simplest) and work up to more complex styles with multiple components and color constraints.
Reference implementations
| Language | Package | Source |
|---|---|---|
| JavaScript | @dicebear/core | src/js/core/src/ |
| PHP | dicebear/core | src/php/core/src/ |