PixelDosa UI

Agent Figure

A hand-drawn agent whose pose shows what a run is doing, how sure it is, and when it needs you.

Research the top 5 competitors.

  1. Understanding your request
  2. Planning the approach
  3. Searching 5 sources
  4. Reading pricing pages
  5. Comparing pricing models
  6. Building the comparison
ListeningListening

Installation

bash
npx shadcn@latest add @pixeldosa/agent-figure

The example above also uses Button. To copy it as-is, install everything it needs:

bash
npx shadcn@latest add @pixeldosa/agent-figure @pixeldosa/button

Requires `cn` at `@/lib/utils` and the agent marker colours (`--agent-working`, `--agent-waiting`, `--agent-blocked`, `--agent-done` and their `-soft` fills) from @pixeldosa/pixeldosa-theme.

Usage

"use client";

import { AgentFigure } from "@/components/ui/agent-figure";

<AgentFigure pose="searching" label="Checking 5 sources" />

Map what your run is doing to a pose and pass it in. The component doesn't read your stream or own the run.

Features

  • Its body shows its state. Every pose maps to something a run really does: listening, planning, searching, reading, comparing, working, asking, blocked, done. There are no moods.
  • It shows how sure it is. confident, probable and unsure are for handing over a result. Drive them from your real confidence, the same tiers as Confidence Meter. Never pick one to reassure.
  • It's still on your turn. While the agent works, its pen lines redraw slightly and small parts move in steps. When it's waiting on you, blocked or done, every line holds still.
  • Words come first. A visible label sits beside the figure by default. The drawing supports the label; it never replaces it.
  • Drawn in code. Seeded strokes render the same on server and client, and every colour comes from the theme, so it follows light and dark mode.
  • Four markers, one job each. Blue is working, orange means it needs you, red is blocked, green is done. Everything else is ink.

Variants

The work poses

ListeningThinkingPlanningSearchingReadingComparing optionsWorking

Your turn: it holds still

ReadyNeeds your inputA source didn't loadDone

How sure it is

Found it: 4 of 5 are per seatProbably per seatTwo pages disagree

Mark

The head alone, for a status line, an avatar or a button.

Reading invoices/2026-q3.csvNeeds your inputSummary ready

Examples

Beside the step a run is on, with the status text doing the talking:

<AgentFigure pose={run.pose} size="lg" labelPosition="below" label={run.status} announce />

As a subtle presence in a dense tool, with only the head:

<AgentFigure variant="mark" pose={run.pose} label={run.status} />

Props

PropTypeDefaultNotes
poseAgentPoserequiredOne of the fourteen poses above.
variant"figure" | "mark""figure"mark draws the head alone.
size"sm" | "md" | "lg""md"Figure: 48, 96 or 160px. Mark: 16, 24 or 32px.
labelstringpose labelWhat the pose means, in words.
hideLabelbooleanfalseHides the visible label; the drawing then carries it as its accessible name.
labelPosition"end" | "below""end"Where the visible label sits.
announcebooleanfalseAnnounces label changes through a polite live region.
stillbooleanfalseHolds the lines still on the agent's turn too.

Accessibility

  • With a visible label, the drawing is hidden from assistive technology and the label is read as ordinary text.
  • With hideLabel, the drawing becomes an image named by label.
  • announce is off by default. Turn it on only when nothing else on the page announces the run's status, so screen reader users don't hear it twice.
  • Under prefers-reduced-motion, nothing moves and every pose stays a distinct drawing.
  • The frame clock stops when the figure is off screen or the tab is hidden.

When not to use

  • As the only signal. Always pair it with words: a label, Live Status Line, or the step list it sits beside.
  • In dense tables and toolbars. Use Agent Presence in its line form there.
  • To show feelings. If a pose would make the agent look more certain, sorry or pleased than the system is, don't use it.

why it's built this way

Engineering Notes

The character is generated, not illustrated: each stroke is a polyline pushed off its true path by a seeded wobble and smoothed with Catmull-Rom curves. Seeds come from the pose, the stroke name and the frame, so the server and client render byte-identical paths and there is no hydration mismatch, and a new pose costs a few coordinates rather than an asset. Every colour is a theme token, so the figure follows light and dark mode and a host's palette. Poses map to things a run actually does, never to feelings: 'blocked' is a state, 'sad' is not, and the three result poses exist so confidence can be shown honestly rather than always looking pleased. The frame clock runs only while the pose is on the agent's turn, the figure is on screen (IntersectionObserver), and the tab is visible; otherwise nothing ticks.

how it moves, and when it doesn't

Motion Notes

Two kinds of motion, both stepped at 280ms rather than eased, so the figure reads as drawn rather than rendered: the line boil (strokes re-seed every second frame) and a few discrete prop movements — eyes scanning a page, a key press, the thought bubble filling in. Both stop completely when the pose is the person's turn (`idle`, `asking`, `blocked`, `done` and the result poses), which is the system's rule that stillness means it's your turn. `still` holds the lines on the agent's turn too. Under `prefers-reduced-motion` nothing animates and each pose remains a distinct static drawing, so no information is lost.