Break it, then ship it
intermediate·9 min·Markdown for agents
What this lesson teaches
Feed a component the realistic worst case before users do, land the production baseline, and sweep the whole app before launch with a severity tree that tells blockers from polish.
The member row looks perfect. It was designed with "Ana Ruiz", three rows and a wide screen. Production sends it "María-José Fernández de Córdoba", 1,284 members and a sidebar that leaves 280px.
Pick a scenario and watch what breaks.
What breaks: The name pushes the badge and the ••• menu out of the box.
Stress what can actually reach it
A is one hostile but realistic input. The trick is to keep only the ones the component can really receive:
| Axis | Keep it when | Try |
|---|---|---|
| Content length | it shows text it didn't write | empty, one word, a paragraph, a long unbreakable URL |
| Content shape | users or other languages write the text | emoji, right-to-left, accents |
| Quantity | it repeats over items | zero, one, the usual count, ten times that |
| Container | always | 320px, squeezed by a sibling, very wide |
| State | it gets the state as a prop | loading, error, disabled |
A static button with a fixed label gets container widths, never long text.
Every hostile value is realistic or the real limit. A hyphenated, accented surname. A 60-character work email. The exact maxLength from the form. Random keyboard mash gets dismissed as "never happens", and the conversation ends.
Read the failure signature
Most breaks have a known cause and a one-line cure:
| You see | Cause | Fix |
|---|---|---|
| avatar squished into an oval | a flex child shrinking | flex-shrink: 0 |
| text pushes the menu out of the box | min-width: auto on the text column | min-width: 0 |
| an email runs off the edge | no place to break | overflow-wrap: anywhere |
"1 members", NaN | a hardcoded plural, a raw number | Intl.PluralRules, a null guard |
| 1,000 rows stutter | every row rendered | virtualize or paginate |
Build the harness
A throwaway page, scenarios side by side, each in a fixed-width box. Feed the data at the data boundary (the fetch or the hook), never by editing the component.Look once, report
Name what broke and the node that owns each fix.Keep the worst-case fixture
Delete the page; keep the data. It's the regression test for the next person.
Dark mode, browser zoom and reduced motion are the user's toggles. Name them for the person to check; don't fake them on the page.
Stress tests run isolated. Design variants run in context. Don't swap them.Stress-test components/MemberRow.tsx. List every value it renders with its real limit (schema, maxLength, API type); "unbounded" is a finding. Build one worst-case fixture shaped like our demo data, plus Empty, One and 1,000 rows. Render them side by side at 320px and our real width. Report what broke and the fix. Don't fix yet.
The production baseline
Before launch, dozens of findings tend to share one root: the app never set its global floor. The is that floor. Land it once and whole classes of bugs vanish.
In the head:
- One viewport tag:
width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content. Nevermaximum-scale; fix the 16px inputs instead. <html lang>set to the real language.- Declare only the colour schemes you designed. A light-only app that says
color-scheme: light darkgets dark native inputs on white cards.
In the root CSS:
font-synthesis: none, so a weight that never loaded fails visibly instead of being faked.- A
:focus-visibleoutline, 2px, withoutline-offset: 2px. - Inputs at 16px on touch;
touch-action: manipulationon controls. text-wrap: balanceon headings,prettyon paragraphs.
Two gates: every :hover behind (hover: hover) and (pointer: fine), and reduced motion handled where the shared durations live. Dev tools mount only in development, through a dynamic import, so their code stays out of the real bundle.
The ship check
A review judges one change against a craft bar. A sweeps the whole app against a ship bar and fixes as it goes. Its question is blunt: what breaks in the first hour?
It goes an inch deep on nine lanes: accessibility, performance, mobile, forms, states, motion, theme, leftovers (lorem, debug logs, secrets) and marketing (titles, link previews). Anything bigger than a few lines goes on a list for a human.
The severity tree
Walk it top to bottom and stop at the first yes.
- YesDoes it lock a group out? (keyboard, screen reader, low vision, motion, touch)
- –Will a typical user hit it in their first session?
- –Does it leak secrets, debug output, dev tools or fake content?
- –Is it visible only on another device, theme, network or data state?
BlockerFix it before launch. It goes at the top of the fix order.
Context can override a lane's default. A missing alt on the only product photo is a blocker. 100vh on a desktop-only admin tool is nothing.
Fix order
Accessibility blockers, then leaks, then the baseline (one change clears dozens), then per-file fixes, then polish while the diff is still readable in one sitting. Then run the typecheck and build. A ship check that breaks the build is worth less than none.
Three ways to fail
- Calling it ready from a laptop. Sticky hover, input zoom, the phone keyboard and safe areas never reproduce in a desktop browser. Say which checks need a real phone.
- A report where a diff belonged. Sixty rows of advice isn't launch prep. Apply the mechanical fixes.
- Redesigning in launch week. New palettes and re-timed motion are taste. Fix defects; taste goes on the human list.
"Already in good shape, small diff" is a valid, good result.
Quick check
During a ship check you find cards losing their border, but only in the dark theme. Using the severity tree, what is it?
In the skill
The nodes your agent reads when it does this work.
- Stress scenariosRender one component under every scenario that can actually reach it — content length and shape, quantity, container width, state — each axis kept only when its cue matches, side by side on a throwaway harness page that is itself the report. Product states (data, account, feature) come from branches the code already has, fed at the data boundary. Viewing modes are named for the user to toggle, never simulated.
meta/stress-scenarios - Production baselineThe floor any app a phone will open needs — one viewport tag, a handful of root declarations, two gates (hover and reduced motion) moved to token or config level, a theme provider that neither flashes nor ripples, and dev mounts behind a build-time check. Merge it in; never fork it.
meta/production-baseline - Ship checkThe last pass before launch sweeps the whole app, not one diff, against a ship bar — nine lanes, a four-question severity tree, a fix order that never masks a finding, and an honest split between what code proves and what only a phone or a person can.
meta/ship-check
Connected lessons
What this lesson builds on and leads to, across every course.