# Blobatar

Locally generated geometric avatars with profile photo fallback and opt-in motion.

> For the complete documentation index, see [llms.txt](/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](/.well-known/agent-skills/site-skill.md).





## Playground [#playground]

```tsx
"use client";

import { Blobatar } from "@/components/ui/blobatar";

export function Demo() {
  return (
    <Blobatar
      name={"vandor"}
      alt={"vandor"}
      size={64}
    />
  );
}
```

## Installation [#installation]

Initialize shadcn in your React project, then install this component from the Vandor UI registry:

```bash
npx shadcn@latest add https://vandor-ui.vercel.app/r/blobatar.json
```

## Dependencies [#dependencies]

Installed automatically by the registry CLI. For manual setup:

```bash
npm install @base-ui/react@^1.8.0 blobatar@^2.7.0 @blobatar/react@^2.7.0 cn
```

## Usage [#usage]

```tsx
import { Blobatar } from "@/components/ui/blobatar";

export function Demo() {
  return <Blobatar name="vandor" alt="Vandor" size={48} />;
}
```

The `name` can be a username, ID, email, or team name. Generation runs locally, with no call to blobatar.dev. Prefer a stable public ID when names could expose personal information in rendered markup. Names are NFC-normalized, trimmed, and lowercased unless you pass `blobatar={{ normalize: false }}`.

<ComponentPreview name="blobatar-demo" title="blobatar-demo.tsx">
  <BlobatarDemo />
</ComponentPreview>

Installation adds only this wrapper and its declared packages. It uses Base UI Avatar directly; it does not install or overwrite a separate Avatar component. Requires React 19, Tailwind CSS 4, and Base UI 1.8+. The generated face has no site-specific theme token requirements.

## Overlay Compositions [#overlay-compositions]

Choose **Composition** in the playground to try the avatar as a Popover, Dropdown, Dialog, or Drawer trigger. Appearance, photos, expressions, and pointer tracking still apply; copied code includes the selected overlay. Composition is a playground setting, not a Blobatar prop.

Install only the overlay you need alongside Blobatar, for example:

```bash
npx shadcn@latest add https://vandor-ui.vercel.app/r/blobatar.json https://vandor-ui.vercel.app/r/popover.json
```

Replace `popover.json` with `dropdown.json`, `dialog.json`, or `drawer.json` for the other examples. Blobatar itself does not depend on these overlays.

### With Popover [#with-popover]

Click the avatar for a compact profile summary. Escape or an outside click dismisses it.

<ComponentPreview name="blobatar-popover-demo" title="blobatar-popover-demo.tsx">
  <BlobatarPopoverDemo size="64" />
</ComponentPreview>

### With Dropdown [#with-dropdown]

Open the account menu and choose a status. The selection updates the text below the avatar; use arrow keys to navigate the menu.

<ComponentPreview name="blobatar-dropdown-demo" title="blobatar-dropdown-demo.tsx">
  <BlobatarDropdownDemo size="64" />
</ComponentPreview>

### With Dialog [#with-dialog]

Use a dialog for focused profile details. Focus stays inside until it closes, then returns to the avatar button.

<ComponentPreview name="blobatar-dialog-demo" title="blobatar-dialog-demo.tsx">
  <BlobatarDialogDemo size="64" />
</ComponentPreview>

### With Drawer [#with-drawer]

A drawer keeps the profile in a side panel. Use its close button, Escape, or swipe toward the right edge on touch.

<ComponentPreview name="blobatar-drawer-demo" title="blobatar-drawer-demo.tsx">
  <BlobatarDrawerDemo size="64" />
</ComponentPreview>

Each trigger is a real button with an accessible name and a visible keyboard focus ring. The avatar inside is decorative (`alt=""`) so its label is not repeated. Triggers keep a minimum 44px hit area even for small avatars.

## Profile Photos [#profile-photos]

Pass `src` when a profile photo exists. The generated fallback is visible while the photo loads and after a load error. Once loaded, the round photo replaces the fallback. Changing `src` lets Base UI resolve the new image; removing it restores the fallback.

<ComponentPreview name="blobatar-photo-demo" title="blobatar-photo-demo.tsx">
  <BlobatarPhotoDemo />
</ComponentPreview>

## Motion and Expressions [#motion-and-expressions]

Animation is off by default. Static faces use a single `<img>` with a local SVG data URI. `animate: "hover"` or `"always"` switches the fallback to inline SVG. Import the package stylesheet once in your application's global entry or the animated component:

```tsx
import "blobatar/motion.css";
import { happy } from "blobatar/expression";
import { Blobatar } from "@/components/ui/blobatar";

<Blobatar name="vandor" blobatar={{ animate: "hover", expression: happy }} />;
```

