ProductBrand
Join us
Foundations
  • Introduction
  • Colors
  • Typography
  • Icons
  • Illustrations
  • Logos
  • Shadows
Components
  • Accordion
  • Alert Dialog
  • Avatar
  • Badge
  • Button
  • Calendar
  • Card
  • Chart
  • Checkbox
  • Collapsible
  • Combobox
  • Context Menu
  • Date Picker
  • Dialog
  • Drawer
  • Dropdown Menu
  • Fluid Avatar
  • Hover Scroll Text
  • Input
  • Kbd
  • Popover
  • Progress Bar
  • Radio Group
  • Scroll Area
  • Select
  • Selectors
  • Separator
  • Separator Dot
  • Shimmer Text
  • Skeleton
  • Slider
  • Switch
  • Tabs
  • Textarea
  • Toast
  • Tooltip
Compositions
  • Empty State
  • File Attachment
  • Floating Bar
  • Input Banner
  • Markdown Editor
  • Media Controls
  • Product Feature Banner
  • Panels
  • Sidebar
  • Reasoning Trace
  • Pagination
  • Data Table
  • Breadcrumb
  • Thread Outline
Inline
  • Overview
  • Stat Tile
  • Metric Row
  • Comparison Table
  • Chart Card
  • Trend Tile
  • Cite
  • Custom
  • Agent skill

Fluid Avatar

A WebGL identity mark whose texture drifts like suspended dust.

Examples

The full set

From an image

The WebGL fallback

forceFallback renders the fallback branch on a working GPU, which is otherwise unreachable without disabling hardware acceleration.

The fallback above is deliberately the hardest shape to contain: an image roughly eleven times wider than the avatar, absolutely positioned so that only one window of it lands in the circle. Showing a chosen region of a larger image is the normal way to build one, so the frame is a positioned clip. That matters because overflow: hidden only clips an absolutely positioned descendant when the clipping element is also its containing block. On a static wrapper the image ignores the clip entirely and paints across whatever surrounds the avatar.

Overview

The avatar maps a generated texture onto the inside of a glass ball and refracts it at IOR 1.52. Motion comes from drifting where the texture is sampled, not from blurring it: the whole field rotates slowly, a mid-scale swirl churns underneath, and two decorrelated noise fields pull neighbouring grains apart at different rates. The grain stays sharp throughout, which is what reads as suspended dust rather than a sliding image.

Reflection is deliberately restrained: a lit bevel, a thin rim line, one soft specular. It should read as its own contents distorting, not as a mirror.

tsx
import { FluidAvatar } from "@rogo-technologies/ui/fluid-avatar";

<FluidAvatar
  source={{ kind: "generated", variant: 3 }}
  tint="#1C6182"
  size={48}
  fallback={<div className="size-full" style={{ background: "#1C6182" }} />}
/>;

fallback is part of the minimum shape, not a refinement to add later. Every example on this page passes one, because an avatar without it renders as an empty circle for anyone whose browser refuses WebGL, and that is invisible to whoever wrote it.

The twelve variants above are the set the agent library uses: an agent's id hashes to a variant, and its configured color becomes the tint.

Usage

Source

source is a discriminated union, so an avatar always has exactly one well-defined source.

  • { kind: "generated", variant: number }: the texture is drawn on the GPU from variant alone. No network request, no image decode, nothing to 404. Each variant maps to a different region, zoom and streak angle of a procedural noise field.
  • { kind: "image", src: string }: refracts a supplied image instead. Same-origin, or CORS-enabled. Detail survives the refraction, so a source with real texture reads better than a flat one. Images are decoded off the main thread and resampled once into the same mipmapped power-of-two texture generated sources use, so after the one-time fetch the two kinds perform identically.

Prefer generated. It costs one GPU pass the first time a variant appears, then nothing.

Tint

tint accepts any CSS color and applies it the way the CSS color blend mode does: hue and saturation from the tint, luminosity from the texture. It recolors without darkening, so no tint can turn an avatar into a black disc. Unparseable values are ignored rather than rendering as black.

Fallback

Pass a fallback every time. The renderer swaps to it when WebGL is unavailable, when the context is lost, and on machines whose GPU is blocklisted, none of which you can see from a working machine. Match the source you were going to render:

  • Generated source: a disc in the same tint. The identity signal is the color, so a flat disc still reads as the right agent. <div className="size-full" style={{ background: tint }} />
  • Image source: the same image, object-cover. That matches how the shader crops it, so the fallback frames the same region. <img src={src} alt="" className="size-full object-cover" />
  • A window onto a larger image: position it absolutely, as in the fallback example above. The frame clips it to the circle.

