Add avatars in three steps
Pick your framework, install one package and render. Everything else on this page is optional.
Quick start
Install
npm i @inlaylab/opaline @inlaylab/opaline-react
Import a style
Styles are separate imports, so you ship only the ones you use. Swap
clayformorph,gradient,auraorinitials.import { Avatar } from '@inlaylab/opaline-react' import { clay } from '@inlaylab/opaline/clay'Render it
Pass anything stable as the seed: a user ID, email or name.
export function UserAvatar({ user }) { return <Avatar seed={user.email} look={clay} size={48} /> }Works in Server Components. Without a photo it ships no client code at all.
That's it. Design yours in the playground →
Install
npm i @inlaylab/opaline @inlaylab/opaline-vue
Import a style
Styles are separate imports, so you ship only the ones you use. Swap
clayformorph,gradient,auraorinitials.import { Avatar } from '@inlaylab/opaline-vue' import { clay } from '@inlaylab/opaline/clay'Render it
Pass anything stable as the seed: a user ID, email or name.
<script setup> import { Avatar } from '@inlaylab/opaline-vue' import { clay } from '@inlaylab/opaline/clay' defineProps(['user']) </script> <template> <Avatar :seed="user.email" :look="clay" :size="48" /> </template>SSR safe with Nuxt. Mounting several Vue apps on one page? Give each its own prefix: app.config.idPrefix = 'sidebar'.
That's it. Design yours in the playground →
Install
npm i @inlaylab/opaline @inlaylab/opaline-svelte
Import a style
Styles are separate imports, so you ship only the ones you use. Swap
clayformorph,gradient,auraorinitials.import { Avatar } from '@inlaylab/opaline-svelte' import { clay } from '@inlaylab/opaline/clay'Render it
Pass anything stable as the seed: a user ID, email or name.
<script> import { Avatar } from '@inlaylab/opaline-svelte' import { clay } from '@inlaylab/opaline/clay' let { user } = $props() </script> <Avatar seed={user.email} look={clay} size={48} />Svelte 5 with runes. SSR safe with SvelteKit.
That's it. Design yours in the playground →
Install
npm i @inlaylab/opaline @inlaylab/opaline-angular
Import a style
Styles are separate imports, so you ship only the ones you use. Swap
clayformorph,gradient,auraorinitials.import { OpalineAvatar } from '@inlaylab/opaline-angular' import { clay } from '@inlaylab/opaline/clay'Render it
Pass anything stable as the seed: a user ID, email or name.
import { Component, input } from '@angular/core' import { OpalineAvatar } from '@inlaylab/opaline-angular' import { clay } from '@inlaylab/opaline/clay' @Component({ selector: 'app-user-avatar', imports: [OpalineAvatar], template: `<op-avatar [seed]="email()" [look]="clay" [size]="48" />`, }) export class UserAvatar { readonly email = input.required<string>() protected readonly clay = clay }Standalone, zoneless and SSR safe, with signal inputs. Angular 20+.
That's it. Design yours in the playground →
Install
npm i @inlaylab/opaline-element
Import a style
Styles are separate imports, so you ship only the ones you use. Swap
clayformorph,gradient,auraorinitials.// once, anywhere in your app's JavaScript import '@inlaylab/opaline-element'
Render it
Pass anything stable as the seed: a user ID, email or name.
<opaline-avatar seed="ada@inlay.dev" look="clay" size="48"></opaline-avatar>
A plain custom element: works with any framework or none. Styles are picked by name with look.
That's it. Design yours in the playground →
Install
npm i @inlaylab/opaline
Import a style
Styles are separate imports, so you ship only the ones you use. Swap
clayformorph,gradient,auraorinitials.import { createAvatar } from '@inlaylab/opaline' import { clay } from '@inlaylab/opaline/clay'Render it
Pass anything stable as the seed: a user ID, email or name.
const { svg } = createAvatar(clay, { seed: user.email, size: 48 }) // svg is a plain string: put it in HTML, an email, or a response res.type('image/svg+xml').send(svg)Zero dependencies. Runs in Node, Deno, Bun, workers and browsers.
That's it. Design yours in the playground →
Styles
Five looks, one API. Each is its own import path: @inlaylab/opaline/<style>.
@inlaylab/opaline/clay@inlaylab/opaline/morph@inlaylab/opaline/gradient@inlaylab/opaline/aura@inlaylab/opaline/initialsOptions
Set an option to fix that part; the seed fills in the rest. An option set to its default gives exactly the same avatar as leaving it out.
| Option | Values | What it does |
|---|---|---|
seed | string · number · null | Any string: user ID, email, name. Emails are lower-cased; whitespace is trimmed. |
size | number | Pixel size. Default 64 (40 in groups). |
palette | vivid · pastel · electric · muted · warm · cool · mono | Color family. Default vivid. |
shape | circle · squircle · square | Every style. Aura and Initials default to circle; on the others it adds a tinted tile. |
finish | flat · soft · gloss | Light and shade. Morph and Gradient default to flat, Clay to soft. |
expression | beam · content · … · giggle | Fix the face. Or set mood: happy · calm · surprised · playful. |
face | boolean | Morph, Gradient and Clay show a face unless false. |
family | egg · duo · tri · cushion · clover · petal | Morph and Gradient blob shape. |
body · ears | jelly · hood · cat · bear · bunny | Clay only. Ears go with the hood. |
text | string | Initials only. Up to 3 letters, shown as given. |
backdrop | none · tint | A soft circle behind the body. |
src · alt | string | Components only: a photo shown once it loads, and an accessible name. |
members · max | array · number | Groups only: who to show, and how many before the +N chip. |
Photos and names
Pass src and the photo appears once it loads. If it fails, the generated avatar stays, so nobody sees a broken image. Pass alt to give the avatar an accessible name; without it, the avatar is decorative and hidden from screen readers.
<Avatar seed={user.email} look={clay} src={user.photoUrl} alt={user.name} /><Avatar :seed="user.email" :look="clay" :src="user.photoUrl" :alt="user.name" />
<Avatar seed={user.email} look={clay} src={user.photoUrl} alt={user.name} /><op-avatar [seed]="user.email" [look]="clay" [src]="user.photoUrl" [alt]="user.name" />
<opaline-avatar seed="ada@inlay.dev" look="clay" src="/photos/ada.jpg" alt="Ada Lovelace"></opaline-avatar>
// The core draws the avatar only. Show a photo on top of it in your own markup, // or use one of the components, which do this for you.
Initials and shapes
initials takes the letters from the seed (ada.lovelace@inlay.dev gives AL) or from text. The letters are a deep shade of the tile's color and clear WCAG AA contrast on every palette. Every style takes shape.
Avatar groups
Overlapping avatars with a +N chip for everyone past max. Set --avatar-ring to your background color; --avatar-more-bg and --avatar-more-fg color the chip.
import { AvatarGroup } from '@inlaylab/opaline-react'
<AvatarGroup
look={clay}
members={team.map((u) => ({ seed: u.email, src: u.photoUrl, alt: u.name }))}
max={4}
alt="Project members"
/><script setup>
import { AvatarGroup } from '@inlaylab/opaline-vue'
</script>
<template>
<AvatarGroup :look="clay" :members="team.map((u) => u.email)" :max="4" alt="Project members" />
</template><script>
import { AvatarGroup } from '@inlaylab/opaline-svelte'
</script>
<AvatarGroup look={clay} members={team.map((u) => u.email)} max={4} alt="Project members" />import { OpalineAvatarGroup } from '@inlaylab/opaline-angular'
// in the component's imports: [OpalineAvatarGroup]
<op-avatar-group [members]="emails" [look]="clay" max="4" alt="Project members" /><opaline-avatar-group look="clay" size="40" max="4" alt="Project members"> <opaline-avatar seed="ada@inlay.dev"></opaline-avatar> <opaline-avatar seed="grace@inlay.dev"></opaline-avatar> <opaline-avatar seed="linus@inlay.dev"></opaline-avatar> </opaline-avatar-group>
import { avatarGroup } from '@inlaylab/opaline'
const { shown, rest, css } = avatarGroup(team, { max: 4, size: 40 })
// shown: the avatars to draw, rest: the +N count, css: inline styles for each partPNG export
For places that take no SVG: profile photo uploads, social cards, push notifications. toPng renders in the browser with no dependencies. On the server, forRaster bakes the colors in for a rasterizer such as resvg. Pass vars to bake in theme colors.
import { toPng } from '@inlaylab/opaline/png'
const png = await toPng(createAvatar(clay, { seed }), { size: 512 })
// a Blob: upload it, or URL.createObjectURL(png) to show or download itimport { forRaster } from '@inlaylab/opaline/png'
import { Resvg } from '@resvg/resvg-js'
const svg = forRaster(createAvatar(clay, { seed }), { size: 512 })
const png = new Resvg(svg).render().asPng() // a BufferTheming
Colors come out as var(--avatar-c1, #hex). Set the variables on any parent to follow your brand or dark mode, with no re-render. --avatar-c1 is always the body; --avatar-fg colors initials.
Server rendering
Server and browser produce the same markup, so hydration never warns. Gradient IDs are scoped per seed and options, and each component instance adds its own suffix, so two avatars on one page never clash. The repo has tested samples for Next (App Router), Nuxt, SvelteKit and Angular SSR.
Same seed, same avatar
The seed is normalized (trimmed, NFC, emails lower-cased) and hashed into a seeded generator: no Math.random, no dates. The same seed and options give a byte-identical SVG across 1.x, checked by checksums in CI, so you can cache avatars on a CDN or store them.
Not sure Opaline is the right fit? See how it compares with DiceBear and Boring Avatars →