Theirs for good
The same person gets the same creature on every page and every device. Nothing to store, upload or fetch.
Animated avatars that breathe, blink and wiggle their antennae. Everyone who signs up gets one of their own, in React, Vue, Astro or plain HTML.
Member lists, profiles, conversations. Pick a teammate and their avatar travels into the profile.
Shipping the onboarding flow today.
On it, reviewing after lunch.
Scarves, hats and antennae at every angle. Each person keeps the same Nurbling wherever they show up.
The same person gets the same creature on every page and every device. Nothing to store, upload or fetch.
They breathe, blink and drift in plain CSS, and hold still for anyone who prefers reduced motion.
An avatar glides from a list into a profile, or a card into a header, instead of jumping there.
Ten ready themes, or two to five brand colours of your own. Faces stay readable either way.
Containers follow the page, and antennae stay visible on either side. One prop, no script.
No dependencies, full TypeScript types, and server components that send no JavaScript to the browser.
Every part is a slot: wrap it, replace it or drop it. Add parts of your own, and stack presets that each change something. The seed still decides everything else.
Wrap the body and add a belly patch, a lighter shade of its own shell colour.
const belly = {
slots: {
body: ({ anchors: a, colours, n }, base) => {
const { left, right, y } = a.band(0.2)
return `${base()}<ellipse cx="${n(a.centre)}" cy="${n(y)}"
rx="${n((right - left) * 0.28)}" ry="${n((a.base - y) * 0.6)}"
fill="${shade(colours.shell, 0.5)}"/>`
},
},
}Keep the eyes and put round glasses over them, on the eye anchors.
const glasses = {
slots: {
eyes: ({ anchors: { eyes: [l, r] }, n }, base) => {
const size = l.rx * 1.7
return `${base()}<g fill="none" stroke="#1b1b1b" stroke-width="1.3">
<circle cx="${n(l.x)}" cy="${n(l.y)}" r="${n(size)}"/>
<circle cx="${n(r.x)}" cy="${n(r.y)}" r="${n(size)}"/>
<path d="M${n(l.x + size)} ${n(l.y)}H${n(r.x - size)}"/></g>`
},
},
}Replace the mouth with a wide smile in the eye colour, as wide as the eyes are apart.
const smile = {
slots: {
mouth: ({ anchors: a, small, paint, n }) => {
if (small) return ''
const w = (a.eyes[1].x - a.eyes[0].x) * 0.35
return `<path d="M${n(a.centre - w)} ${n(a.mouth)}
Q${n(a.centre)} ${n(a.mouth + w)} ${n(a.centre + w)} ${n(a.mouth)}"
fill="none"${paint('eye', 'stroke')}
stroke-width="1.6" stroke-linecap="round"/>`
},
},
}Swap the scarf, collar or hat for a heart pin on the chest.
const pin = {
slots: {
extra: ({ anchors: a, small, n }) => {
if (small) return ''
const x = a.chest.x + (a.band(0.2).right - a.chest.x) * 0.25
const y = a.band(0.2).y
return `<path d="M${n(x)} ${n(y + 2.6)}l-3-3a1.8 1.8 0 0 1 3-2.4
a1.8 1.8 0 0 1 3 2.4z" fill="#e63972"/>`
},
},
}Add a part of your own: a status dot, read from props on each call, ringed in the container colour so it switches with the page.
const status = {
parts: {
status: {
after: 'mouth',
small: true,
draw: ({ anchors: a, props, paint, n }) => {
const { right, y } = a.band(0.14)
const fill = props.status === 'online' ? '#2e9e5b' : '#9a9a9a'
return `<circle cx="${n(right - 2)}" cy="${n(y)}" r="7"
fill="${fill}"${paint('ground', 'stroke')} stroke-width="2.5"/>`
},
},
},
}
avatars.nurbling(user.id, { props: { status: 'online' } })A season of new accessories, added to the list the seed picks from. Here the originals are dropped, so every avatar wears one of the new three.
const winter = {
extras: {
beanie: {
draw: ({ anchors: a, colours, n }) => {
const rows = [0.62, 0.68, 0.74, 0.8, 0.85].map((f) => a.band(f))
const right = rows.map((r) => `${n(r.right)},${n(r.y)}`)
const left = rows.map((r) => `${n(r.left)},${n(r.y)}`).reverse()
return `<path d="M${[...right, ...left].join('L')}Z"
fill="${colours.wear}"/>`
},
},
earmuffs: {
draw: ({ anchors: a, colours, n }) => {
const { left, right, y } = a.band(0.5)
return [left, right].map((x) => `<circle cx="${n(x)}"
cy="${n(y)}" r="4.5" fill="${colours.accent}"/>`).join('')
},
},
bowtie: {
draw: ({ anchors: { chest: c }, colours, n }) =>
`<path d="M${n(c.x)},${n(c.y)}l-5,-3v6zM${n(c.x)},${n(c.y)}l5,-3v6z"
fill="${colours.accent}"/>`,
},
},
}
// add them to the built-in extras: use: [winter]
// or wear only the new season:
createNurblings({
use: [winter],
extras: { none: false, scarf: false, hat: false, collar: false },
})Keep the container and give it your brand colour.
const brand = {
slots: { backdrop: (ctx, base) => setFill(base(), '#ffd166') },
}Every recipe here is a preset. Stack them with use: each slot wraps what the ones before it drew.
createNurblings({
use: [belly, glasses, smile, pin, status, brand],
})A component for React, Vue and Astro, and one HTML tag for everything else.
import { Nurbling } from '@nurblings/react'
export function Avatar({ user }) {
return <Nurbling seed={user.id} title={user.name} size={48} />
}<script setup>
import { Nurbling } from '@nurblings/vue'
defineProps(['user'])
</script>
<template>
<Nurbling :seed="user.id" :title="user.name" :size="48" />
</template>---
import { Nurbling } from '@nurblings/astro'
const { user } = Astro.props
---
<Nurbling seed={user.id} title={user.name} size={48} /><script type="module"
src="https://cdn.jsdelivr.net/npm/@nurblings/element/dist/define.js/+esm"></script>
<nurbling-avatar seed="user-42" title="Ada Lovelace" size="48"></nurbling-avatar>import { nurbling, nurblingSrc } from 'nurblings'
element.innerHTML = nurbling('user-42', { size: 48, title: 'Ada Lovelace' })
image.src = nurblingSrc('user-42')Install the Nurblings skill once, then ask for avatars in plain words.
npx skills add albertesparragoza/nurblings # then, in your agent > Give every commenter an avatar.