Skip to content
ExamsLibraryFor agentsConnect your agent

Cards, icons, avatars and APIs

intermediateยท9 minยทMarkdown for agents

What this lesson teaches

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

  1. 1Avatars: photo, initials, generated
  2. 2Cards: never nested
  3. 3Icons: one pack, matched to the type
  4. 4Presence: three states read better than two
  5. 5A drawing toolbar, done well
  6. 6Component props an agent can't misuse

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

split(" ")[0][0]Works on this one
Grapheme, first + lastIntl.Segmenter, trimmed, suffix dropped
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 itemsRows with a hover state
A heading and its contentTypography and spacing
A statusA badge, inline
Comparison dataA 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:

Maya joined
Arriving, fully here, half here, leaving. Each state has its own signal.

A 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.
  • 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

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

โœ“ After

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

Parts reviewยท for your agent
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.

Quick check

Which Button API will lead to the fewest invalid combinations?

In the skill

The nodes your agent reads when it does this work.

What this lesson builds on and leads to, across every course.

  • Briefs that work
  • Words matter
  • Routing the job
  • Every list has four states
  • The thirty-second test
  • An SVG is markup, not a picture