# Cards, icons, avatars and APIs

> Get the everyday parts right (cards, icons, avatars, presence and drawing tools) and design component props an agent can't misuse.

Module: Components and states · Level: intermediate · 9 min · https://school.agents.org.in/design/components/parts

Tap the names one by one and watch the left avatar.

*[Interactive demo: The one-liner breaks on emoji, suffixes, extra spaces and names without spaces.]*

`name.split(" ")[0][0]` is what every agent writes for initials, and real names break it: an emoji splits into half a character, "Christopher Montgomery III" becomes "CI", "王秀英" has no spaces. Small parts like this repeat on every screen, so their mistakes do too.

## Avatars: photo, initials, generated

Pick an avatar strategy before you scale; switching later is painful because people learn what they look like in your product.

1. **Real photo**, always preferred. `object-fit: cover` handles a 4000×200 upload; `flex-shrink: 0` keeps the circle round in a crowded row.
2. **Initials**, the common fallback. First and last word, by grapheme cluster (`Intl.Segmenter`), trimmed, suffixes like "III" dropped, uppercased. No usable name: the email's first letter, then a generic person icon.
3. **Generated**, from a seed. DiceBear makes the same SVG face from the same user id every visit. Self-host it (`@dicebear/core`) rather than hitting its public API on every page load, and check each style's license.

Match the style to the product: `initials` or abstract shapes for B2B, robots (`bottts`) for an AI agent, illustrated faces for consumer social. Healthcare, legal and finance stay with initials. And give every image an `onError` that falls back down the chain; a broken-image glyph should never show.

## Cards: never nested

A card is a self-contained unit with its own interior. A card inside a card inside a card means three borders, three shadows, three radii, and a design that has lost its way. The inner card is usually something else:

| Reaching for an inner card? | It is probably |
|---|---|
| A list of items | Rows with a hover state |
| A heading and its content | Typography and spacing |
| A status | A badge, inline |
| Comparison data | A real table |

Inside the card, space beats lines: a 24px gap between header and body reads cleaner than a divider. One small icon per card at most. Edit and delete actions can appear on hover, but **always show them on touch**. And if a card has a 12px radius and an image filling its top, the image needs `12px 12px 0 0`, or it looks off. If a screen is *all* cards, it has no hierarchy.

*(Most "this card looks off" complaints are a mismatched inner radius.)*

## Icons: one pack, matched to the type

The eye notices three icon packs on one screen instantly, even when nobody can say why. **Never mix icon packs.** If you need an icon your pack lacks, draw it in that pack's style.

- **Lucide is fine but everywhere.** Phosphor, Hugeicons or Tabler give a product its own look.
- **Sizes:** 16px inline with text, 20px in toolbars, 20 to 24px in navigation, 32 to 48px in empty states.
- **Stroke matches font weight:** regular text (400) wants about a 1.5px stroke; medium (500) about 1.75 to 2px.
- **One icon, one meaning.** A chevron can't mean both "expand" and "next page". Upload and download, which differ only by a 180° turn, need a label.
- **Transform, don't crossfade.** A chevron opening rotates 180°.
- 6 to 8px between an icon and its label; ambiguous icons (the floppy disk) get a text label.

## Presence: three states read better than two

Most real-time products show presence as one green dot. Benji Taylor's chat app Honk made presence the thing every feature was built around, and the patterns travel to any shared surface. Step through them:

*[Interactive demo: Arriving, fully here, half here, leaving. Each state has its own signal.]*

A presence signal (How a shared product shows who else is there and how much attention they are paying: arriving, here, half here, leaving. More than a green dot) shows who is here and how much. **Arriving** gets a moment (a ring filling), not a dot changing color. **Here** shows live activity: the typing bubble grows letter by letter. **Half here** (in the app but looking elsewhere) is a dashed outline, the state most products drop and the one that explains a slow reply. **Leaving** gets a short goodbye beat.

