What I optimize for
A checkout flow that converts and then double-charges is not a design success. The interface and the systems under it are one job with two ways to fail. This is how I break the tie when two good options conflict.
A checkout flow that converts beautifully and then double-charges is not a design success. A billing system that reconciles perfectly for a product nobody can figure out how to use is not an engineering one.
That’s the position. The rest follows from it.
The interface and the systems under it are one job
“User-centered” usually means sympathetic. I mean something stricter. The interface makes a promise to a person. The services, data, and infrastructure behind it either keep that promise or they don’t. If they don’t, the design is a lie.
So I don’t split this into an interface person and a systems person. It’s one job. It has two ways to fail. Most of the hard work sits where they meet.
Constraints work the same way. A compliance requirement is a fact about the problem, like a latency budget. Teams that treat it as an interruption ship worse products, and they take longer doing it.
Eighty percent data, twenty percent judgment
I want to be exact here, because “trust your taste” is usually an excuse.
I run about 80% on data and 20% on judgment. The 20% only counts because it came out of the 80. If your instinct has never been corrected by a real outcome, it isn’t instinct. It’s an opinion.
What the 80% looks like:
- VyprVPN. Measured in Mixpanel end to end. Onboarding aimed at the first connection, because the behavior data showed that was the moment the habit formed.
- U.S. Gold Bureau. The gains came out of a running CRO experiment program. Not out of a redesign someone had a feeling about.
- Engineering. When I said the org got more productive on an agentic workflow, I didn’t rest it on one definition of a “substantive” pull request. One metric is easy to argue with and easy to game. I used signals that agree with each other: median PR size up, trivial work down, SonarQube quality holding, post-release regressions down.
- Interface. The gates are numbers, and they pass or fail. Contrast ratios. Frame times on a slow phone. Zero axe-core violations in both themes.
The 20% picks what to test next. It tells me where to look when the data is thin. And it handles the case nobody likes to talk about, where the number is good and the experience is still bad.
It also tells me when to stop. I shipped a scroll animation on my own name, watched it run once, decided it was jarring, and deleted it. No test told me to do that. That was the 20%, and I’d rather label it than dress it up as evidence.
Follow the number to its cause
The VPN I ran had a 3.2 store rating. The cheap fix was right there: build a prompt that pushes happy users toward the store.
But a rating is a lagging indicator. It reports an outcome. It doesn’t explain one. So I went to the inputs behind it: the review text, the connection data, and what people actually did in the app. They all said the same thing. Connections dropped, and the app had picked up enough features to stop making sense.
We fixed connection quality and rebuilt all four native apps. Onboarding got aimed at one thing: get the user to their first connection. The rating reached 4.7, because the complaint stopped being true.
I did build a review prompt system afterward, and I won’t pretend it’s neutral. It reads sentiment from app behavior and connection signals. Positive-leaning users get the prompt. Negative-leaning users get automatic troubleshooting, then a person. The second half is the part I’ll defend. Nobody gets routed into a dead end.
The same rule works at small scale. On my own site, a header link was dropping readers a quarter of a screen above the section they asked for. The easy fix is an offset in the click handler. What I did instead was change the property the browser uses to resolve the jump. Now a click, a keyboard jump, and a pasted link all land on the same pixel. They agree by design, not because three separate numbers happen to match. The old version only looked fine in one path, and that was because that path’s error was smaller.
Fix what produces the number and the number stays fixed. Move the number directly and you’ve only hidden the problem. If the work doesn’t reach the cause, it isn’t done.
I’d rather rebuild than optimize a dead end
At the U.S. Gold Bureau, the store ran on Magento 2 with the stock Luma frontend. Luma set the ceiling on everything we wanted to test. So we rebuilt it headless: a custom Next.js frontend talking to Magento over GraphQL.
That opened up a real CRO program, and the program is what moved conversion and order value. Funnel work raised checkout completion. Merchandising and product-page work, mostly bundling, grew typical order size. Two results, from two separate lines of work.
The cost was real and I knew it going in. A custom frontend against a Magento backend means two systems to keep in sync, on both sides of that GraphQL boundary. That’s ongoing work. I took it in exchange for control and the freedom to test.
Knowing when you’re optimizing inside a dead end is most of the skill. Sunk cost is why people miss it.
Don’t lose the reader’s place
Orientation is unfashionable, and it’s where I spend more time than people expect. If someone loses their place, that’s a broken promise. It’s easy to miss if conversion is the only number you watch.
My case studies used to send you back to the top of an index you had already scrolled through. Now they send you back to your own row, and the row marks itself when you land on it.
An accordion that closes one row while it opens another will push the row you tapped up the screen. On a phone it moves far enough that you lose track of what you were reading. So it measures the geometry before the toggle, then starts the scroll on the next frame. Two separate movements look like a bug. One movement looks intentional.
And a control labeled “Top” was pointing at something that wasn’t the top. My note on that fix says the whole thing in six words: “Top” should mean the same thing everywhere.
Don’t take things away from someone who’s just looking
The career filter on my site doesn’t filter. It marks what matches and fades back the rest. The faded state is pinned to a color that holds 5.3:1 contrast in both themes. Doing that fade with opacity is banned, because opacity would push it under the accessibility floor.
Someone taps a control to explore. They should never end up with less than they started with. Hiding is destructive, and they were only looking around.
Text has to stay readable. That isn’t a style opinion and it isn’t up for debate.
My system carries two shades of the same accent color. They look almost identical. One is better looking and fails contrast at small sizes, so it’s allowed on rules, borders, focus rings, and large numerals, and nowhere near text. The other one is readable, and it’s the only one allowed near text. Using the good-looking one on small text is written down as a defect, not a style disagreement. An audit caught it once, and I didn’t want the argument reopened.
The clearest case: a scroll animation was fading elements in. Once it worked properly, rows sat at 0.56 opacity at rest. Not for a frame. That’s a state a reader can stop and squint at. I didn’t delete the animation. I shortened its range, so elements are fully opaque by the time three or four lines are readable. The effect survived, but only in the form that can’t hurt reading.
Set the speed budget before you start
Speed is another promise you either keep or you don’t. So the budget goes in before the work starts, and you measure against it. You don’t collect a score afterward and explain it. The number isn’t the goal. The number is the gate, and it doesn’t move because a deadline is close.
My site ships as static files. About 41 of them, roughly 1.5 MB. No framework runtime. No server. Every page reads with JavaScript off. The type system is three self-hosted font files, 101 KB total, down from 237 KB, by cutting from three families to two.
The career filter is a radio group and a CSS selector. The script only mirrors your choice into the URL so the view is shareable. Where JavaScript is unavoidable, it uses observers instead of scroll handlers, so nothing runs every frame while you scroll.
The gate is 60fps with zero long tasks on a 4x-throttled phone, re-run after anything that touches motion or layout. A filter change costs about 27ms and changes no geometry anywhere on the page. Nothing reflows. Nothing competes with the scroll.
I design for the phone directly. I don’t shrink the desktop and hope. I raised the type floor, because 11px uppercase at wide letter-spacing is hard work on a handset, however well it reads on a monitor. One size doesn’t scale at all, because below 13px a one-pixel range does nothing anyone can see.
One thing I keep noticing: the fast decisions and the user-centered decisions turn out to be the same decisions. Skipping an animation library is lighter, more accessible, and more restrained, all at once. That’s held on my work. I won’t call it a law.
How I break a tie
You can’t see someone’s taste until two good options conflict.
| When this conflicts with this | This wins |
|---|---|
| An effect I built | Someone saying it felt wrong |
| A visual flourish | Something that helps people find their way |
| A better looking color | A readable one |
| A tidier abstraction | Behavior that’s the same on every path |
| Something that works in one browser | One thing that behaves the same everywhere |
| Sunk cost | Everything |
What changes my mind: a measurement, a real user’s experience, a contrast ratio, a frame-time trace, or a case where two paths through the same interface disagree.
What doesn’t: it looks better, everyone else does it this way, we already built it, users won’t notice. Or any adjective doing a number’s job.
I delete work I shipped, and I write down why
The masthead animation above was the cheap version. Here’s the expensive one. I shipped animated page transitions. Someone reported they felt wrong in Safari. I found three causes. Two were fixable. The third was structural, and it broke the part readers actually needed, so I removed the whole feature. I also drew visible column guides across the layout and then took them out, because at reading distance they were noise. Both of those had working code behind them.
Then I write the reason where the removed thing used to be. There are comments in that codebase whose only job is to explain something that isn’t there: why there’s no page transition, why the masthead doesn’t move, why the grid is never drawn. Each one is there because the missing feature looks like an obvious improvement to whoever shows up next.
Nobody admires the animation you didn’t ship. There’s no credit in it. The point is that the standard holds when I’m not in the room and someone is confidently proposing the thing I already tried.
Delight is worth chasing. It’s just rare.
Everything above is subtraction, so let me be clear about what I’m not saying.
I want to delight people. I think it has to be earned, not applied.
Linear was the first SaaS product in a long time where the interactions actually delighted me as a user. Not the feature set. The way it answered when I touched it. That’s rare now, and it’s rare because it’s hard. You can’t file it as a ticket. It comes from someone caring about a small moment that nobody asked them to care about.
Every user deserves that. Most products don’t give it to them.
So the standard is to find the small places where you can do it without charging the user for it. Not a splash screen. Not something that shows off. The accordion that moves once instead of twice. The row that marks itself when you come back, so you know exactly where you are. Those are small. They’re supposed to be small.
That’s also why the scroll animation on my site still exists. I didn’t kill it. I shortened its range so it can’t hurt reading. The delight survives in the form that costs the reader nothing.
You only find those places if you like the work. That’s the honest requirement. If you’re grinding through tickets, you’ll ship something that functions and nobody will feel a thing. People deserve our best here, and the best part is that this is the fun part.
The short version
I’m not designing for restraint as a style. I’m designing so that nothing the interface says turns out to be untrue. Not the price on the page. Not the label on the button. Not the suggestion that you’re still where you think you are. Not the promise that this loads on a bad connection on a cheap phone.
Restraint isn’t the goal. It’s what’s left after everything that couldn’t keep a promise is gone. Delight is what I try to put back.
One caveat, because I’d rather say it than have someone catch it. I didn’t have the list above when I made those calls. I wrote it down afterward, by looking at what I actually chose. So it describes a pattern.
Revisions
- First published.
- Removed conversion and average order value figures pending verification.
- Replaced the remaining conversion and order-value sentences with the qualitative account of the CRO program.