
A page migration that left the legacy behind
InvO
People had learned our expenses list, and still clicked where it doesn’t work. How to pay down design debt on a page migration without breaking what they already know.
People had learned our expenses list. They still clicked where it doesn’t work.
That is what the session recordings showed me and a number on its own wouldn’t have. Even through a pattern they had learned, people kept falling back into the bigger pattern — the one every other table on the internet honours, and ours didn’t.
FakturaOnline is moving its front end from Vue to React, page by page, onto a new design system, and Výdaje was one of the first pages to go. Which makes the whole thing one hard question: how do you pay down design debt without breaking what people have already learned?

What Clarity showed. On the expenses section, 24.5% of clicks were dead, twice the invoice list’s rate. The hot spot was the file upload on the expense form. On the invoice list, which uses the same table, around 42,600 clicks a month landed on rows that did nothing.
Where we broke the global pattern
Three places, one defect.
The row. Production makes only the number clickable. That is where the 42,600 goes. Nobody there is confused — they are doing what every other table taught them.
The phone. The row opened from a 30 px arrow and nowhere else. Same defect, smaller target.
The layout. Production picks the mobile layout by sniffing the user agent, so a desktop browser dragged narrow gets the desktop table and a sideways scrollbar nobody designed.
All three now do the ordinary thing. The whole row opens the record, the whole row is the tap target, and the layout follows width at the same breakpoint as the rest of the system. The row click stays a convenience on top of a real link, because a row is not a link and keyboard users would lose exactly what the mouse users just got.
Even through a pattern they have learned, people fall back into the bigger one. If you never built that one, it just looks like they can’t click.
And where we were only sloppy
The rest isn’t a broken pattern, it is accumulation. Nobody had ever defended any of it.
Change | Before | After |
|---|---|---|
Sort arrow | Could point the wrong way | Always matches the order |
Default columns | Eight, half of them for the accountant | Six, with status; the rest in settings |
Column settings | No way back to the default | An “Obnovit výchozí” (restore default) link |
Empty cells | Blank, as if it failed to render | A light grey dash |
The old default had eight columns and neither of the two I’d call load-bearing: no total with VAT, no status. That set was never decided; it accumulated. The new one is what fits the container without a sideways scrollbar, and the four that came off are accountant work — the VAT date, the net amount, two tax flags.
Trade-off. The accountant now fetches those four columns every session so that the sole trader who never opens them stops scrolling sideways. I took the deal because the accountant knows what she is looking for and the sole trader does not.

Which is why column settings also got an “Obnovit výchozí” link, and why locked columns stay in the list as disabled, checked rows instead of vanishing from it.
Same widget, honest numbers
The summary bar above the list keeps its shape: a total with a donut and one tile per status. What changed is how it behaves.
Change | Before | After |
|---|---|---|
Status tile | Only the 16 px checkbox is clickable | The whole tile toggles |
Total and donut | The whole period, ignoring filters | Only the statuses left on |
Tile with zero | Can’t be switched off | Works like any other tile |
Donut tooltip | The chart library’s default | The design system’s tooltip, named like the tile |
The total was the one number on the bar that lied about the table right under it.
Making the total follow the filters is obvious right up to the moment you try the same thing on the tiles — switch Zaplaceno off and five of the six read 0 Kč, which removes the only thing you were choosing between. So the total and the donut describe what is in the table, and each tile keeps describing its own status whether it is switched on or not.

The filter I didn’t build
Same rule, pointing the other way. Faceted filters are where our lists are heading and I had already built the chip and a working faceted row. Then I checked the filters we have: on the invoice list, 100,000 views produced zero dead clicks on the status tiles.
Nobody is falling back to anything there. People have learned that control and it works for them, so it is not debt — it is just not what I would design today. Our core user issues one or two invoices a month and doesn’t want to relearn a screen.
Trade-off. The lists keep a filter model I have already outgrown, and finished work sits unused in the file. What it buys is that nobody relearns the one control they touch every time.
The hard part wasn’t the decision. It was looking at a finished component I can’t use.
For the record, the thing that actually bothers me on this screen is the filter zone, and that the buttons aren’t in one place — there is one sitting right next to the heading. That is not this stage’s job, so it isn’t in this ticket.
Handoff
Every rule above lives in the design system repo twice: a .md spec written for a person, and a .manifest.json the repo validates. A developer builds from those two files without opening Figma, and CI checks the prose and the machine-readable facts still say the same thing. The spec also records what production does today, next to what the component does instead — a rule that only exists in a Figma frame gets read as a suggestion.
And the gaps are written down as gaps. This pass has no loading state, no first-use empty state, no “couldn’t load the list” frame. I’d rather hand over a spec that says a state is missing than one that invents it, because an invented state ships.
What it changed, and what I don’t know
Every table I build now comes out of one component, so screens come together faster (and all my tables are pretty now). There are no after-numbers, because the page isn’t in production. That is not a small asterisk.
What I’d do differently: go to the recordings earlier, and with the question already written down. The aggregate numbers told me where to look and nothing else.
Dead clicks find the spot and can’t tell you a thing about why. I knew that. Did the fast thing anyway.
Until it ships, this is a very well-documented hypothesis.
Also I did facelift for landings, but its a whole new story… 👀