Expressions are imported **values**, not strings. The upstream roster includes idle, happy, sad, mad, surprised, wink, sleepy, smug, unsure, scared, love, shy, sick, and thinking. Poses remain until changed. A pose works without `animate`; morphing between poses requires inline SVG. Thinking's eye cycle also requires `blobatar/motion.css`.

<ComponentPreview name="blobatar-motion-demo" title="blobatar-motion-demo.tsx">
  <BlobatarMotionDemo />
</ComponentPreview>

Prefer static faces for long lists and hover motion for small grids. Reserve always-on motion for a single prominent avatar. Upstream motion becomes static under `prefers-reduced-motion` and hover motion does not activate on touch.

## Following the Mouse [#following-the-mouse]

Enable `followPointer` for eyes that follow your mouse anywhere on the page. This uses the official `useGaze` hook, automatically switches the fallback to inline SVG with always-on motion, and requires both stylesheets:

```tsx
import "blobatar/motion.css";
import "blobatar/gaze.css";
import { Blobatar } from "@/components/ui/blobatar";

<Blobatar name="vandor" size={128} followPointer pointerTravel={3} />;
```

<ComponentPreview name="blobatar-pointer-demo" title="blobatar-pointer-demo.tsx">
  <BlobatarPointerDemo />
</ComponentPreview>

`pointerTravel` defaults to 3, measured in SVG viewBox units (the face is 100 units wide), not screen pixels. The playground offers 1–4. It is easier to see at 96–128px than at list-avatar sizes. `followPointer` takes precedence over `blobatar.animate`, including `false`. Disabling it restores the configured animation mode.

Tracking attaches only for a fine pointer with hover support and without reduced motion. It stops on unmount, while a loaded photo replaces the fallback, and when those media preferences change. Photos do not track the pointer. Importing this wrapper includes the upstream gaze driver, but no pointer tracking listeners attach unless `followPointer` is enabled. The public `ref` still targets the outer span.

## Appearance [#appearance]

Pass generator options through `blobatar`. The fallback defaults to a full-frame `"circle"` backdrop, matching the photo's occupied area. The upstream body sits inside its SVG viewBox with intentional breathing room; this is not CSS padding. The backdrop fills that room without cropping the body or its motion. Set `background: false` for the original transparent silhouette. `background` also accepts `"square"` and `"squircle"`. Hue, tone, traits, palette, normalize, and contrast are forwarded to the official renderer:

```tsx
<Blobatar name="vandor" blobatar={{ hue: 220, background: "squircle" }} />
```

The outer `size` controls both photo and fallback. Override dimensions through `style`; `className` merges on the outer span. Photo styling is fixed to a circle; fallback backdrop configuration does not change the photo. Body shapes are generated from the name, not the backdrop.

Identical names produce identical faces within a package major version. Registry dependencies stay on major 2; upgrading to a new major may change the name-to-face mapping. Custom palette colors bypass upstream's eye/body contrast guarantee. See the [upstream API](https://blobatar.dev/llms.txt) for traits and palette configuration.

## Behavior and Accessibility [#behavior-and-accessibility]

`alt` defaults to an empty string, making the face decorative when a name is already written beside it. Pass `alt="Person's name"` for a standalone avatar. The label is used for both the photo's alternative text and the generated SVG title. Expressions are decorative: announce loading or presence separately in real text, not through facial poses.

Native span attributes, event handlers, style, and the public `ref` target the outer span. There is no internal click behavior or keyboard control. For an actionable avatar, wrap it in an accessible button or link rather than adding an unlabeled click handler. Children and Base UI's render-composition API are not exposed.

The local SVG data URI requires your Content Security Policy to allow `data:` in `img-src`. Photo URLs need their own allowed origins. Animation CSS is an explicit consumer setup step; copying Source alone will not enable motion.

## Portable Storybook Stories [#portable-storybook-stories]

Install Blobatar first, then optionally install stories without replacing your customized component:

```bash
npx shadcn@latest add https://vandor-ui.vercel.app/r/blobatar-stories.json
```

The stories import the colocated `./blobatar` component. They do not install Storybook packages or change `.storybook/`. Supply your existing matching Storybook React packages (types checked against `@storybook/react` 10.6.1), React 19, Tailwind CSS 4, and your consumer theme. Include `**/*.stories.tsx` under your UI directory in the Storybook stories glob. Import `blobatar/motion.css` in `.storybook/preview.ts` to enable the motion stories, plus `blobatar/gaze.css` for PointerTracking. No optional addons are required.

Ordinary Blobatar installation includes neither stories nor Storybook dependencies. Storybook UI is not verified here because this project has no Storybook server.

## Props [#props]