One color per person, everywhere: cursor, selection, avatar ring, name label. Reactions pin to where they were tapped, stored as relative coordinates. And presence fires often, so scale the signal down as frequency goes up, announce joins and leaves in a live region, and never fire it for your own arrival.

## A drawing toolbar, done well

If your product needs a little markup (circling a bug on a screenshot), Benji Taylor's Drawesome is the version worth copying. The bar **morphs** instead of opening panels: pick a color and the row of pens becomes the palette; minimize it and it rolls into a disc that still shows the tool in hand. Pens behave like their names: a pen thins as you speed up, a highlighter keeps its width, the eraser removes area rather than whole strokes. Shift locks a stroke to eight directions; `E` erases; `[` and `]` resize. Export returns exactly what is on screen. Under about 680px, the bar stands up as a rail on the left edge with fewer tools.

## Component props an agent can't misuse

This is about the props of a shared component, not its pixels. Two failures repeat: too rigid (people fork it) or too configurable (thirty props). The fixes:

- **Composition over configuration.** A `Card` that takes `CardHeader` and `CardContent` as children beats a `Card` with `header={{ title }}` and `footer={{ actions }}` props, because children can be reordered, omitted and wrapped.
- **Compound components (A component split into parts that share state behind the scenes, like ,  and , so you arrange the pieces instead of passing a big config object)** share state through context: `Dialog`, `Dialog.Trigger` and `Dialog.Content` are separate parts you arrange.
- **The layer order:** `variant`, then `size`, then `className` as the escape hatch, then `asChild` to render as another element (a Link styled as a Button). Never style props like `paddingX`.
- **Restrict the escape hatch.** Leave `className` off components whose look *is* the system (Button, Badge, Input), or agents "fix" spacing with literals one override at a time. A new look is a new variant.
- **Name like the platform:** `disabled`, not `isDisabled`; `onOpenChange`; positive booleans.
- **Controlled and uncontrolled:** `defaultValue` for internal state, `value` plus `onChange` when the parent owns it.
- **Forward refs and spread the rest**, so `aria-*` attributes reach the DOM and tooltips can position. Default `type="button"`, never `submit`.

  **Before (avoid):**

`Button primary large rounded leftIcon={PlusIcon} iconSpacing={8}`: boolean soup and a prop for every icon detail

  **After (do this):**

`Button variant="primary" size="lg"`, with the icon and the words "New project" passed as children

**Parts review (paste into your agent):**

```text
Review these shared parts:
- Avatars: photo, then grapheme-cluster initials (Intl.Segmenter, first + last word, suffixes dropped), then an icon; onError falls back; flex-shrink: 0; object-fit: cover.
- Cards: no card nested in a card; space over dividers; hover actions always visible on touch; inner radius matches.
- Icons: one pack only (tell me if any mix), stroke matched to font weight, sizes 16/20/24, one meaning per icon.
- Component API: variant → size → className → asChild; no boolean soup or style props; forward refs, spread props, type="button" default; no className on Button/Badge/Input.
```

**Check yourself:** Which Button API will lead to the fewest invalid combinations?

- [ ] Boolean props: primary, secondary, large, small
- [x] Enums: variant="primary" size="lg"
- [ ] Style props: backgroundColor="blue" paddingX={16}
- [ ] One config object: config={{ look: 'primary', big: true }}

*Why:* Enums for variant and size allow exactly one choice each. Boolean soup permits primary and secondary at once, and style props are a second CSS system.

## In the skill

- https://school.agents.org.in/skill/components/cards-design.md
- https://school.agents.org.in/skill/components/icon-systems.md
- https://school.agents.org.in/skill/components/avatar-systems.md
- https://school.agents.org.in/skill/components/presence-signals.md
- https://school.agents.org.in/skill/components/drawing-toolbar.md
- https://school.agents.org.in/skill/components/component-api-design.md
