Layout and labels

Layout and labels

The rules a page must follow. Added 2026-07-28 after a review of the Social Media Planner found four different treatments doing the same job on one screen, and no written rule to point at.

Case: sentence case, everywhere

Buttons, labels, headings, navigation, card titles and menu items are all sentence case. “Save draft”, not “Save Draft”. “Publish now”, not “Publish Now”.

Title Case is retired. There is no consistent rule about which words take a capital, so two authors produce two results, which is exactly how the planner ended up showing “Publish now” and “Publish Now” for the same action on the same screen.

The one exception is the eyebrow, which is uppercase. Nothing else is.

Two supporting reasons, both practical:

  • Satoshi has a large x-height and open counters, so lowercase carries the legibility at the 13-14px sizes most of our interface uses. Sentence case reads larger and faster than the same string in caps at the same size.
  • All-caps removes word shape. Readers recognise familiar words by their ascender and descender silhouette; caps flatten every word to a rectangle and force letter-by-letter reading. Invisible over one or two words, costly beyond that. That is why the eyebrow is capped at about three words.

Never type capitals into markup. Write sentence case and apply text-transform: uppercase in CSS. Some screen readers spell out genuinely uppercase strings letter by letter. The eyebrow utility already does this correctly.

Comedik is never uppercased. A hand-drawn face depends on its irregular lowercase for its character; capitals flatten it and read as shouting.

Headings: eyebrow or heading, not both jobs

Each of these does one job. Pick by role, not by how big you want the text.

Element When Treatment
Eyebrow Where am I, above a headline Uppercase, tracked, muted lavender, max ~3 words
Page headline The page’s one h1 Bold ink, page-header component
Card title Names a panel Bold ink h3 element at the small step (14px/700), sentence case. Small deliberately: it names a panel rather than heading a page, and at the h3 size it competes with the page headline
Sub One sentence of context Muted, capped at 70 characters wide
Hint Guidance attached to a control Italic muted, small

Sibling cards on a page must use the same header treatment. A card with a bold ink title beside a card with a muted uppercase label reads as two systems bolted together, and a card with no header at all beside cards that have one reads as unfinished.

Use real heading elements. A div styled to look like a heading gives screen-reader users nothing to navigate by and silently removes the page from the document outline.

Spacing: pick a step, never a number

Spacing comes from the 4px scale. The named steps below are the only ones a page should reach for.

Use Step
Inside a card space-5 (24px)
Between cards space-5 (24px)
Between a label and its control space-2 (8px)
Between stacked text in a card space-3 (12px)
Between major page sections space-6 (32px)

Hand-picking a value per page is how card padding across the estate ended up ranging from zero to 30px, and how one selector ended up set to both 24px and 12px in the same file.

Prefer a container that owns the gap (.ts-stack, .ts-pill-row, or grid and flex gap) over margins on each child. Per-child margins collapse, double up, and leave the spacing dependent on which element happens to be first or last.

Pill and chip rows must set align-items: center. A flex row defaults to stretch, which inflates a small chip to the height of the tallest pill beside it. Use .ts-pill-row.

Empty states live inside the card

An empty card keeps its frame, its title and its place in the layout. Inside it, three things:

  1. What would be here, stated plainly.
  2. Why it is empty, if that is not obvious.
  3. The action that fills it.

“No posts scheduled for this date.” satisfies none of these. It is a dead end at exactly the moment the user’s intent is clearest, since they have just selected the date.

Selection means one thing

Everything selectable uses the same treatment: brand border on the selected item. Mode cards, calendar day cells, platform tabs and toggles all share it.

Do not introduce a second selection language, such as a filled square with sharp corners in a system otherwise made of 12px cards and full pills.

Colour roles are roles, not decoration

Green is the full-auto action. Orange is secondary. Purple is everything else. Red is danger, and danger buttons additionally carry the hazard tape edge so that danger is distinguishable from secondary without relying on hue alone: red and orange are precisely the pair colour-blind users lose.

The most consequential action on a page must not be quieter than a lesser one beside it. Check this by role, not by taste.

Contrast is checked at two thresholds

Every colour that carries text ships as a background and foreground pair, and both of these must hold:

  1. Label on fill, at least 4.5:1.
  2. Fill against the page behind it, at least 3:1.

Checking only the first is how the old palette passed review and still failed in practice: buttons whose edges vanished against the page.

Injected UI: our controls inside someone else’s page

The extension puts controls inside YouTube’s own interface. Two rules, both learned the expensive way while injecting into their community composer.

Be compact by default. A host’s control row is usually a wrapping flex container, so every pixel added can reflow controls we do not own. An icon with a label matched YouTube’s rhythm perfectly and pushed their own Video option onto a second line. Breaking the host’s layout is a worse outcome than our button being unlabelled, so injected controls start icon-only and earn a label only where there is provably room.

Take colour from the host’s token, never inherit. Our marks fill with currentColor, and inherit resolved to a value with no contrast against YouTube’s surface, so the button rendered invisible while its own panel rendered correctly. Pinning to the host’s variable, --yt-spec-text-primary in that case, fixed it and follows their light and dark themes for free.

The general rule behind both: inside someone else’s page, their layout and their tokens are the environment. Ours apply to our own surfaces only, and an injected control that ignores the host is the one that breaks.