| Prop                | Type                                        | Default  | Description                                                                                                                                                             |
| ------------------- | ------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| alt                 | string                                      | ""       | Accessible name shared by photo and fallback. Empty means decorative; pass a label when the avatar stands alone.                                                        |
| blobatar            | BlobatarAppearance                          | Not set  | Appearance options: animate, expression, background, hue, tone, traits, palette, normalize, and contrast. size and title are owned by the wrapper's size and alt props. |
| blobatar.animate    | false \| "hover" \| "always"                | false    | Opt-in inline SVG motion. Follow pointer overrides this to always. Requires blobatar/motion.css and respects reduced motion.                                            |
| blobatar.background | false \| "circle" \| "square" \| "squircle" | "circle" | Full-frame circle by default, matching a photo. Choose none for the upstream transparent silhouette.                                                                    |
| blobatar.expression | Expression                                  | idle     | Expression value imported from blobatar/expression, not a string. The playground shows four of fourteen upstream poses.                                                 |
| className           | string                                      | Not set  | Merged classes on the outer span. Use style to override its pixel dimensions.                                                                                           |
| followPointer       | boolean                                     | false    | Eyes follow a fine mouse pointer. Forces inline SVG with always motion; requires motion.css and gaze.css. Inert on touch and under reduced motion.                      |
| name                | string                                      | Required | Stable identity for local generation. NFC-normalized, trimmed, and lowercased by default.                                                                               |
| pointerTravel       | number                                      | 3        | Eye movement range in SVG viewBox units. Only used when followPointer is enabled.                                                                                       |
| size                | number                                      | 40       | Wrapper width and height in pixels. Explicit style overrides these dimensions; the playground starts at 64px.                                                           |
| src                 | string                                      | Not set  | Profile photo URL. Loading, missing, and failed photos show the generated fallback.                                                                                     |
| style               | CSSProperties                               | Not set  | Outer span styles, applied after size. Native span attributes, events, and ref are supported; children are not.                                                         |

The reference lists component-specific props and selected native props, not the complete React API. Other native attributes are forwarded to the rendered element.

## Source [#source]

```tsx
"use client";

import { Avatar } from "@base-ui/react/avatar";
import { Blobatar as GeneratedBlobatar } from "@blobatar/react";
import { useGaze } from "@blobatar/react/gaze";
import type { BlobatarOptions } from "blobatar";
import { cn } from "cn";
import type * as React from "react";

export type BlobatarAppearance = Omit<
  BlobatarOptions,
  "size" | "title" | "animate"
> & {
  animate?: false | "hover" | "always";
};

export type BlobatarProps = Omit<React.ComponentProps<"span">, "children"> & {
  /** Stable identity used to generate the fallback locally. */
  name: string;
  /** Optional profile photo. Loading and failed photos show the fallback. */
  src?: string;
  /** Accessible label for both photo and fallback. Empty means decorative. */
  alt?: string;
  /** Width and height in pixels. Explicit style can override them. */
  size?: number;
  /** Follow a fine pointer. Requires motion.css and gaze.css; forces inline SVG. */
  followPointer?: boolean;
  /** Eye excursion in SVG viewBox units, not screen pixels. */
  pointerTravel?: number;
  /** Generator options. Motion requires blobatar/motion.css. */
  blobatar?: BlobatarAppearance;
};

export const Blobatar = ({
  name,
  src,
  alt = "",
  size = 40,
  followPointer = false,
  pointerTravel = 3,
  blobatar,
  className,
  style,
  ...props
}: BlobatarProps) => {
  const { ref: gazeRef } = useGaze({
    lookAt: followPointer ? "pointer" : null,
    travel: pointerTravel,
  });
  return (
    <Avatar.Root
      data-slot="blobatar"
      className={cn("relative inline-flex shrink-0 align-middle", className)}
      style={{ height: size, width: size, ...style }}
      {...props}
    >
      {src ? (
        <Avatar.Image
          data-slot="blobatar-image"
          src={src}
          alt={alt}
          keepMounted
          className="absolute inset-0 size-full rounded-full object-cover data-[loading]:invisible data-[error]:invisible"
        />
      ) : null}
      <Avatar.Fallback data-slot="blobatar-fallback" className="flex size-full">
        <GeneratedBlobatar
          background="circle"
          {...blobatar}
          animate={followPointer ? "always" : blobatar?.animate || undefined}
          ref={followPointer ? gazeRef : undefined}
          name={name}
          title={alt || undefined}
          className="size-full"
        />
      </Avatar.Fallback>
    </Avatar.Root>
  );
};

```

## Credits [#credits]

* [Blobatar by Alain](https://github.com/Alain00/blobatar): MIT-licensed generator and React adapter used as dependencies. Vandor UI adds a Base UI photo/fallback composition, sizing, playground, and portable stories.
