Deterministic geometric blobatars from any string. No dependencies, ~4.4 KB gzipped.
bun add blobatar # npm / pnpm / yarn all work tooA blobatar always stands for somebody — a user, a bot, a team, a repo — so the
value it is generated from is that somebody's name: a username, a display
name, an email, a handle, an id. Any string works, and the same string always
renders the same blobatar.
bun add blobatar @blobatar/reactimport { Blobatar } from "@blobatar/react";
<Blobatar name={user.email} size={48} />;Everything but name is optional. Remaining props land on the underlying
element, so className, alt and the rest behave as you would expect.
bun add blobatar @blobatar/vue<script setup>
import { Blobatar } from "@blobatar/vue";
</script>
<template>
<Blobatar name="alain@example.com" :size="48" />
</template>The same props and the same behavior as the React adapter. Anything not
declared as a prop lands on the underlying element, so class, style, alt
and the rest behave as you would expect.
bun add blobatar @blobatar/svelte<script>
import { Blobatar } from "@blobatar/svelte";
</script>
<Blobatar name="alain@example.com" size={48} />Svelte 5 or newer. This package ships its component as source rather than as
built JavaScript, because a .svelte file only becomes renderable code inside
your own compiler — any toolchain that resolves the svelte export condition
handles that with no configuration, and one that does not reports an unresolved
import naming the package rather than handing your bundler a file it cannot
execute. See ADR-0010.
bun add blobatar @blobatar/solid # or @blobatar/preactimport { Blobatar } from "@blobatar/solid";
<Blobatar name={user.email} size={48} />;The React section with the import swapped, which is the whole difference — each
is compiled by its own framework's JSX transform rather than re-using React's,
so solid-js reactivity and Preact's runtime both behave as they should.
bun add blobatar @blobatar/react-native
npx expo install react-native-svg # or bun add react-native-svgimport { Blobatar } from "@blobatar/react-native";
<Blobatar name={user.email} size={48} />;Expo needs nothing beyond that install. An Expo app is a React Native app, and
react-native-svg is the same library in both, so there is no @blobatar/expo
and there is not going to be one.
Two differences from every other adapter, and both are the platform rather than the package:
size is required. On the web, omitting it lets CSS size the element and
the viewBox scales to whatever the page decides. React Native has no such
fallback, so an unsized blobatar is a blank square. Defaulting it here was the
alternative and it is the one thing an adapter may never do: a default size is
a default that changes the picture, and the core is the only place a default is
written down.
There is no animate. Blobatar's idle motion is a stylesheet: motion.css,
a root class, and a dozen seeded custom properties the CSS reads. React Native
has none of the three. The prop is absent from the type rather than accepted and
ignored, so passing it is a compile error instead of a blobatar that silently
sits still.
expression does work, in full. A static pose bakes into the geometry, which is
why it survives here for the same reason it survives in the string API. What is
missing is only the morph between poses, which was always the part that needed
CSS.
Everything else is the same: same names, same options, same blobatar. Anything
not declared as a prop lands on the underlying <Svg>. title becomes the
accessibility label rather than a <title> element, because react-native-svg
has none. Without one the tree is hidden from screen readers, which is the
same call aria-hidden makes on the web.
The registry serves shadcn's Avatar with a blobatar as the fallback for a
missing profile image:
npx shadcn@latest registry add @blobatar=https://blobatar.dev/r/{name}.json
npx shadcn@latest add @blobatar/avatarOr straight from this repository, without registering anything:
npx shadcn@latest add Alain00/blobatar/avatarThat reads main, so it is the current composition rather than the last
released one. Both forms copy the same file.
import { Blobatar } from "@/components/ui/blobatar";
<Blobatar name={user.email} src={user.avatarUrl} />;That Blobatar is not the adapter's — it takes a src alongside the name,
and everything the adapter takes goes in a blobatar prop. A project using both
imports one of them under another name.
It is also all that gets copied into your project: the composition, which you
own and edit. The generator stays in blobatar and @blobatar/react, installed
as ordinary dependencies — a copied-in generator would be pinned to whichever
generation you took it from, and the whole point is that a name renders the same
picture everywhere.
Four more items are interface built out of that avatar, each one a composition you own after installing it:
npx shadcn@latest add @blobatar/presence-avatar
npx shadcn@latest add @blobatar/agent-list
npx shadcn@latest add @blobatar/user-table
npx shadcn@latest add @blobatar/group-chatpresence-avatar is a face with a presence dot and an unread badge, animated,
plus a thinking state that adds a three-dot loading indicator. agent-list is the aside an agent runner needs,
built on it. user-table is a table of people with a static blobatar per row.
group-chat is a thread with consecutive messages grouped under one face. All
four are rendered, with the command that installs each, at
blobatar.dev/components.
blobatar() returns SVG markup as a string, and blobatarUri() wraps it in a
data: URI for <img src> or background-image:
import { blobatar } from "blobatar";
import { blobatarUri } from "blobatar/uri";
blobatar("alain@example.com"); // '<svg xmlns="..." viewBox="0 0 100 100">…'
el.style.backgroundImage = `url("${blobatarUri(user.id)}")`;The main entry also carries the palette and trait utilities. If all you do is render, import the renderer on its own and save about a kilobyte:
import { blobatar } from "blobatar/blob";Options are the same across every adapter and the string API. background, hue
and tone cover the common cases; traits pins any individual axis as the 0–1
position the hash would otherwise have produced:
<Blobatar name={user.email} background="circle" hue={210} size={48} />;
// Always a sun with wide eyes — colour and everything else still per name.
blobatar(user.email, { traits: { shape: 0.95, "eye.ratio": 0 } });Keys you leave out still come from the name — lock the two things that carry your brand, and every user still gets their own creature. Pin everything and the name stops mattering, which is how you build one fixed blobatar.
Every value those options take, and what each one draws:
Both are opt-in. animate idles the blobatar — breathe, bob, blink, glance —
and expressions are imported as values so you ship only the poses you use:
import { Blobatar } from "@blobatar/react";
import { happy } from "blobatar/expression";
import "blobatar/motion.css"; // required — nothing animates without it
<Blobatar name={user.email} animate="hover" expression={happy} size={64} />;The same props work in every adapter — only the component import changes.
The one exception is React Native, which has no animate. See that section above.
Those subpaths still work and render exactly the same component — the packages above re-export them. They are deprecated and go in v3. Move whenever it suits you:
bunx blobatar-codemod .
bun add @blobatar/react # and/or @blobatar/vueanimate changes the rendering mode: a static blobatar is a single <img>, an
animated one is inline SVG. Use "hover" in a grid and "always" for the
single-blobatar case. Motion respects prefers-reduced-motion.
No install and no build step — a URL that renders one:
<img src="https://blobatar.dev/avatar/alain00?size=48" width="48" height="48" alt="">The path segment is the name, percent-encoded, and the query string is the
options under the names the library gives them — size (or s), background,
hue, tone, expression, title, gen:
https://blobatar.dev/avatar/alain%40example.com?size=64&background=squircle
https://blobatar.dev/avatar/team-rocket?expression=smug
Gravatar's d, f and r are accepted and ignored, so an existing Gravatar
URL becomes a blobatar by changing the host and nothing else:
- https://www.gravatar.com/avatar/<hash>?s=200&d=identicon
+ https://blobatar.dev/avatar/<hash>?s=200&d=identiconThe hash is used as the name. It is one-way, so the email cannot be recovered —
but it is itself derived from the email, so each person still gets one stable
blobatar of their own. It will not be the same one /avatar/<email> gives:
pick one scheme per application.
gen picks the shape vocabulary. New silhouettes cannot be added without
reshuffling existing ones — the thresholds partition a single range — so they
arrive as a new generation instead:
| silhouettes | |
|---|---|
gen=1 |
round, organic, boxy, nub, cloud, sun |
gen=2 |
…and capsule, triangle, hexagon, droplet |
An unversioned URL follows the current package major and now renders gen 2.
Pin ?gen=1 on existing URLs that must keep the original six shapes:
https://blobatar.dev/avatar/alain00?gen=1
https://blobatar.dev/avatar/alain00?gen=2
A generation that has appeared in a URL keeps answering. The list only ever
grows: gen=1 will not be retired, redirected to a newer vocabulary, or
answered with a placeholder, because a URL that renders someone's face is
usually written down somewhere its author no longer controls. That is what
naming a generation buys, and it is why a pinned URL earns a year-long
immutable cache rather than a day — it cannot come back different. The cost
falls on the endpoint, which depends on every frozen package major it still
serves; the library itself carries only the current one.
GET /avatar/ returns the whole parameter list as plain text, which is the
reference this section is a summary of.
blobatar.dev/avatar is free to use, but it is one small Worker rather than
something you have an agreement with, and it is rate-limited. If avatars are
load-bearing for you, run the endpoint yourself. The button clones
apps/api into your GitHub or GitLab account and deploys it to
your Cloudflare account — no configuration, no card, about a minute — and you
get:
https://blobatar-api.<your-subdomain>.workers.dev/avatar/<name>
Attach your own hostname afterwards in the Cloudflare dashboard, or as a
routes entry in wrangler.jsonc, and every push to your clone redeploys.
It stays inside the Workers free plan for anything short of real scale: 100,000 requests a day, and a blobatar costs 12µs of CPU against the 10ms allowed per request and 357 gzipped bytes to send. There is no database, no bucket and no state — the avatar is a pure function of the URL, which is also why every cache between you and it does the actual work.
Full docs — options table, guarantees, and how it works →
| Path | What it is |
|---|---|
packages/blobatar |
The library. Docs here. |
packages/cli |
The terminal surface, published as @blobatar/cli. |
apps/api |
The HTTP endpoint, deployable on its own. Serves /avatar/<name>. |
apps/site |
The landing page, plus apps/api behind blobatar.dev. |
apps/demo |
The tuning grid — the internal design tool, not a demo. |
bun install
bun dev # tuning grid → localhost:3001
bun site # landing page → localhost:3000
bun api # the endpoint → localhost:8787/avatar/alain
bun test # library tests
bun run check # tests + size budgets
bun run media # redraw the README images (needs Chrome + ImageMagick)CONTEXT.md is the glossary — worth two minutes before changing
anything, since shape and the name/seed split mean specific and
easily-confused things here. Architectural decisions live in docs/adr/.

