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.
Tap the names one by one and watch the left avatar.
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.
- Real photo, always preferred.
object-fit: coverhandles a 4000ร200 upload;flex-shrink: 0keeps the circle round in a crowded row. - 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. - 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.
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:
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
Cardthat takesCardHeaderandCardContentas children beats aCardwithheader={{ title }}andfooter={{ actions }}props, because children can be reordered, omitted and wrapped. - share state through context:
Dialog,Dialog.TriggerandDialog.Contentare separate parts you arrange. - The layer order:
variant, thensize, thenclassNameas the escape hatch, thenasChildto render as another element (a Link styled as a Button). Never style props likepaddingX. - Restrict the escape hatch. Leave
classNameoff 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, notisDisabled;onOpenChange; positive booleans. - Controlled and uncontrolled:
defaultValuefor internal state,valueplusonChangewhen the parent owns it. - Forward refs and spread the rest, so
aria-*attributes reach the DOM and tooltips can position. Defaulttype="button", neversubmit.
โ 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
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.
- Cards designDon't nest cards. Design card interiors intentionally โ whitespace, hover-only actions, subtle icons.
components/cards-design - Icon systemsNever mix icon packs. 10-32px range. Match stroke to font weight. Lucide is overused.
components/icon-systems - Avatar systemsUse real photos when available; fallback to deterministic generated avatars. DiceBear provides 30+ styles for the fallback.
components/avatar-systems - Presence signalsShowing that another person is here, half here, or gone in a real-time product โ an arrival that fills a ring, partial attention as a dashed outline, a leaving beat, live typing that resizes its bubble, reactions pinned where they were tapped in relative coordinates, shared activities that pause when one person leaves, and one colour per person everywhere. From Benji Taylor's Honk.
components/presence-signals - Drawing toolbarA drawing or markup tool inside a product โ a toolbar that changes shape instead of swapping panels, pens in a tray rather than a row of icons, strokes whose width answers speed or direction, an eraser that removes area, export that matches the screen exactly, defaults that are the shipping version, and a bar that stands up on the side of a phone. From Benji Taylor's Drawesome.
components/drawing-toolbar - Component API designA component API is a contract. Composition over configuration, compound parts sharing context, customization layered variants โ size โ className โ asChild with className restricted on styled components, platform prop names, controlled and uncontrolled both, refs forwarded and props spread, type="button" by default.
components/component-api-design
Connected lessons
What this lesson builds on and leads to, across every course.