Nine slightly different buttons on the left, the same nine identical on the right

Capital One

UI Consistency

The app had grown a dozen local dialects of the same interface: inline styles instead of tokens, four table layouts, three ways to say “no data”. I audited it, designed the shared layer underneath, and cut the 208-file change into ten pull requests a human could actually review.

RoleDesign engineer,
system owner
StackReact, TypeScript,
MUI theming
Scope208 files,
9 commits, 10 PRs
StatusShipped
NoteIf you would like the detail, please get in touch
Context

Nobody set out to build four table layouts

Each one was a reasonable local decision. A team needed a table, the existing one was close but not right, and copying it was ten minutes against a week of negotiation. Enough reasonable local decisions later, nobody could tell you what the product's table looked like, because the honest answer was that it depended on the page.

The cost was not really aesthetic. It was that every change had to be made in every copy, and the last copy was always the one somebody forgot. A shared layer is worth building at the point where keeping things in sync by hand costs more than the abstraction does, and we were well past it.

Stillcontact sheet

Every live variant of one component, side by side

The real inventory, cropped from the actual pages and laid out on one sheet. Buttons are the clearest case: nine near-identical things next to each other make the argument in a way no count ever does.

  • Crop each variant at the same zoom, on a neutral ground
  • Keep their real spacing and radii; do not tidy them
  • Caption each with the surface it came from
Size 799x391 @2xFormat PNGNote component chrome only, no data
Key decisions

Counting what was actually there

The instinct is to design the target state and then go find everything that does not match it. I did the opposite: inventoried every variant already shipping, with no judgement attached, before proposing anything.

That order mattered more than it sounds. Two of the variants I would have deleted on sight turned out to be load-bearing, solving a real constraint the canonical component could not. They became part of that component's API rather than exceptions to it. Designing first and reconciling later produces a system that is correct in isolation and wrong in the product, which is the usual way these fail.

One theme, no inline styles

The foundation is a single theme provider wrapping the app, with tokens for colour, typography, spacing and radius. Components inherit from it instead of carrying their own inline values.

Underneath that, the shared pieces: one toolbar with search, filter chips and actions in a single row; one footer with the count on the left and export on the right; real empty and not-found states instead of the inline “no data” text that had been written separately on every page. Tables lost twenty pixels of row height, gained proper header treatment and zebra striping, and dropped to a subtler border.

The filter chips are the detail I am most pleased with. They show at full width rather than truncating, overflow collapses into a +N chip, the dropdown stays open while you are still choosing, and typing hides the chips so you get a clean search field. Four small decisions, all of them about not interrupting someone mid-thought.

Stillbefore / after, one component

One table, before and after the shared layer

The same table on the same page, captured either side of the change. Row height, header treatment, striping and border weight all move at once, which is why one component carries the story better than a token diagram.

  • Identical data and window width in both
  • Same scroll position, so only the styling differs
  • Label BEFORE / AFTER rather than old / new
Size 799x391 @2xFormat two PNGs, or one sliderNote the 72px to 52px row height should be obvious

Ten pull requests, in dependency order

208 files across 9 commits is not a reviewable change. It is a change that gets approved without being read, which is the same as not being reviewed, on a diff touching every page in the app.

So it went out as ten pull requests of fifteen files or fewer, ordered so each could merge on its own. The theme provider had to land first because everything else assumes it. The shared table and filter components went second because the page-level work consumes them. After that the remaining seven were independent and could go in parallel, in any order, by whoever had time.

The foundation PR contained no behavioural changes at all — visual consistency only. That was deliberate: the riskiest change in the sequence is the one everything depends on, so it should also be the one with the least in it.

Stillvariants grid

The filter chip, in all four of its states

The chip work is the detail worth showing close up: full-width labels, the +N overflow chip, the dropdown staying open, and typing hiding the chips for a clean search field. Four small decisions, one grid.

  • State 1: one chip selected, label at full width
  • State 2: overflow, showing the +N chip
  • State 3: dropdown open mid-selection
  • State 4: typing, chips hidden
Size 799x391 @2xFormat PNG, 2x2 gridNote crop tight to the control, not the page
01PR 1The theme provider and global defaults. No behavioural change. Everything downstream assumes it.
02PR 2Shared table, toolbar and filter components, consumed by every page-level PR that follows.
03PRs 3–10Page-level adoption. Independent of each other, reviewable in parallel, mergeable in any order.
Impact

One product, one language

Every page now renders from one theme, and a change to a shared component is one edit rather than a search-and-replace across the app.

The result I care about most is quieter than the file count: design review stopped spending its first ten minutes establishing which version of a component we were looking at. The system's real output is the argument it makes unnecessary.

208 files · 9 commits · 10 pull requests

0files changedacross the whole application
0reviewable pull requests15 files or fewer, dependency-ordered
0theme providerreplacing inline styles throughout

Riskiest PR  The one with no behaviour in it