Contents are laid out against a relative frame that is overflow: hidden and fully rounded, so size-full fills the circle and absolutely positioned children are cropped by it. Verify with forceFallback rather than assuming.

Size and chrome

size is the diameter in CSS pixels (default 120). It works down to 16px: generated textures are mipmapped, and the rim is held to a few device pixels rather than a fraction of the radius, so the lit edge stays equally crisp at every diameter.

  • ring (default on) draws the shadow-ring-xs hairline that separates the avatar from the surface. It sits on an overlay rather than a radius on the canvas, so the shader's own antialiased edge isn't softened by a second clip.
  • contactShadow (default on) adds a soft ellipse underneath, which makes the container 10% taller than it is wide. Turn it off in avatar slots so the avatar stays square.

Performance

Every avatar on the page shares one WebGL context and one animation frame loop, held in a module-level renderer. Each instance owns a cheap 2D canvas that the shared context blits into. Browsers cap a document at roughly 16 live WebGL contexts, so one context per avatar would start silently dropping them on any directory page.

The render is a single texture sample and four noise evaluations per pixel, and there are no blur kernels, which is what keeps the grain sharp and the cost low enough for lists. Avatars that scroll out of view stop rendering, the loop pauses when the tab is hidden, and the loop is capped at 30fps because the drift is far too slow to read as smoother above that.

Avatars agreeing on source, tint, size and pointer paint identical pixels, so they are drawn once and the result blitted to each canvas. A directory of same-coloured agents costs one draw per frame, not one per row.

Generated textures are cached per variant, and a frame generates at most one of them: the source pass is a megapixel of layered noise, so a directory that introduces a dozen variants at once would otherwise stall its first frame. The cache is capped, evicting the textures no mounted avatar is still sampling.

Accessibility

  • Honors prefers-reduced-motion: the renderer paints a single still frame and never starts the loop. The avatar keeps its full look, it just doesn't drift. Toggling the OS setting repaints live, no reload needed.
  • Avatars are hidden from assistive tech by default. Pass label when one carries meaning rather than decoration, which renders it as role="img" with that accessible name.
  • If WebGL is unavailable or the context is lost, every mounted avatar swaps to fallback inside a circular frame. Pass something, since otherwise it renders as an empty circle on machines without WebGL. Avatars swap back on their own once the GPU returns, which is what a backgrounded tab on iOS Safari looks like from here.
  • The circular frame is a positioned clip, so a fallback that positions oversized content against it (an off-center texture crop, say) is cropped to the circle rather than painting over the surrounding layout.
  • Machines with no usable GPU render the fallback too. The renderer refuses a software-rasterized context rather than accept one, where the source pass would take seconds and freeze the tab.

API

PropTypeDefaultDescription
sourceFluidAvatarSource-{ kind: "generated", variant } or { kind: "image", src }.
sizenumber120Diameter in CSS pixels.
tintstring | nullnullAny CSS color. Recolors without darkening.
interactivebooleantruePointer-tracked parallax and specular highlight.
ringbooleantrueHairline shadow-ring-xs around the avatar.
contactShadowbooleantrueSoft ellipse underneath. Makes the container 10% taller.
labelstring-Accessible name. Omitted avatars are hidden from assistive tech.
fallbackReactNode-Rendered instead when WebGL is unavailable. Always pass one.
forceFallbackbooleanfalseRenders fallback on a working GPU, for reviewing how it looks.
classNamestring-Additional CSS classes on the wrapper.

Any other <div> attributes pass through to the wrapper.

Guidelines

Do

  • Use it as an identity mark: agent avatars, empty states, onboarding steps, detail-page heroes
  • Prefer generated sources, since they cost nothing to fetch and can't fail to load
  • Always pass a fallback, and look at it once with forceFallback. The viewers who get it are the ones you can't reproduce locally
  • Turn contactShadow off in avatar slots so it stays square

Don't

  • Don't put text or interactive controls over it; drifting contrast makes anything above it hard to read
  • Don't reach for image sources when generated will do; they add a request that can fail
  • Don't hand-roll a border around it, since ring already matches the rest of the system
  • Don't ship one without a fallback because it looks fine on your machine; that is the exact bug this page's fallback example exists to catch
PreviousDropdown Menu
NextHover Scroll Text
Made in NYC© 2026 Rogo Technologies Inc.

Design at Rogo

We’re redesigning an entire industry. Come design it with us.

See open roles

Design at Rogo

  • How we workThe mission of design at Rogo, and the open roles
  • About RogoThe company and the product