polish
Polish is not adding things. It is the list of things that are already wrong that nobody has looked at yet. Run it top to bottom; fix everything you find; do not skip an item because it seems small. Small is the point.
Document and metadata
-
<title>is specific per page: "Settings — App", not "App". Updates on client navigation. - Title reflects state where useful: an unread count, an unsaved dot ("• Untitled").
- Favicon: SVG with a dark-mode variant via
prefers-color-schemeinside the SVG, plus a 32px PNG fallback and a 180px apple-touch-icon. - Open Graph and Twitter images exist, are 1200×630, and show the page's title in the site's type, not a generic logo.
-
theme-colormeta for both schemes so the browser chrome matches. -
langon<html>.color-schemeon the root. - Canonical URLs; no duplicate content at
/pageand/page/. - A 404 page in the app's frame, with a way back and a search if the app has one.
Text
- Sentence case in every button, label, heading and menu item.
- No orphan words on titles:
text-wrap: balance. - Tabular numerals on every number in a column, counter, price, timestamp or shortcut.
- Real apostrophes and quotes (’ “ ”), en dashes for ranges, ellipsis character not three dots.
- Empty states, error messages and loading text are written, not placeholder.
- No "Lorem", no "TODO", no
console.login production. - Dates are formatted for the locale, relative when recent ("4m ago"), with the absolute time on hover.
Colour and surface
-
::selectionset to match the palette. - Scrollbars themed:
scrollbar-width: thin; scrollbar-color. - Focus ring visible on every surface: test on the darkest and lightest backgrounds.
- Both themes reviewed on every screen. No
#ffftext on#000. - No flash of the wrong theme on load.
- Form controls,
<select>popups and date pickers match the theme (color-scheme). - Disabled states are visibly disabled but still legible.
Layout
- Nothing shifts on load: images have dimensions, fonts have metric fallbacks, skeletons match content.
- Sticky header offset applied to anchors and focus targets (
scroll-margin-top). - No horizontal scroll at 375px.
- Side gutters 16px on phones, never 0.
- Safe areas respected for fixed bottom elements.
- Long content tested: the longest name, the longest error, 0 items, 10,000 items.
- Scrollbar gutter compensated when a dialog locks scroll (no 15px jump).
Interaction
- Every button has
:activefeedback. - Hover effects gated to
@media (hover: hover). - Every icon-only control has an
aria-labeland a tooltip. - Keyboard shortcuts shown somewhere visible: tooltips, menu items, a footer hint.
- Copy actions confirm inline (icon swap) and handle clipboard failure.
- External links open in a new tab with an arrow marker and
rel="noopener". - Links have
cursor: pointer; drag handles havegrab. - Escape closes the top layer; focus returns to the trigger.
- Double submit prevented.
- Toasts pause on hover and when the tab is hidden.
Motion
-
prefers-reduced-motionhandled globally and nothing is hidden behind an animation that never runs. - No
transition: all. - Entrances ease out; exits are shorter.
- Nothing animates on every re-render (check list filters, tab switches, sorts).
- Page-load animations happen once, on the first paint, not on every navigation back.
- No animation on keyboard-driven actions.
Icons and imagery
- One icon set, one stroke width,
currentColor. - Icons optically aligned with text (usually 1px up).
- Decorative icons
aria-hidden. - Images:
altwritten,loading="lazy"below the fold, modern formats,srcsetfor retina. - Avatars have a fallback (initials on a neutral surface) that matches the size exactly.
- Favicons for external items fetched from the site's own declared icon, with a fallback letter tile.
Performance you can feel
- Fonts preloaded (the two weights above the fold),
font-display: swapwith metric overrides. - Route transitions do not white-flash; the frame persists.
- Interaction to next paint under 100ms on a throttled CPU for the primary actions.
- Large lists virtualised; large images not decoded on the main thread on scroll.
-
content-visibility: autoon long below-the-fold sections. - No layout thrash from measuring in loops.
Machine readers
- Headings form an outline (one
h1, thenh2…). - Landmarks:
header,nav,main,footer, witharia-labelwhen there is more than one of a kind. - A skip link to main content.
- Structured data if the content is a list, article or product.
-
robots,sitemap, and if the site is a resource,llms.txt.
The no-JavaScript and slow-network pass
- Every word and link is in the HTML. Turn JavaScript off and read the page.
- Elements hidden for a reveal animation are visible without JS (
html:not(.js) [data-reveal] { opacity: 1 }). - Throttle to Slow 3G once and watch the load order. Nothing critical arrives last.
Ship gate
The interface is finished when you can:
- Tab through every screen without a mouse.
- Use it at 375px with one thumb.
- Switch theme on every screen and nothing looks wrong.
- Turn on reduced motion and nothing is missing.
- Read every state: loading, empty, error, done.
- Find no value that is not a token: colours, spacing, easing, duration, radius.
From the catalogue
- Details That Make Interfaces Feel Better — the spirit of this list.
- HTML Can Do That — behaviour that ships in HTML before JS.
- web.dev — Core Web Vitals and the measurements behind "feels fast".
- Favicon and OG tools — generate and check the assets.
- Lighthouse, axe, and the checks — run before shipping.
- More at stealth.pm/tools.