For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to any URL or sending an Accept: text/markdown header. An agent skill is available at /.well-known/agent-skills/site-skill.md.
1
Support

BoringAvatar

Six locally generated avatar styles with profile photo fallback and no extra wrapper background.

Playground

Live preview
No extra wrapper background. Original SVG color fields remain. Photos take priority; missing or failed photos show the generated avatar. Reset restores all controls.
Customize BoringAvatar
Customize
64.00
boringavatar-demo.tsx
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:

Dependencies3 required packages
  • @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 cn

Usage

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.

boring-avatar-demo.tsx
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.

beam
marble
pixel
sunset
ring
bauhaus
boring-avatar-variants-demo.tsx
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.

boring-avatar-photo-demo.tsx
"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.

boring-avatar-palette-demo.tsx
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
boring-avatar-popover-demo.tsx
"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

Component props, types, defaults, and descriptions
PropType / DescriptionDefault
altstring

Accessible label for photo and fallback. Empty makes the avatar decorative.

""
classNamestring

Merged classes on the outer span, with no default background, border, or shadow.

Not set
colorsstring[]

Custom non-empty hexadecimal palette. Empty arrays use the upstream defaults. Playground offers default, warm, and cool presets.

Upstream five-color palette
namestring

Stable, case-sensitive identity. Generated locally without a network request or normalization.

Required
sizenumber

Outer width and height in pixels. Explicit style overrides size; preview starts at 64px.

40
squareboolean

Use square clipping for both SVG fallback and photo instead of a circle.

false
srcstring

Optional photo URL. Loading, missing, and failed photos show the generated fallback.

Not set
styleCSSProperties

Outer 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.