← Blog

Abstraction is beautiful

John Jeong

I spent eleven days telling coding agents to use the same Card component.

On October 8, I asked Codex to look at the scattered UI in Char. On October 19, I opened a Claude Code session with: "why is the clean up card like this? it does not seem to use shared card component".

Somewhere between those messages, we had defined the components, written stories, and asked for the migration across desktop, mobile, and web. I was still finding exceptions.

I'm building Char, where you work with an assistant called Charlie. An email can lead to a conversation, and that conversation can produce something you need to approve. We needed a consistent way to represent those interactions wherever they appeared.

A Card is where the conversation and work happen. It has room for a preview, the conversation, and a composer. A Chip is a compact reference to something, like a person or a Page, with a preview when you need more context. A Prompt sits inside a Card when Charlie needs guidance or approval.

Card.Header means the same thing in a design discussion and an import. Prompt.Clarification collects guidance; Prompt.Confirmation lets you review a prepared action before approving it.

I like being able to point at something and use its actual name.

Initially, I grouped the components by where they appeared. Cards and Chips lived in the editor. Panels lived in the Deck. Then I asked why an expanded Card and a Panel couldn't share an implementation, with the host deciding the width.

The place something appeared had become part of its identity. Once I questioned that, we could share more of the component.

Chips made the details harder to ignore. I asked to see all the variants together in a Storybook grid. Previews that were supposed to have the same shape didn't. Titles repeated information already visible in the Chip. A person preview showed a generic organization icon where we already had an organization Chip with its own avatar.

Putting the examples next to each other gave me something concrete to correct. It also made me decide what should stay different: a GitHub pull request has its own states, and its Chip should respect them.

With Prompts, I kept asking to see the real Card with the composer present. An isolated confirmation could look fine while the composed version had duplicate borders or awkward spacing. I wanted to review the thing a person would actually use.

These are screenshots from those reviews, two days apart. The blue annotations are mine; both show the same example, Charlie reorganizing Home, while we were still changing it.

October 16 Storybook review: the confirmation to reorganize Home has nested frames, a repeated draft header for Home, and a full-width Confirm button below the preview.

October 16: the draft had its own header and toolbar inside another frame. The example had no composer.

October 18 Storybook review of the same example: Confirm sits in the compact Prompt header, the draft preview sits beneath it, and the Card composer remains visible below.

October 18: Confirm moved into the Prompt header, the repeated draft toolbar was gone, and the composer stayed below the preview. I was still marking spacing problems.

But a Storybook example could also bypass the shared component.

We had a shared component and competing implementations of it. Each new exception meant another place to explain the same decision.

That is why we started connecting our design docs to the way agents work.

Our root design.md is an index. It points to the relevant component rules and product concepts, with app-specific guidance for desktop and mobile. Sharing a Card doesn't mean a phone and a desktop should have the same interaction.

Storybook shows the components and their states using production code. We can open an example, interact with it, and discuss the exact part that needs to change. The code remains the source of truth when the docs and the app disagree.

The design skill tells the agent how to use both. Before writing UI, find the existing component and inspect its consumers. Reuse it if it fits. Extend it if a state is missing. Create something new only after checking that nothing covers the need.

It also tells the agent where to make the fix. A shared problem belongs in the shared component; a mobile interaction belongs in the mobile composition. Changes to the experience should update the relevant docs and stories together.

I don't want to paste eleven days of corrections into the next session. I want the next session to find the decisions where it does the work.

We're still migrating the remaining paths. I can't claim the docs or the skill have stopped drift. The clean-up Card was a pretty direct reminder of that.

Abstraction is beautiful when I can make a decision once and have it apply everywhere that decision belongs. Right now, I'm still finding out where that is.