- Accordion
- AppShell
- Autocomplete
- Badge
- Blobatar
- BoringAvatar
- Button
- Calendar
- Checkbox
- DataGrid
- Date Picker
- Date Range Picker
- DateTime
- Dialog
- Drawer
- Dropdown
- Empty
- Error State
- Frame
- Input
- Input Amount
- Input Group
- Input OTP
- Input Password
- Input Phone
- Input Search
- Input Secret
- Loading
- PageContainer
- PageHeader
- Popover
- Select
- StatsCard
- Switch
- Tabs
- TextArea
- Toast
- Tooltip
Playground
import { BoringAvatar } from "@/components/ui/boring-avatar";
export function Demo() {
return (
<BoringAvatar
alt={"Vandor"}
name={"vandor"}
size={64}
square={false}
variant={"beam"}
/>
);
}Installation
Initialize shadcn in your React project, then install this component from the Vandor UI registry:
$ pnpm dlx shadcn@latest add https://vandor-ui.vercel.app/r/boring-avatar.json
Dependencies
Installed automatically by the registry CLI. For manual setup:
- @base-ui/react@^1.8.0
- boring-avatars@^2.0.4
- cn
$ pnpm add @base-ui/react@^1.8.0 boring-avatars@^2.0.4 cnUsage
import { BoringAvatar } from "@/components/ui/boring-avatar";
export function Demo() {
return <BoringAvatar name="vandor" alt="Vandor" size={48} />;
}The wrapper adds no background, border, or shadow. The original SVG artwork still contains its colored fields and backdrop: this is not a transparent-silhouette renderer. Unlike Blobatar, there is no additional backdrop option, animation, expression control, or pointer tracking.
Generation runs locally through boring-avatars, without an avatar-service request. Names are case-sensitive and are not trimmed or normalized. Prefer a stable public ID over sensitive personal data. Identical names, variants, and palettes produce identical artwork within the installed generator version; upgrades may change the mapping.
import { BoringAvatar } from "@/components/ui/boring-avatar";
export const BoringAvatarDemo = () => (
<div className="flex flex-wrap items-center gap-3">
{["vandor", "ada", "luna", "mars"].map((name) => (
<BoringAvatar key={name} name={name} alt={name} size={48} />
))}
</div>
);Requires React 19, Tailwind CSS 4, Base UI 1.8+, boring-avatars 2.0.4+, and cn. Installation adds only the wrapper and declared packages, not a separate Avatar component. No extra stylesheet or theme tokens are required for the avatar itself. Source can be copied manually after installing these packages.
Variants
beam is the default face style. marble, pixel, sunset, ring, and bauhaus expose the original generator artwork. Change the variant in the playground to inspect the same identity in all six styles.
import { BoringAvatar } from "@/components/ui/boring-avatar";
export const BoringAvatarVariantsDemo = () => (
<div className="flex flex-wrap justify-center gap-5">
{(["beam", "marble", "pixel", "sunset", "ring", "bauhaus"] as const).map(
(variant) => (
<div key={variant} className="flex flex-col items-center gap-2">
<BoringAvatar
name="vandor"
alt={`Vandor, ${variant}`}
variant={variant}
size={56}
/>
<span className="text-sm text-muted-foreground">{variant}</span>
</div>
)
)}
</div>
);Profile Photos
Pass src for a profile photo. The generated fallback stays visible during loading and after a failure. A loaded photo replaces it. Changing src starts the new image lifecycle; removing it restores the generated avatar. alt labels whichever image is visible.
The generated avatar is the fallback.
"use client";
import { useState } from "react";
import { BoringAvatar } from "@/components/ui/boring-avatar";
import { Button } from "@/components/ui/button";
const photo =
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3Crect width='100' height='100' fill='%23d9e4d5'/%3E%3Ccircle cx='50' cy='50' r='25' fill='%23596d51'/%3E%3C/svg%3E";
const sources = {
failed: "data:image/png;base64,invalid",
generated: undefined,
photo,
};
export const BoringAvatarPhotoDemo = () => {
const [mode, setMode] = useState<keyof typeof sources>("generated");
return (
<div className="flex flex-col items-center gap-4">
<BoringAvatar name="vandor" alt="Vandor" src={sources[mode]} size={80} />
<div
className="flex flex-wrap justify-center gap-2"
role="group"
aria-label="Photo scenario"
>
{(["generated", "photo", "failed"] as const).map((value) => (
<Button
key={value}
variant="outline"
size="sm"
aria-pressed={mode === value}
onClick={() => setMode(value)}
>
{
{ failed: "Failed photo", generated: "No photo", photo: "Photo" }[
value
]
}
</Button>
))}
</div>
<p className="text-sm text-muted-foreground" role="status">
{mode === "photo"
? "Photo replaces the generated avatar once loaded."
: "The generated avatar is the fallback."}
</p>
</div>
);
};Palette and Shape
colors accepts a non-empty array of hexadecimal colors. Omit it or pass an empty array to use the upstream palette: #92A1C6, #146A7C, #F0AB3D, #C271B4, and #C20D90. Beam chooses dark or light facial features based on the body color, so use six-digit hex colors for predictable contrast. The playground provides default, warm, and cool presets.
square affects both the SVG clipping and profile photo. By default both are circular. size controls the wrapper; explicit style dimensions override it and the artwork fills the wrapper.
import { BoringAvatar } from "@/components/ui/boring-avatar";
const colors = ["#F2D7B6", "#B55A30", "#E8A652", "#783F51", "#42665E"];
export const BoringAvatarPaletteDemo = () => (
<div className="flex flex-wrap items-center gap-4">
<BoringAvatar name="ada" alt="Ada, round" colors={colors} size={64} />
<BoringAvatar
name="ada"
alt="Ada, square"
colors={colors}
square
size={64}
/>
</div>
);Actionable Avatar
Wrap the avatar in a real button or link for actions. Here the button labels the action and the avatar is decorative to avoid a repeated accessible label. The trigger retains a minimum 44px hit area and a visible keyboard focus ring. Escape or an outside click closes the popover.
Install Popover separately for this example:
$ pnpm dlx shadcn@latest add https://vandor-ui.vercel.app/r/boring-avatar.json https://vandor-ui.vercel.app/r/popover.json
"use client";
import { BoringAvatar } from "@/components/ui/boring-avatar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
export const BoringAvatarPopoverDemo = () => (
<Popover>
<PopoverTrigger
render={
<button
type="button"
aria-label="Open Vandor profile"
className="inline-flex min-h-11 min-w-11 cursor-pointer items-center justify-center rounded-full focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ring"
>
<BoringAvatar name="vandor" size={64} />
</button>
}
/>
<PopoverContent className="w-64 max-w-[calc(100vw-2rem)]">
<p className="font-medium">Vandor</p>
<p className="text-sm text-muted-foreground">Product designer</p>
</PopoverContent>
</Popover>
);The same button composition works with Dropdown, Dialog, or Drawer. Those overlays are not BoringAvatar dependencies and must be installed separately.
Behavior and Accessibility
alt defaults to "", making the avatar decorative when a name is already visible beside it. Provide an accessible label for a standalone avatar. The generated SVG uses that label rather than exposing the seed as a title. A photo uses the same alternative text.
Native span attributes, handlers, styles, and React 19 refs target the outer span. Children and Base UI's render prop are not exposed. The avatar itself is not interactive, focusable, or a status indicator. Announce presence and other meaningful states separately in text. All artwork is static, including under reduced motion.
SVG generation requires no remote request or data: image permission. Profile URLs still need allowed CSP origins; the example's inline photo requires data: in img-src. Custom palettes must remain legible in both themes.
Portable Storybook Stories
Install BoringAvatar first, then optionally install its stories:
$ pnpm dlx shadcn@latest add https://vandor-ui.vercel.app/r/boring-avatar-stories.json
Stories import the colocated ./boring-avatar. The stories item does not reinstall the component, install Storybook packages, or change .storybook/. Use matching Storybook React packages in your existing setup (types checked against @storybook/react 10.6.1), React 19, Tailwind CSS 4, and your consumer theme. Include **/*.stories.tsx under your configured UI directory in your stories glob. No extra CSS or optional addons are required.
Ordinary installation includes neither stories nor Storybook dependencies. Storybook UI is not verified here because this project has no Storybook server.
Props
| Prop | Type / Description | Default |
|---|---|---|
| alt | stringAccessible label for photo and fallback. Empty makes the avatar decorative. | "" |
| className | stringMerged classes on the outer span, with no default background, border, or shadow. | Not set |
| colors | string[]Custom non-empty hexadecimal palette. Empty arrays use the upstream defaults. Playground offers default, warm, and cool presets. | Upstream five-color palette |
| name | stringStable, case-sensitive identity. Generated locally without a network request or normalization. | Required |
| size | numberOuter width and height in pixels. Explicit style overrides size; preview starts at 64px. | 40 |
| square | booleanUse square clipping for both SVG fallback and photo instead of a circle. | false |
| src | stringOptional photo URL. Loading, missing, and failed photos show the generated fallback. | Not set |
| style | CSSPropertiesOuter span styles, applied after size. Native span attributes, event handlers, and React 19 refs are supported; children are not. | Not set |
| variant | "beam" | "marble" | "pixel" | "sunset" | "ring" | "bauhaus"Original Boring Avatars artwork. The wrapper adds no background; SVG color fields remain part of each variant. | "beam" |
Native span attributes, handlers, style, and React 19 refs target the outer span. Children and Base UI render composition are not exposed.
Source
"use client";
import { Avatar } from "@base-ui/react/avatar";
import GeneratedAvatar from "boring-avatars";
import { cn } from "cn";
import type * as React from "react";
export type BoringAvatarVariant =
| "beam"
| "marble"
| "pixel"
| "sunset"
| "ring"
| "bauhaus";
export type BoringAvatarProps = Omit<
React.ComponentProps<"span">,
"children"
> & {
/** Stable, case-sensitive identity for local generation. */
name: string;
/** Optional photo; loading and failed photos show the generated fallback. */
src?: string;
/** Shared accessible label. Empty means decorative. */
alt?: string;
/** Pixel dimensions. Explicit style overrides these. */
size?: number;
variant?: BoringAvatarVariant;
/** Square clipping for both the generated SVG and the photo. */
square?: boolean;
/** Non-empty palette of hexadecimal colors; empty uses upstream defaults. */
colors?: string[];
};
export const BoringAvatar = ({
name,
src,
alt = "",
size = 40,
variant = "beam",
square = false,
colors,
className,
style,
...props
}: BoringAvatarProps) => (
<Avatar.Root
data-slot="boring-avatar"
className={cn("relative inline-flex shrink-0 align-middle", className)}
style={{ height: size, width: size, ...style }}
{...props}
>
<Avatar.Fallback
data-slot="boring-avatar-fallback"
className="flex size-full"
>
<GeneratedAvatar
name={name}
variant={variant}
colors={colors?.length ? colors : undefined}
square={square}
size="100%"
title={false}
role={alt ? "img" : undefined}
aria-label={alt || undefined}
aria-hidden={alt ? undefined : true}
focusable="false"
className="size-full"
/>
</Avatar.Fallback>
{src ? (
<Avatar.Image
data-slot="boring-avatar-image"
src={src}
alt={alt}
keepMounted
className={cn(
"absolute inset-0 size-full object-cover data-[loading]:invisible data-[error]:invisible",
!square && "rounded-full"
)}
/>
) : null}
</Avatar.Root>
);
Credits
- Boring Avatars by Boring Designers: MIT-licensed SVG generator used as a dependency. Vandor UI adds Base UI photo/fallback composition, accessible labeling, sizing, playground, and portable stories.