Role

Product designer who ships

Audit to production code

Timeline

2025 to 2026

Featured case: Mar to Jul 2026

Team

Design system committee

Designers and engineers

Platforms

Web, iOS, Android

Hardening M1's design system

Closing the gap between Figma and code

Liquid's components looked one way in Figma and behaved differently in code, and on every client. I owned components end to end, from audit to production code. The clearest example: I replaced a rules-free container with two accessible components, Menu and Popover, built them myself and swapped every web instance in one release.

18 → 35

Hardened components, mid-March to end of Q3

−90

Net lines of code: 940 removed, 850 added, 19 files

1 day

From merge to production, then adopted by M1's AI advisor

01. Why Liquid needed hardening

Teams had built independently, so the same component existed several ways across web, iOS and Android, with no owner and no single answer. The Figma-to-code gap was the top pain point in our committee survey, so the goal for 2025 and 2026 was to harden one to two components a month on every client.

Our definition of done

Design matches code on every client, and it's implemented across the product. A new version alone isn't enough.

Hardened components in Liquid

Mid-March 2026

4 in progress, 16 outstanding

18

End of Q3 2026

The last five are planned for Q4

35

Components I owned end to end: Menu and Popover, Links, Group radio option, App bar, and Avatar and pie slice image.

02. How we hardened a component

The same four steps every time, so any designer or front-end engineer could own a component.

01

Audit

Find every instance and document the gaps per client.

On Menu and Popover

Mapped every usage with Claude and reviewed scope with the team.

02

Requirements

Must-dos to align Figma and code, plus optional design changes.

On Menu and Popover

Wrote the usage rule and documented both components.

03

Build

Figma, code, design QA and a testing party, then merge the library.

On Menu and Popover

Built both, replaced every web instance, merged the library.

04

Share the win

Announce what changed so teams use it.

On Menu and Popover

Posted to the committee and was recognized at the next meeting.

Case study · Replacing FloatingPanel with Menu and Popover

03. The problem: a container with no rules

FloatingPanel let anything go inside, and every client built it differently. The result: uneven scrolling, layering bugs, no accessibility story, and a pattern spreading into places it didn't belong.

Before · the Move Money menu

Move money

Transfers after 4pm ET arrive the next business day.

Deposit

From a linked bank

Withdraw

To a linked bank

Transfer

Between M1 accounts

Instant

More options ▾

  • Banner

    A notification inside what claims to be a menu

  • List with pills

    Items with descriptions and status, more than an action

  • Progressive disclosure

    A button that reveals more items in place

  • It looked like a menu, but behaved like neither a menu nor a dialog.

The audit

Map the usage. I used Claude to find every panel in the code and flag the snowflakes.

Document the gaps. Tracked separately for web, iOS and Android.

Decide together. The committee set scope as a group, which made adoption easier later.

04. Split by behavior, not by looks

The rule I wrote for the team

If every item is the same type of thing, use a Menu. If it holds more than one type of thing, use a Popover.

Content

Menu

One type of item

Popover

Mixed: list, banner, button, information

Keyboard

Menu

Arrow keys; closes on choose

Popover

Tab; closes on Escape, outside click or ×

Screen reader

Menu

Announced as a menu

Popover

Announced as a dialog

On mobile

Menu

Action sheet (iOS), dropdown (Android)

Popover

Bottom sheet

A menu and a dialog are announced differently, so one container with variants couldn't be accessible for both. That's why it's two components.

Try it · Menu and Popover, live

Menu

Every item is the same type of thing

Keyboard: ↑ ↓ move between items · Enter chooses and closes · Esc closes

Screen reader: Open it with a click, or focus it and press ↓.

Popover

More than one type of thing inside

Keyboard: Tab moves through the content · Esc, outside click or × closes · page keeps scrolling

Screen reader: Open it, then Tab through the content.

05. Designing the rules

I treated the docs as part of the product: anatomy, composition patterns, do and don't, accessibility and specs, shared early for async feedback.

Constrained composition. Only named parts and approved Liquid components go inside.

Accessibility built in. Engineers don't override focus or keyboard behavior.

Rules with numbers. Eight items max without grouping, destructive actions last, labels of one to three words.

Menu · Anatomy

Menu anatomy on web: optional label, items with optional icons, and separators, each with size and color tokens

Popover · Composition patterns

List, banner and progressive disclosure: Move money with a verification bannerInformation plus list: choosing a new pie for your slicesZero state: create a pie to move slices

Menu · Do and don't

Menu do and don't: a clean action menu and a list menu, versus the Move money panel captioned A Popover use case being forced into a Menu, and descriptor text inside menu items

The Don't is the old Move Money panel, kept on purpose as the clearest example of what a Menu is not.

Popover · Do and don't

Popover do and don't: a Move money list with progressive disclosure, versus a photo and a chart with a hardcoded width

Menu · Accessibility

Trigger button with aria-haspopup menu and aria-expanded false, then the open menu with aria-expanded true, focus on the first item, and role menu
↑ ↓ Arrow keys
Move between items. Required for menu semantics, never suppressed.
Enter or Space
Chooses the item and closes. A checkbox item toggles without closing.
Escape
Closes without choosing and returns focus to the trigger.
Tab
Closes the menu. Tabbing through content is Popover behavior.

The critical difference from Popover: role="menu" versus role="dialog". The primitive handles it, so engineers don't add ARIA by hand.

06. Build, ship and roll out

Done meant shipped, so I didn't hand off the spec. I built both components in Liquid's React library and worked the same workflow as engineering.

Code review. Pull requests reviewed and approved by engineers.

Tested before release. Alpha builds, previews, automated tests and every affected screen.

Versioned releases. Follow-ups fixed panels under dialogs and a dark-mode mismatch.

One rollout. Every web instance replaced in a single pull request, with before-and-after screenshots.

In the product · Popover

M1 home with the Move money Popover open: a verification banner, transfer options with pills and descriptions, and more options

In the product · Menu

Manage connections with an institution Menu open: Add an account and Remove connection

Review made it better

A hidden default. A built-in minimum width would have widened two panels, so I set explicit widths.

Simpler triggers. Call sites now use the standard Button instead of custom styling.

Changes flagged openly. When I moved a Reset action, I explained why and offered to move it back.

07. Results

Shipped June 30, 2026. The first component hardened in Q3, recognized at the next committee meeting.

Two documented components. Each with a usage rule, accessibility behavior and do and don't guidance.

Design and code agree. The Figma library lists Menu and Popover where FloatingPanel used to be.

Adopted beyond the team. M1's AI advisor uses the Popover for citations on web.

The system nearly done. 35 components hardened by the end of Q3, with the last five planned for Q4.

One tradeoff: Popover doesn't block scrolling, so a panel opened low can drift out of view. The documented answer is a Modal when the page needs to lock.

BoldinBoldin Withdrawal Order →
NEXT
BoldinBoldin Withdrawal Order →
NEXT
BoldinBoldin Withdrawal Order →
NEXT
BoldinBoldin Withdrawal Order →
NEXT
BoldinBoldin Withdrawal Order →
NEXT
BoldinBoldin Withdrawal Order →
NEXT
BoldinBoldin Withdrawal Order →
NEXT
BoldinBoldin Withdrawal Order →
NEXT
BoldinBoldin Withdrawal Order →
NEXT
BoldinBoldin Withdrawal Order →
NEXT
BoldinBoldin Withdrawal Order →
NEXT
BoldinBoldin Withdrawal Order →
NEXT