Skip to content
edmi-ui

Agent Avatar

5 by 5 pixel identicon generated from an agent id, tinted with a chart color.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
Keeper
Writer
Analyst
Engineer
Designer
Keeper
Writer
Analyst
Engineer
Designer
Keeper
Writer
Analyst
Engineer
Designer

Edmi ✦ addition. Built on plain SVG; no other Edmi items.

Terminal window
npx shadcn@latest add @edmi-ui/ai-agent-avatar
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/ai-agent-avatar
Terminal window
yarn dlx shadcn@latest add @edmi-ui/ai-agent-avatar
Terminal window
bunx shadcn@latest add @edmi-ui/ai-agent-avatar

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/ai-agent-avatar
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/ai-agent-avatar
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/ai-agent-avatar
Terminal window
bunx shadcn-vue@latest add @edmi-ui/ai-agent-avatar

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-agent-avatar.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-agent-avatar.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-agent-avatar.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-agent-avatar.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

import { AgentAvatar } from "@/components/ai/agent-avatar";
<div className="flex flex-col gap-6">
<div className="flex items-center gap-5">
</div>
<div className="flex items-center gap-3.5">
<AgentAvatar seed="keeper" size={20} />
<AgentAvatar seed="keeper" size={28} />
<AgentAvatar seed="keeper" size={40} />
<AgentAvatar seed="keeper" size={56} />
<AgentAvatar seed="keeper" size={28} tile={false} color="chart-2" />
</div>
</div>
<script setup lang="ts">
import { AgentAvatar } from "@/components/ai/agent-avatar";
</script>
<template>
<div class="flex flex-col gap-6">
<div class="flex items-center gap-5">
<div :key="name" class="flex flex-col items-center gap-1.5">
<AgentAvatar :seed="name.toLowerCase()" :label="name" />
<span class="text-xs text-muted-foreground">{{ name }}</span>
</div>
</div>
<div class="flex items-center gap-3.5">
<AgentAvatar seed="keeper" :size="20" />
<AgentAvatar seed="keeper" :size="28" />
<AgentAvatar seed="keeper" :size="40" />
<AgentAvatar seed="keeper" :size="56" />
<AgentAvatar seed="keeper" :size="28" :tile="false" color="chart-2" />
</div>
</div>
</template>
<script lang="ts">
import { AgentAvatar } from "$lib/components/ai/agent-avatar";
</script>
<div class="flex flex-col gap-6">
<div class="flex items-center gap-5">
{#each agents as name (name)}
<div class="flex flex-col items-center gap-1.5">
<AgentAvatar seed={name.toLowerCase()} label={name} />
<span class="text-xs text-muted-foreground">{name}</span>
</div>
{/each}
</div>
<div class="flex items-center gap-3.5">
<AgentAvatar seed="keeper" size={20} />
<AgentAvatar seed="keeper" size={28} />
<AgentAvatar seed="keeper" size={40} />
<AgentAvatar seed="keeper" size={56} />
<AgentAvatar seed="keeper" size={28} tile={false} color="chart-2" />
</div>
</div>
registry/components/ai/agent-avatar.tsx
"use client";
import { cn } from "cn";
import type { ComponentProps } from "react";
import { useMemo } from "react";
// FNV-1a hash + xorshift32: the same seed gives the same mark in every framework port.
function seedRandom(seed: string) {
let h = 2166136261;
for (let i = 0; i < seed.length; i++) {
h ^= seed.charCodeAt(i);
h = Math.imul(h, 16777619);
}
let s = h >>> 0 || 1;
return () => {
s ^= s << 13;
s >>>= 0;
s ^= s >>> 17;
s ^= s << 5;
s >>>= 0;
return s;
};
}
const OPACITIES = [0.45, 0.58, 0.71, 0.84, 0.97];
export type AgentAvatarCell = { x: number; y: number; opacity: number };
/** 5x5 mirrored grid: three columns are drawn, the outer two mirror them. */
export function agentAvatarCells(seed: string): {
cells: AgentAvatarCell[];
chart: number;
} {
const next = seedRandom(seed);
const chart = (next() % 5) + 1;
const cells: AgentAvatarCell[] = [];
for (let y = 0; y < 5; y++) {
for (let x = 0; x < 3; x++) {
const v = next() % 8;
if (v < 3) continue;
const opacity = OPACITIES[v - 3] ?? 0.97;
cells.push({ x, y, opacity });
if (x < 2) cells.push({ x: 4 - x, y, opacity });
}
}
return { cells, chart };
}
export type AgentAvatarProps = Omit<ComponentProps<"svg">, "color"> & {
/** Agent id: the same seed always draws the same mark. */
seed: string;
/** `chart-1` to `chart-5`, or any CSS color. Defaults to a chart token picked from the seed. */
color?: string;
/** Pixel size of the square. */
size?: number;
/** Muted rounded tile behind the mark (default). `false` draws the bare mark. */
tile?: boolean;
/** Accessible name; the mark is decorative when omitted. */
label?: string;
};
export function AgentAvatar({
seed,
color,
size = 40,
tile = true,
label,
className,
...props
}: AgentAvatarProps) {
const { cells, chart } = useMemo(() => agentAvatarCells(seed), [seed]);
const fill = color
? /^chart-[1-5]$/.test(color)
? `var(--${color})`
: color
: `var(--chart-${chart})`;
const inner = tile ? size * 0.62 : size;
const offset = tile ? size * 0.19 : 0;
const cell = inner * 0.17;
const step = (inner - cell) / 4;
return (
<svg
data-slot="ai-agent-avatar"
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
role={label ? "img" : undefined}
aria-label={label}
aria-hidden={label ? undefined : true}
className={cn("shrink-0", className)}
{...props}
>
{tile && (
<rect
x={0.5}
y={0.5}
width={size - 1}
height={size - 1}
rx={size * 0.28}
fill="var(--muted)"
stroke="var(--border)"
/>
)}
{cells.map((c) => (
<rect
key={`${c.x}-${c.y}`}
x={offset + c.x * step}
y={offset + c.y * step}
width={cell}
height={cell}
rx={cell * 0.17}
fill={fill}
opacity={c.opacity}
/>
))}
</svg>
);
}
registry/components/ai/agent-avatar/AgentAvatar.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { computed } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
import { agentAvatarCells } from "./identicon"
const props = withDefaults(defineProps<{
class?: HTMLAttributes["class"]
/** Agent id: the same seed always draws the same mark. */
seed: string
/** `chart-1` to `chart-5`, or any CSS color. Defaults to a chart token picked from the seed. */
color?: string
/** Pixel size of the square. */
size?: number
/** Muted rounded tile behind the mark (default). `false` draws the bare mark. */
tile?: boolean
/** Accessible name; the mark is decorative when omitted. */
label?: string
}>(), {
size: 40,
tile: true,
})
const data = computed(() => agentAvatarCells(props.seed))
const fill = computed(() => {
if (props.color)
return /^chart-[1-5]$/.test(props.color) ? `var(--${props.color})` : props.color
return `var(--chart-${data.value.chart})`
})
const inner = computed(() => (props.tile ? props.size * 0.62 : props.size))
const offset = computed(() => (props.tile ? props.size * 0.19 : 0))
const cell = computed(() => inner.value * 0.17)
const step = computed(() => (inner.value - cell.value) / 4)
</script>
<template>
<svg
data-slot="ai-agent-avatar"
:width="size"
:height="size"
:viewBox="`0 0 ${size} ${size}`"
:role="label ? 'img' : undefined"
:aria-label="label"
:aria-hidden="label ? undefined : true"
:class="cn('shrink-0', props.class)"
>
<rect
v-if="tile"
:x="0.5"
:y="0.5"
:width="size - 1"
:height="size - 1"
:rx="size * 0.28"
fill="var(--muted)"
stroke="var(--border)"
/>
<rect
v-for="c in data.cells"
:key="`${c.x}-${c.y}`"
:x="offset + c.x * step"
:y="offset + c.y * step"
:width="cell"
:height="cell"
:rx="cell * 0.17"
:fill="fill"
:opacity="c.opacity"
/>
</svg>
</template>
registry/components/ai/agent-avatar/identicon.ts
// FNV-1a hash + xorshift32: the same seed gives the same mark in every framework port.
function seedRandom(seed: string) {
let h = 2166136261;
for (let i = 0; i < seed.length; i++) {
h ^= seed.charCodeAt(i);
h = Math.imul(h, 16777619);
}
let s = h >>> 0 || 1;
return () => {
s ^= s << 13;
s >>>= 0;
s ^= s >>> 17;
s ^= s << 5;
s >>>= 0;
return s;
};
}
const OPACITIES = [0.45, 0.58, 0.71, 0.84, 0.97];
export type AgentAvatarCell = { x: number; y: number; opacity: number };
/** 5x5 mirrored grid: three columns are drawn, the outer two mirror them. */
export function agentAvatarCells(seed: string): {
cells: AgentAvatarCell[];
chart: number;
} {
const next = seedRandom(seed);
const chart = (next() % 5) + 1;
const cells: AgentAvatarCell[] = [];
for (let y = 0; y < 5; y++) {
for (let x = 0; x < 3; x++) {
const v = next() % 8;
if (v < 3) continue;
const opacity = OPACITIES[v - 3] ?? 0.97;
cells.push({ x, y, opacity });
if (x < 2) cells.push({ x: 4 - x, y, opacity });
}
}
return { cells, chart };
}
registry/components/ai/agent-avatar/index.ts
export { default as AgentAvatar } from "./AgentAvatar.vue";
export * from "./identicon";
src/lib/registry/ai/agent-avatar/agent-avatar.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { SVGAttributes } from "svelte/elements";
import { agentAvatarCells } from "./identicon.js";
let {
ref = $bindable(null),
seed,
color,
size = 40,
tile = true,
label,
class: className,
...restProps
}: Omit<SVGAttributes<SVGSVGElement>, "color"> & {
ref?: SVGSVGElement | null;
/** Agent id: the same seed always draws the same mark. */
seed: string;
/** `chart-1` to `chart-5`, or any CSS color. Defaults to a chart token picked from the seed. */
color?: string;
/** Pixel size of the square. */
size?: number;
/** Muted rounded tile behind the mark (default). `false` draws the bare mark. */
tile?: boolean;
/** Accessible name; the mark is decorative when omitted. */
label?: string;
} = $props();
const data = $derived(agentAvatarCells(seed));
const fill = $derived(
color
? /^chart-[1-5]$/.test(color)
? `var(--${color})`
: color
: `var(--chart-${data.chart})`
);
const inner = $derived(tile ? size * 0.62 : size);
const offset = $derived(tile ? size * 0.19 : 0);
const cell = $derived(inner * 0.17);
const step = $derived((inner - cell) / 4);
</script>
<svg
bind:this={ref}
data-slot="ai-agent-avatar"
width={size}
height={size}
viewBox="0 0 {size} {size}"
role={label ? "img" : undefined}
aria-label={label}
aria-hidden={label ? undefined : true}
class={cn("shrink-0", className)}
{...restProps}
>
{#if tile}
<rect
x={0.5}
y={0.5}
width={size - 1}
height={size - 1}
rx={size * 0.28}
fill="var(--muted)"
stroke="var(--border)"
/>
{/if}
{#each data.cells as c (`${c.x}-${c.y}`)}
<rect
x={offset + c.x * step}
y={offset + c.y * step}
width={cell}
height={cell}
rx={cell * 0.17}
{fill}
opacity={c.opacity}
/>
{/each}
</svg>
src/lib/registry/ai/agent-avatar/identicon.ts
// FNV-1a hash + xorshift32: the same seed gives the same mark in every framework port.
function seedRandom(seed: string) {
let h = 2166136261;
for (let i = 0; i < seed.length; i++) {
h ^= seed.charCodeAt(i);
h = Math.imul(h, 16777619);
}
let s = h >>> 0 || 1;
return () => {
s ^= s << 13;
s >>>= 0;
s ^= s >>> 17;
s ^= s << 5;
s >>>= 0;
return s;
};
}
const OPACITIES = [0.45, 0.58, 0.71, 0.84, 0.97];
export type AgentAvatarCell = { x: number; y: number; opacity: number };
/** 5x5 mirrored grid: three columns are drawn, the outer two mirror them. */
export function agentAvatarCells(seed: string): {
cells: AgentAvatarCell[];
chart: number;
} {
const next = seedRandom(seed);
const chart = (next() % 5) + 1;
const cells: AgentAvatarCell[] = [];
for (let y = 0; y < 5; y++) {
for (let x = 0; x < 3; x++) {
const v = next() % 8;
if (v < 3) continue;
const opacity = OPACITIES[v - 3] ?? 0.97;
cells.push({ x, y, opacity });
if (x < 2) cells.push({ x: 4 - x, y, opacity });
}
}
return { cells, chart };
}
src/lib/registry/ai/agent-avatar/index.ts
import AgentAvatar from "./agent-avatar.svelte";
export * from "./identicon.js";
export { AgentAvatar };
Part Prop Type Default
AgentAvatar seed string (agent id: same seed, same mark)
AgentAvatar color chart-1 to chart-5 or any CSS color picked from the seed
AgentAvatar size number (px) 40
AgentAvatar tile boolean (muted rounded square behind the mark) true
AgentAvatar label string (accessible name; decorative when omitted)

Flat only. It is a decorative mark with no depth. It is not the same component as the identicon of agent-card; each keeps its own look.

  • The mark is a 3 by 5 grid mirrored to 5 by 5, so it stays recognisable at 20px. Use it only where several agents appear.