Rules for every developer
Design Implementation
Standard
How to build a website from a Figma design. The Figma file gives you the exact values — this document tells you what to check, what Figma cannot show, and the minimums we never break. Every rule below comes with a wrong vs. correct example.
The 3 Golden Rules
The Figma design gives you the exact values. This document tells you what to check, what Figma cannot show, and the minimums we never break.
1.1Copy, don’t guess. Every value in your CSS must exist in Figma. Use Dev Mode to read the exact values. If something is missing — ask the designer.
Card title
Padding eyeballed from a screenshot. Every side is different.
Eyeballed values: uneven padding, a colour that is "close enough".
Card title
Padding and colour read directly from Figma Dev Mode.
Exact values read from Figma Dev Mode — even spacing, the real token colour.
1.2Consistency comes first. The same element must look and work the same on every page. If Figma shows two versions of a component, ask the designer — don’t build both.
home / pricing / about — three different buttons
The same "Contact" button built three different ways on three pages.
one Button component — identical everywhere
One Button component — identical on every page.
1.3Minimums are never broken. If the design breaks a minimum (contrast, click size, text size), tell the designer before building. We never ship below the minimums.
This is 12px light-grey body text on white, built exactly as drawn. Most visitors physically cannot read this paragraph.
12px light-grey text built "as drawn" — try to read it.
This is 16px body text with passing contrast. Comfortable to read for everyone.
Flagged to the designer, fixed to 16px with passing contrast.
Design Tokens
Rule: zero one-off values. Every value in code comes from a named token in Figma.
2.1Turn every Figma style (colours, fonts, shadows) into a token in code: CSS variables or Global Colours & Fonts.
Five slightly different blues invented across the site.
One named token — every element uses the same value.
2.2If a value appears more than once, it must be one token used in both places — never typed twice by hand.
Button and link were "the same blue", typed twice — they drifted apart.
Both read the same token — they can never drift.
2.3About to type a raw hex code or pixel value? Stop. Either it exists as a token (use it) or it shouldn’t exist (ask the designer).
A border colour invented on the spot — now a 6th grey exists.
The existing Gray/200 token is used — or the designer decides.
Spacing
Elements must have breathing room — never cramped.
3.1All spacing must sit on the design’s spacing scale (usually steps of 4 or 8). If Figma shows an odd off-scale value, confirm with the designer.
Random gaps: 13px, 27px, 19px — nothing lines up.
Everything on the 8px scale: 8, 16, 24 — visual rhythm.
3.2The vertical space between page sections is the same across the whole site: one desktop value, one mobile value. No section quietly gets "a bit more".
Every section has a different gap before it.
One section-spacing token — identical rhythm all the way down.
3.3The gap between a heading and its text, between cards, between an icon and its label — each is one consistent value site-wide.
Heading → text gap
Our Mission
We build products people love.
Our Team
A group of passionate designers and engineers.
Paragraph → paragraph gap
Great design starts with empathy. Understanding the people who use your product is the single most important step.
Consistency builds trust. Every screen that behaves the same teaches users how the product works.
Clarity over cleverness. A simple sentence beats a clever one every time.
Heading→text: 18 px on one section, 3 px on the next. Paragraph→paragraph: 22 px then 6 px. No rhythm, no rule.
Heading → text gap
Our Mission
We build products people love.
Our Team
A group of passionate designers and engineers.
Paragraph → paragraph gap
Great design starts with empathy. Understanding the people who use your product is the single most important step.
Consistency builds trust. Every screen that behaves the same teaches users how the product works.
Clarity over cleverness. A simple sentence beats a clever one every time.
One heading-gap token (6 px) and one paragraph-gap token (12 px) — the same on every section, every page.
3.4Make spacing only with margin, padding, or gap. Never with empty divs or <br> tags.
Our Services
We offer web design and development.
Two <br> tags and a spacer div create a random, unmaintainable gap.
Our Services
We offer web design and development.
A single margin token — predictable and consistent.
3.5Related elements sit closer together than unrelated ones. If the design made two gaps different on purpose, keep them different.
Too close — no separation between topics
Design Principles
Good design is invisible. It solves problems without drawing attention to itself.
Typography Rules
Limit your page to two typefaces. One for headings, one for body copy.
Colour Usage
Use colour purposefully. Every colour choice should communicate something.
Too far — headings feel disconnected from their text
Design Principles
Good design is invisible. It solves problems without drawing attention to itself.
Typography Rules
Limit your page to two typefaces. One for headings, one for body copy.
Too close: all gaps 2 px — headings and paragraphs merge into one unreadable block. Too far: 20 px heading→text — the heading feels orphaned from its own content.
Correct grouping — tight within, loose between
Design Principles
Good design is invisible. It solves problems without drawing attention to itself.
Typography Rules
Limit your page to two typefaces. One for headings, one for body copy.
Colour Usage
Use colour purposefully. Every colour choice should communicate something.
5 px heading→paragraph keeps them as a unit. 20 px section→section separates topics. The eye groups instantly.
3.6Menu items, buttons, and links must never sit so close together that they look like one element or are hard to tap separately. If the space in Figma looks too tight, flag it — don’t just shrink gaps to fit.
Menu items squeezed together — they read as one long word.
Comfortable gaps — each item is clearly its own target.
Layout & Breakpoints
4.1Build the exact grid from Figma: one container width, one column count, one gutter width — for the whole site.
home · 1240px
about · 1180px
contact · 1300px
Every page has its own container width — content jumps as you navigate.
home · 1200px
about · 1200px
contact · 1200px
One container token — content lines up on every page.
4.2Content never touches the edge of the screen. The side padding applies at every width.
narrow viewport
At container width, text slams into the screen edge.
narrow viewport
Side padding keeps content off the edge at every width.
4.3On very wide screens, content stops at the container width. Backgrounds can go full-width if designed that way.
27" monitor
On a wide monitor, text stretches edge to edge — 40-word lines.
27" monitor
Background spans full width, text stops at the container.
4.4Breakpoints are defined once for the whole site. Never different breakpoints per page.
Each page switches to mobile at a different width.
One set of breakpoints — every page switches together.
4.5Cards in a row get equal height with flex or grid — never manual pixel heights.
Web design
Comprehensive enterprise cloud infrastructure migration services
SEO
Cards end at different heights the moment content differs.
Web design
Comprehensive enterprise cloud infrastructure migration services
SEO
Grid stretches every card to the tallest — automatically.
4.6Check the widths Figma doesn’t show (drag the browser window). The layout must never break between the designed sizes.
900px — nobody looked here
Tested only at 375px and 1440px — at 900px the nav overlaps the logo.
checked at every width, 320 → 1920
Dragged from 320px to 1920px — every in-between width holds.
Alignment
Elements that sit together must line up on a shared axis — nothing floats a few pixels off.
5.1An icon next to a single line of text is vertically centred with it. Use flex with items-center — never nudge the icon with manual margins.
Icons nudged with magic-number margins — each one sits at a different height.
flex + items-center — every icon is perfectly centred with its text, automatically.
5.2An icon next to multi-line text aligns with the first line — not centred against the whole block.
Your subscription renews on 12 August. You can cancel any time before the renewal date from your account settings.
Icon centred against the whole paragraph — it floats next to no line in particular.
Your subscription renews on 12 August. You can cancel any time before the renewal date from your account settings.
Icon aligned to the first line of text — it clearly belongs to the sentence.
5.3Controls that sit in one row (inputs, buttons, dropdowns) share one height and one baseline — they must line up as a single strip.
Input, select, and button all have different heights — the row looks broken.
One shared height token — the row reads as one clean strip.
5.4The same element across sibling cards aligns across the row: titles on one line, buttons on one line — regardless of content length.
Basic
For individuals.
Pro
For teams that need collaboration, roles, and advanced reporting.
Buttons land wherever the text ends — a different height in every card.
Basic
For individuals.
Pro
For teams that need collaboration, roles, and advanced reporting.
mt-auto pins every button to the bottom — one straight line across the row.
Container
The whole site lives inside one shared container. Header, every content section, and footer all start and end on the same vertical lines — the same max-width and the same left/right padding everywhere.
6.1Define one container (max-width + horizontal padding) and reuse it for header, main content, and footer. If the header has 20px side padding, everything else has exactly 20px — never more, never less.
Header, content, and footer each use their own padding — the left edge zig-zags down the page.
One container class everywhere — logo, headings, cards, and footer all share the same edge.
6.2Content never overflows the container. Cards, images, and text blocks stay inside the same gutters as the header — nothing sticks out past the shared edge.
A card is wider than the container — it breaks out past the header edge and causes horizontal scroll.
Everything stays inside the container — full-bleed backgrounds are allowed, but their inner content still aligns to the shared edge.
6.3Full-width backgrounds are fine, but the content inside them still uses the shared container. Put the background on the outer element and the container on the inner one.
The banner’s text hugs the screen edge — it ignores the container the rest of the page uses.
Background spans edge-to-edge, but the inner content starts on the same line as the header logo.
Inner Padding
Every element inside a container needs breathing room on all four sides. A logo flush with the top of the header, text kissing the card border, or a button icon pressed against its own edge all look broken and unfinished.
7.1Header content (logo, navigation, CTA) must have equal top and bottom padding inside the header bar. The logo must never touch the top or bottom edge of the header.
Logo and nav sit flush with the header edges — no breathing room, looks clipped.
Consistent vertical padding inside the header gives the logo and nav clear space on all sides.
7.2Text, icons, and controls inside a card or panel need inner padding on all four sides. Content must never start at the very edge of its container.
Plan your next trip
Discover destinations, hotels, and experiences tailored to you.
Heading and body text begin at the card edge — feels cramped and unfinished.
Plan your next trip
Discover destinations, hotels, and experiences tailored to you.
Uniform inner padding on all four sides gives content room to breathe inside the card.
7.3Button labels and icons must have horizontal and vertical padding inside the button. The text must never touch the button border.
No horizontal or vertical padding
icon and label touch top, bottom, left, and right border
Label fills the entire button width — text touches the left and right borders.
px-5 py-2 (20px / 8px)
label has clear space from every edge
Horizontal padding creates a safe zone so the label never crowds the button edge.
7.4Apply the same rule to every container: modals, tooltips, badges, dropdowns, input fields. The content inside always has a safe inner margin.
0 px inner padding
Tooltip text and badge label start right at the container border — no padding at all.
Consistent inner padding
Inner padding on every container type — consistent safe zone regardless of size.
Text
8.1Use only the text styles from Figma — exact fonts, sizes, weights, line-heights. No in-between sizes.
Heading, 23px?
Body at 17px — Figma has 16 and 18, but 17 "looked better".
A 15px semi-semi-bold caption.
In-between sizes invented per page — 17px, 15px, weight 550.
Heading / 24
Body / 16 — exactly the Figma text style.
Caption / 14 Medium.
Only the defined scale — every size maps to a Figma style.
8.2One <h1> per page. Never skip heading levels. The HTML tag follows meaning; the CSS makes it look like the design.
Welcome h1
Our Services h1 again
Web Design h4 — skipped h2, h3
Two h1s, then a jump straight to h4 — the document outline is broken.
Welcome h1
Our Services h2
Web Design h3
h1 → h2 → h3 in order — CSS controls the look, HTML the meaning.
8.3Body text is never smaller than 16px. Keep paragraph lines a comfortable reading width, even inside wide sections.
This paragraph is 13px and runs the full width of its container with no maximum line length, which makes every line very long and very hard to follow back to the start of the next line, especially on wide screens where a single line can hold well over two hundred characters of text.
13px text across the full width — exhausting to read.
This paragraph is 16px with a comfortable maximum line length. Your eyes flow easily from one line to the next.
16px minimum, ~65 characters per line.
8.4Load only the font weights you actually use. No fake bold or italic. Use font-display: swap.
All 9 weights loaded, 2 used — plus faux-bold where a weight is missing.
Only the used weights load, with font-display: swap.
8.5Test with real long content: long titles, long names, translated text (~30% longer). Nothing may overflow or wrap badly.
Comprehensive Enterprise Cloud Infrastructure Migration Services
The title escapes the card and breaks the row.
Tested with "Card title" — a real title overflows the card.
Comprehensive Enterprise Cloud Infrastructure Migration Services for Global Teams
Two-line clamp — the card always holds.
Long titles clamp cleanly — tested before handover.
8.6Never put text inside an image. All text must be real, selectable text.
Headline baked into the JPG — blurry, unselectable, invisible to search.
Grow your business
real, selectable textReal HTML text layered over the image — sharp, selectable, translatable.
Colour
9.1All colours come from tokens. Meaning-colours (success = green, error = red) are the same everywhere. Error text is never grey.
Payment failed
Invalid email address
Session expired
one grey error + three different redsA grey error, plus three different reds across the site.
Payment failed
Invalid email address
Session expired
one --color-error token everywhereOne error token — same red, same icon, everywhere.
9.2Text must pass WCAG AA contrast against its background — including text on top of images or video. Add a dark overlay and test against the lightest possible image.
Barely readable on a bright photo
White text straight on a bright image — unreadable.
Readable on any image
A dark overlay guarantees contrast on any image.
9.3If the Figma design fails a contrast check, tell the designer before building. We never ship failing contrast.
Read our latest case studies
2.3 : 1 — fails WCAG AALight grey on white — 2.3:1, fails WCAG AA. Built as drawn.
Read our latest case studies
7.6 : 1 — passes AA and AAAChecked, flagged, fixed — 4.9:1 passes AA.
9.4Never use colour alone to show meaning. An error needs an icon and a message, not just red. Same for statuses and required fields.
a red border is the only signal
The only error signal is a red border — invisible to colour-blind users.
Please enter a valid email address
Red border + icon + specific message.
Click & Tap
The design shows the visual size. The clickable area is your job.
10.1Everything clickable must have a click area of at least 44 × 44 px, on mobile and desktop. If the visible icon is smaller, extend the area with padding. Try clicking both below.
The click area is only the 16px icon itself — try hitting it.
Same 16px icon, but a 44×44 click area (dashed) around it.
10.2Leave space between neighbouring clickable elements so users don’t tap the wrong one.
Action row — 0 px between buttons
Nav links — 4 px apart
Touching targets — wrong tap guaranteed on mobile
0 px between icon buttons and 4 px between nav links — a clumsy tap on Edit fires Delete instead.
Action row — 8 px between buttons
Nav links — 16 px apart
Each target has its own clear zone
8 px between action buttons, 16 px between nav links, and the destructive Delete pushed further right with extra margin.
10.3If a whole card is clickable, the entire card is the click area — not just the "Read more" text. Hover both cards below.
How we cut load time by 60%
Hovering the card does nothing…
Read moreOnly the two words "Read more" are clickable.
How we cut load time by 60%
…the whole card is the link.
Read moreThe whole card is one link — hover anywhere.
10.4For checkboxes and radio buttons, clicking the label also activates the control. Try clicking the text below.
Clicking the text does nothing — only the tiny box works.
The label is connected — clicking the text toggles the box.
Interactive States
Rule: if you can click it, it must react to hover, react to keyboard focus, and show a pointer cursor. No exceptions. These demos are live — try them.
11.1Hover: every clickable element visibly changes (shade, underline, lift), with a smooth transition. Always cursor: pointer. Hover both buttons.
Nothing happens on hover, and the cursor stays an arrow.
Visible change, smooth transition, pointer cursor.
11.2Focus: a clearly visible outline when reached by keyboard (Tab). Never remove the outline without an equal replacement. Click a demo, then press Tab.
outline: none — keyboard users navigate blind.
A clear focus ring shows exactly where you are.
11.3Active: visible pressed feedback. Disabled: greyed out, cursor: not-allowed, skipped by Tab.
A clickable div: no pressed state, "disabled" is only visual.
A real button: pressed feedback, true disabled state.
11.4Loading: submit buttons show a spinner and become disabled while sending — this prevents double submissions. Click both Send buttons quickly.
click it a few times…
Click 5 times while waiting — 5 emails get sent.
try double-clicking
One click: spinner, disabled, one email.
Forms
14.1Every input has a visible label. A placeholder is not a label — it disappears when the user types. Type in both fields below.
start typing…
Start typing and the "label" is gone — what was this field again?
the label never disappears
The label stays visible above the field, always.
14.2Errors: shown next to the field, in the error colour plus an icon and specific text. Check on blur or submit — never while the user is still typing. After a failed submit, focus jumps to the first error.
Invalid!
Name
which field? why?
A generic red "Invalid!" at the top of the page — which field? Why?
Name
Please enter a valid email address
A specific message with an icon, right next to the field.
14.3Use correct input types (email, tel, url, number) so phone keyboards adapt. Add autocomplete attributes. Input text at least 16px (stops iPhones zooming in).
Contact us
Full name
Phone number
Mobile keyboard triggered
full QWERTY — wrong for a phone number
type="number" adds up/down spinners, strips the leading zero from 0501234567, and shows a full QWERTY keyboard on mobile.
Contact us
Full name
Try typing "abc123" — letters are blocked
Mobile keyboard triggered
numeric keypad — correct for a phone number
type="tel" shows a numeric keypad, preserves leading zeros, and live validation blocks letters and flags bad formats.
14.4On submit: loading state on the button, then a clear success message or a clear failure message. A form that fails silently is a launch-blocking bug.
click submit…
Submitted… and nothing. Did it work? The user will never know.
try it
Loading → clear success (or clear failure with retry).
14.5Before launch: test that the form really delivers (email arrives), and turn on spam protection. Mark required/optional fields the same way across the whole site.
Inbox (0)
wrong API key — two weeks of leads lost
The success message shows — but no email ever arrives. Leads lost.
Inbox (1) — Test submission received
delivery confirmed + spam protection on
A real test submission confirmed in the inbox before closing the task.
Images & Icons
Images must never hide their important content.
15.1One shape per image role: all blog cards the same ratio, all team photos the same ratio. Enforce with aspect-ratio + object-fit: cover. Never stretched.
Three blog cards — no consistent ratio, each image a different height
Three blog cards with hardcoded heights of 28 px, 52 px, and 80 px — every image slot is a different shape, the row looks broken.
Same three cards — one shared aspect-ratio, object-fit: cover
aspect-ratio: 16/9 on every card — identical height regardless of the source image, object-fit: cover handles any subject without distortion.
15.2When an image uses cover/fill, check what is actually visible at every breakpoint. A face cut in half or a product cropped at the edge is not acceptable. Fix with object-position or ask for a better-framed image.
head cropped off
The crop cuts the subject’s head off at this breakpoint.
object-position: top
object-position keeps the important part in frame.
15.3Serve modern formats (WebP/AVIF) at the right sizes with srcset. Never load a giant original into a small slot.
DSC_4820-original.jpg · 6000px wide
An 8 MB, 6000px original loaded into a 400px slot.
team-800.webp · srcset + sizes
A 42 KB WebP at the right size via srcset.
15.4Lazy-load images below the fold. Never lazy-load the hero image. Reserve space for every image so nothing jumps while loading.
image lands → everything jumps
No reserved space — the page jumps as each image lands.
dimensions reserved → zero shift
Width/height reserved — content never moves.
15.5Meaningful images get descriptive alt text. Decorative images get alt="". Icons: one set, one style, SVG preferred, colours from tokens.
alt="image", and outline icons mixed with filled ones.
Descriptive alt text, one consistent icon style.
15.6Background video: muted, playsinline, with a poster image, and a static fallback on mobile if heavy. The user must always be able to pause moving content.
45 MB, unmuted, autoplaying, no poster, no way to pause.
Muted, playsinline, poster image, visible pause control.
Animation
16.1Transitions must not be too fast or too slow. Under 80ms feels instant but gives no feedback; over 400ms feels broken and sluggish. Aim for 150–250ms for hover and small interactions, 250–350ms for panels and modals.
Hover each button to feel the difference
Too fast (50ms) feels like no animation at all. Too slow (1200ms) makes the UI feel broken.
Hover to feel the right speed
Speed guide
200ms — noticeable but never sluggish. Responsive and polished.
16.2Scroll-in animations (if designed): subtle, run once, and never hide content — even for fast scrollers or users with JavaScript off.
opacity: 0 — JS failed, content gone forever
opacity: 0 in the CSS — if JS fails, the content is invisible forever.
visible by default — animation is a bonus
Visible by default — animation is an enhancement only.
Beyond the Design File
Figma shows the happy path. Production must handle the rest.
17.1Loading: skeletons or spinners — never a blank hole or a layout jump.
(blank while loading)
then everything jumps 600px down
A blank void while loading — then everything jumps into place.
skeletons at final size — zero shift
Skeletons at final dimensions — zero layout shift.
17.2Empty: a friendly "No results found" with a suggestion — never a broken gap.
(nothing renders — looks broken)
Zero results renders… nothing. Looks broken.
No results found
Try a shorter search term
A friendly message with a helpful suggestion.
17.3Error: a friendly message with a way to retry.
undefined
the API failed — this is what users see
The API failed and the section renders "undefined".
We couldn’t load the latest posts.
A clear message and a Retry button.
17.4Long content: clean truncation with "…" — tested with real long titles, names, URLs. Wide content: tables scroll inside their own box on mobile.
No truncation — long title breaks the card layout
Migrating a Twenty-Year-Old Monolith to a Modern Edge-Rendered Architecture Without Any Downtime
Mar 12, 2025 · 8 min read
Hello World
Mar 10, 2025 · 2 min read
Two blog cards with no clamp — the long title stretches the first card much taller, making the pair uneven.
line-clamp-2 — both cards identical height regardless of title length
Migrating a Twenty-Year-Old Monolith to a Modern Edge-Rendered Architecture Without Any Downtime
Mar 12, 2025 · 8 min read
Hello World
Mar 10, 2025 · 2 min read
line-clamp-2 cuts both titles at exactly 2 lines with “…” — both cards stay the same height.
17.5Missing data: missing image → styled placeholder. Missing optional text → the layout closes up cleanly.
No fallbacks — missing image and missing bio leave visible holes
Sara Ahmed
Product Designer
James Park
Engineer
Broken image icon with a red cross and an empty dashed box where the bio should be — looks broken.
Initials avatar fallback + layout closes up when bio is missing
Sara Ahmed
Product Designer
Designing systems that scale with the team.
James Park
Engineer
Initials avatar always renders instead of a broken image; missing bio simply disappears with no hollow gap.
Responsive
No horizontal scrollbar. At any width. Ever.
18.1Build every designed breakpoint, then drag the browser window to check every width in between. The layout must never break.
375px ✓
1000px ✗ — cards overlap
1440px ✓
Fine at 375px and 1440px — broken at 1000px, where most laptops land.
375px ✓
1000px ✓
1440px ✓
Every in-between width checked and fixed before handover.
18.2Anything shown on hover must also be reachable by tap on touch devices. Try both info icons below — with mouse and by clicking.
hover works — tap never will
Hover-only tooltip — touch users can never see it.
hover, focus, and tap all work
Opens on hover, focus, and tap.
18.3When columns stack on mobile, they must stack in logical reading order — check the DOM order, not just how it looks.
Visually looks correct — but DOM order is wrong
Sidebar
Main content
DOM / screen-reader order
Screen reader announces main content before sidebar — logical order is broken
The sidebar appears first on screen but sits second in the DOM — screen readers announce main content before the sidebar.
DOM order matches visual order — sidebar first in HTML, pushed left with CSS
Sidebar
Main content
DOM / screen-reader order
DOM order = visual order = screen-reader order — consistent for everyone
HTML order matches visual order — sidebar first in both the DOM and on screen, consistent for everyone.
18.4Sticky elements (header, chat widget, floating buttons) never cover content or each other on small screens. Test on a real iPhone and a real Android phone, not just DevTools.
We use cookies…
360px Android
Chat bubble covers the cookie banner covers the Submit button.
each element has its own zone
Each sticky element has its own reserved zone.
18.5No horizontal scroll — ever. Not on page load, not mid-animation, not on any viewport width. Use max-w-full, overflow-x-hidden on the root, and min-w-0 on flex children. Never use fixed pixel widths wider than the viewport.
Two common causes of horizontal scroll
Cause 1 — fixed px width exceeds container
Cause 2 — slide-in panel not clipped during animation
A fixed-width element and an animating element that briefly exceeds the viewport both trigger a horizontal scrollbar.
Three rules that prevent horizontal scroll at any point
nothing can ever grow wider than its parent
clips offscreen elements mid-animation too
stops text or images stretching items past the container
Quick test in DevTools console
document.body.scrollWidth > window.innerWidthReturns true if you have a horizontal overflow anywhere on the page.
max-w-full + overflow-x-hidden on the wrapper + min-w-0 on flex children — no horizontal scroll at any point.
18.6Never add multiple nested scrollable areas on the same page unless the UX explicitly requires it (e.g. a data table inside a fixed-height panel). Stacked scroll bars confuse users and trap keyboard focus.
Three independent scroll areas on one screen
scroll #2
scroll #1 (page)
scroll #3
Page scrolls, sidebar scrolls, and a card inside also scrolls — three independent scroll bars on one screen.
One scroll context — only the page scrolls
no scroll
scroll #1 — only one on the page
grows with content — no inner scroll
When inner scroll IS acceptable
Only the page itself scrolls. Inner containers grow with their content or are intentionally fixed with one clear scroll context.
18.7Mobile menus must be a fixed size — 100vw × 100dvh or a constrained drawer. They must never overflow the viewport, push the page sideways, or create a horizontal scrollbar.
Menu wider than viewport — page shifts sideways
width: 180px
Mobile menu is wider than the viewport — the page shifts right and a horizontal scrollbar appears.
Menu exactly fills the viewport — no shift, no overflow
w-screen · 100dvh
stops at 100dvh
Required CSS on the menu
Also set overflow: hidden on body while menu is open.
Mobile menu is exactly 100vw wide and 100dvh tall, clipped to the viewport, with overflow-hidden on the body while open.
Accessibility
Contrast, click size, and focus outlines are covered above. Also:
19.1The whole site works with keyboard only: Tab reaches everything in logical order, Escape closes popups, and the user can never get stuck.
Join our newsletter
Focus cycles inside the popup forever — there is no way out.
Join our newsletter
Esc closesTab flows in order; Escape always closes; never trapped.
19.2Use semantic HTML: header, nav, main, footer. Add a "skip to content" link as the first Tab stop.
assistive tech sees zero structure
Anonymous divs — assistive tech sees no structure at all.
Real landmarks — screen readers can jump straight to content.
19.3Modals keep keyboard focus inside while open, and return focus to the button that opened them when closed.
Confirm delete?
focus is out there ↖ behind me
focus escapes behind the modal
Tab wanders behind the modal; on close, focus is dumped at the top.
Confirm delete?
Tab cycles in here only
focus trapped inside, returned on close
Focus trapped inside; returned to the opening button on close.
19.4Every form field is connected to its label in code (for/id). The lang attribute on <html> is correct. Before handover: run Lighthouse or axe and do a manual keyboard walkthrough. Click the word "Name" in both demos.
announced as just "edit text"
A loose span — clicking it does nothing; screen readers announce "edit text".
announced as "Name, edit text"
Connected label — clicking focuses the field; announced as "Name, edit text".
Performance
20.1No layout shift anywhere, and the main image loads fast — mostly a result of good image discipline. Meet the project’s Lighthouse targets before handover.
Buy now — oops, you clicked the ad
content jumps as things load
Text jumps as fonts and images land — you tap the wrong thing.
Buy now — exactly where you aimed
CLS = 0
Space reserved for everything — nothing ever moves.
20.2Zero console errors, no broken assets, no dead links, no href="#" in production. All tel: and mailto: links correct.
✕ Uncaught TypeError: undefined is not a function
✕ GET /images/hero.jpg 404
▲ href="#" found in 3 links
✕ tel:0501234567 — missing country code
Errors in the console, missing images, links that go nowhere.
(empty — no errors, no warnings)
✓ all assets resolve
✓ tel:+971501234567 · real hrefs everywhere
Clean console, every asset resolves, every link works.
20.3Favicon, unique page titles, and meta descriptions. A designed 404 page and a friendly 500 page — both branded with a recovery action. Never a raw server error.
Browser tabs — all identical title, no favicon
404 page — raw server response
404
Not Found
nginx/1.18.0 (Ubuntu)
No branding, no explanation, no way back — user is stuck
500 page — raw server response
500
Internal Server Error
at Object.<anonymous> app.js:42
Stack trace exposed — dangerous and confusing for users
No favicon, identical title on every tab, raw nginx 404, stack trace on 500 — no brand, no way back.
Browser tabs — unique titles, branded favicon
404 page — designed, branded, helpful
404
Hmm, that page doesn’t exist
The link may be broken or the page may have moved.
500 page — friendly, no stack trace
500
Something went wrong on our end
We’re on it. Try refreshing or check back in a moment.
Branded favicon, unique title per page, designed 404 and 500 pages with logo and clear recovery buttons.