E-Commerce Platform Redesign

Quantitative
We instrumented every field in the checkout with interaction tracking, then analyzed ~40,000 sessions:
| Step | Entry | Completion | Drop-off |
|---|---|---|---|
| Cart review | 100% | 84% | 16% |
| Shipping info | 84% | 61% | 23% |
| Payment | 61% | 38% | ~38% |
| Confirmation | 38% | 36% | 2% |
The payment step was the clear bleed point. Session recordings showed users repeatedly scrolling up to re-check the order total — which wasn't visible on that screen.
Qualitative
Twelve moderated sessions with existing customers, plus five with first-time buyers. The single most common phrase, heard in nine of seventeen sessions, was some variation of "wait, how much is this going to be?"
Approach
We consolidated four pages into a single-page accordion flow with a persistent order summary.
Design Principles
- Never hide the total. The order summary is sticky on desktop, collapsible-but-pinned on mobile
- Guest checkout first. Account creation is offered after purchase, pre-filled with the data already entered
- One decision per section. Each accordion panel asks for one category of information
- Fail gracefully. Inline validation on blur, never on submit
Component Library
The redesign shipped with a 34-component library built on design tokens. A representative token definition:
{
"color": {
"surface": {
"base": "#FFFBF5",
"raised": "#FFFFFF",
"sunken": "#F4F0E8"
},
"action": {
"primary": "#F04B3E",
"primaryHover": "#D83D31"
}
},
"space": {
"xs": "4px",
"sm": "8px",
"md": "16px",
"lg": "24px",
"xl": "40px"
}
}
Components consumed tokens exclusively — no hardcoded hex values passed code review. The validation hook that powers inline field errors:
function useFieldValidation<T>(
value: T,
validators: Array<(v: T) => string | null>
) {
const [error, setError] = useState<string | null>(null);
const [touched, setTouched] = useState(false);
const validate = useCallback(() => {
for (const validator of validators) {
const result = validator(value);
if (result) {
setError(result);
return false;
}
}
setError(null);
return true;
}, [value, validators]);
return {
error: touched ? error : null,
onBlur: () => {
setTouched(true);
validate();
},
};
}
Results
Measured over the first full quarter post-launch, against the same quarter the prior year:
| Metric | Before | After | Change |
|---|---|---|---|
| Cart abandonment | 68% | 45% | −23 pts |
| Avg. checkout time | 4m 12s | 1m 48s | −57% |
| Mobile conversion | 1.8% | 3.1% | +72% |
| Support tickets (checkout) | 340/mo | 89/mo | −74% |
The projected realized revenue impact exceeded the original forecast by roughly 15%.
What We'd Do Differently
Two things, honestly:
- We spent too long on the desktop layout before testing mobile. Mobile was ~70% of traffic, and we should have designed there first rather than adapting down
- The component library was built during the redesign rather than before it, which caused some rework when patterns diverged late
Stack
Built with Next.js (App Router), TypeScript, and Tailwind CSS. State managed via Zustand for cart persistence. Payments through Stripe Elements. Analytics instrumentation via a thin wrapper around PostHog.
Gallery
