How I Compose a Screen
Every part can be correct and the screen still be wrong. Nothing automatic catches that, so I write the rules down.
A screen can be made entirely of approved parts, use only the colours from the palette, only the type sizes from the scale, only the words from the translation file, and still be wrong. Every piece passes and the assembly fails.
This surprises people who expect tooling to catch it. Tooling checks values: is this colour allowed, is this text translated, is this spacing a real number. Assembly is about relationships between things, and a relationship isn’t a value. A spell-checker will happily pass a sentence that means the opposite of what you intended.
So I keep a short book of rules about assembly. Where things sit and why, rather than what colour they are. Here’s most of it, in plain language.
Copy the nearest screen
Before building a new screen I open the closest one that already exists and copy its skeleton: where the header sits, what holds the scrolling, how much air is at the top. The content is mine to invent, the frame isn’t.
The reason is mundane and important. A batch of screens built in one sitting, by anyone, without looking sideways, will quietly invent its own skeleton. Every screen in the batch agrees with the others and disagrees with the whole app. Nothing warns you, because each one is internally consistent. Six months later somebody calls it “inconsistent design” and nobody can point at the line where it went wrong.
One owner per kind of space
In my screens the outer container owns the vertical rhythm. The gap between blocks is set once, by the container, for everybody. Side margins belong to a single component whose only job is side margins, and a block inside never adds its own.
When more than one thing can add space, they all do, and the gaps double. The screen next door, built the same week by the same person, doubles them somewhere else. Now you have two screens with the same parts and different breathing, and no way to say which is right.
Two ways to group rows
There are exactly two ways I group a list of rows. One is a full-width band that runs edge to edge with a hairline above and below, for a screen that’s one long list. The other is a raised card, inset from the edges, sitting on a slightly sunken background, for settings-style forms. In the second one the card edge does the grouping, and the gap of background between two cards says “different group.”
Each one answers a different question, so they don’t mix. A screen that uses both reads like two design systems bolted together, and people feel that before they can name it. Pick one per screen. If you want the other one, you probably want a different screen.
Align to the title, not the middle
This is the one I care most about, because it’s invisible until you see it and then you can’t unsee it.
A row usually has a title, sometimes a supporting line under it, and things around them: an icon on the left, a value or a switch on the right. The instinct is to centre all of that vertically in the row. That’s correct for a single-line row and wrong the moment a second line appears.
Here’s why. Put a small title over a small supporting line and the centre of that two-line block sits about eleven points lower than the centre of the title. The icon drops into the gap between the two lines, where it points at neither one. And if the supporting line only appears on some rows, your icons sit at two different heights down the column, which is the part people actually notice.
So everything aligns to the first line of the title. The title is what the row is about, and the supporting line hangs below it the way a footnote hangs below a sentence.
One extra rule with it. Work the offset out from the line height rather than typing in the number it happens to equal today. People can make text bigger on their phone, the line box grows, and the icon doesn’t. A number you froze is a number that drifts out of alignment for exactly the person who needed it larger.
What a button does decides where it sits
A panel slides up from the bottom of the screen. Where does its button go?
It depends on what pressing it does. A button that only dismisses, the “Done” you press because you’ve finished looking at something, goes in the top corner, small and quiet. A button that commits what you typed or chose goes at the bottom, full width, large. It belongs at the end of the thing it submits, where your eye already is after the last field. And a greyed-out full-width slab says “you can’t do this yet” far more plainly than a greyed-out word in a corner.
I keep one exception, a short form whose commit button sits in the corner anyway, because its body is three lines long and putting the button at the foot would mean putting it somewhere no panel before it had one. The exception has a reason written next to it. That’s the difference between an exception and drift.
Where a message goes: four questions, in order
Something has gone wrong, or changed, and you have to tell the person. There are four places to say it, and I ask four questions in this order and stop at the first yes.
Is there nothing to show? The screen could not load the thing it exists to show. There’s no content for a message to sit beside, so the message is the screen: a centred block with one button to try again.
Is one particular value wrong or missing? Then the message goes at that value, under that field, in that row. Only the field knows where the person has to go. A message at the top of the page saying “something is wrong below” turns a fix into a hunt.
Is it still true while they’re looking at the screen? You’re offline, this data is old, sending is paused. That’s a condition rather than an event, so it gets a strip across the top that stays as long as the condition does.
Is it over? Nothing to decide, nothing still broken, the result already visible behind it. That’s a little message that appears and leaves on its own.
Four tie-breakers I learned by getting each one wrong:
If the thing outlives about four seconds, it can’t be the disappearing message. “You are offline” is a condition, and a message you dismiss by waiting must never be the only record of something still true.
The disappearing message carries no recovery button. A button on a timer is a race, and the person who loses the race has lost their only way out. Undo is the single exception, because letting it expire is a valid answer.
If the screen already has the button that would fix it, the strip states the reason and offers nothing. Two buttons for one action read as two different actions.
And a strip’s button is one word. It sits beside the text, so the two share a single row of width, and every extra word in the button is a word taken off the sentence. “Reload” says what “Load the latest version” said and gives the sentence its width back.
The same claim gets the same surface everywhere
Whatever surface a kind of message gets, it keeps everywhere in the app. Old data is a strip on this screen and a strip on that one. A completed save is a disappearing message here and a disappearing message there.
The temptation is to decide it per feature, because each feature arrives as its own ticket with its own text written by someone who wasn’t designing a system. Deciding per ticket is how one app ends up speaking two languages about the same fact.
Widen the row, don’t copy it
When a row needs to be slightly different, a switch on the right instead of a value, a chevron that turns instead of pointing, the tempting move is to build a new row.
I’ve paid for that one. One row anatomy had been copied into six files. When the alignment rule above turned out to be wrong, it was wrong in six places, and fixing it in the original fixed nothing in the other five. Nothing detected the copies either, because a copy that shares no name shares nothing a tool can count.
So the one row gets wider rather than mirrored. If a setting genuinely can’t express the shape, that’s worth a conversation before it’s worth a new file.
When the layout flips
The app runs in Arabic, so the whole layout mirrors. Navigation, back arrows, chevrons, which side the floating button sits on.
What doesn’t flip is anything with its own internal order. Digits, phone numbers, clocks, durations, media controls, checkmarks, logos. A clock isn’t a sentence and doesn’t read backwards in any language.
The reason this belongs in a post about composition rather than one about translation is that mirroring only exposes an assembly you were never explicit about. If you nudged something to the left with a hard-coded number, it’s still on the left in the mirror, pointing the wrong way. Build a screen out of relationships rather than positions and it mirrors for free.
Why any of this is written down
None of these rules can be checked automatically, and I’ve made my peace with that. They’re all about a relationship between two things, and the tooling I have can only see one thing at a time.
What’s left is review, and review only works if the rule exists somewhere outside the head of the person who made it. A rule you can point at settles an argument in one line. A rule you only feel takes an afternoon, twice.
So the book lives next to the screens, and every rule in it has the mistake that produced it written underneath. The mistake is the part that makes people believe the rule.
Summary
- Copy the skeleton of the nearest existing screen before inventing one.
- Give each kind of spacing exactly one owner.
- Group rows one way per screen. Edge-to-edge band or inset card, not both.
- Align a row’s icons and values to the title’s first line, not the middle of the row.
- Let the button’s job pick its place. Dismiss in the corner, commit at the foot.
- Choose a message’s surface with four questions: nothing to show, one value wrong, still true now, already over.
- Give the same kind of claim the same surface on every screen.
- Widen the row you have instead of copying it.
- Build from relationships, not positions, so the screen mirrors on its own.