Skip to content

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.

To change anything: get written approval (designer for visual changes, CTO for technical changes) and write it on the Jira task.
01

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.

Wrong

Card title

Padding eyeballed from a screenshot. Every side is different.

pad: 22? 13? 27?#4a4a4a "close enough"

Eyeballed values: uneven padding, a colour that is "close enough".

Correct

Card title

Padding and colour read directly from Figma Dev Mode.

padding/24Gray/700

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.

Wrong

home / pricing / about — three different buttons

The same "Contact" button built three different ways on three pages.

Correct

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.

Wrong

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.

Correct

This is 16px body text with passing contrast. Comfortable to read for everyone.

Flagged to the designer, fixed to 16px with passing contrast.

02

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.

Wrong
#1d4ed8
#1e40af
#2563eb
#1a4fd0
#2058e0
5 near-identical blues, all typed by hand

Five slightly different blues invented across the site.

Correct
--color-primary
LinkBadge
one token feeds every element

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.

Wrong
Learn more
typed twice — one got "fixed" later and they drifted

Button and link were "the same blue", typed twice — they drifted apart.

Correct
Learn more
both read var(--color-primary) — can never drift

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).

Wrong
invented today
a 6th grey nobody asked for

A border colour invented on the spot — now a 6th grey exists.

Correct
border: var(--gray-200) — existing token reused

The existing Gray/200 token is used — or the designer decides.

03

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.

Wrong
13px27px19px

Random gaps: 13px, 27px, 19px — nothing lines up.

Correct
8px24px16px

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".

Wrong
Hero
96px
About
110px
Contact

Every section has a different gap before it.

Correct
Hero
--section-space
About
--section-space
Contact

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.

Wrong

Heading → text gap

Our Mission

18px

We build products people love.

Our Team

3px

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.

22px

Consistency builds trust. Every screen that behaves the same teaches users how the product works.

6px

Clarity over cleverness. A simple sentence beats a clever one every time.

random gaps — 3 px, 6 px, 18 px, 22 px — no rhythm

Heading→text: 18 px on one section, 3 px on the next. Paragraph→paragraph: 22 px then 6 px. No rhythm, no rule.

Correct

Heading → text gap

Our Mission

6px

We build products people love.

Our Team

6px

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.

12px

Consistency builds trust. Every screen that behaves the same teaches users how the product works.

12px

Clarity over cleverness. A simple sentence beats a clever one every time.

one token (6 px heading gap, 12 px paragraph gap) — consistent rhythm

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.

Wrong

Our Services

<br />
<br />
<div class="spacer">

We offer web design and development.

Two <br> tags and a spacer div create a random, unmaintainable gap.

Correct

Our Services

We offer web design and development.

margin-bottom: var(--space-4)

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.

Wrong

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.

2 px between heading and its own text — 2 px between unrelated sections — eye cannot group

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

Correct grouping — tight within, loose between

Design Principles

Good design is invisible. It solves problems without drawing attention to itself.

20 px between sections

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.

heading → text: 5 pxsection → section: 20 px
heading hugs its paragraph — sections breathe apart — grouping is instant

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.

Wrong

Menu items squeezed together — they read as one long word.

Correct

Comfortable gaps — each item is clearly its own target.

04

Layout & Breakpoints

4.1Build the exact grid from Figma: one container width, one column count, one gutter width — for the whole site.

Wrong

home · 1240px

about · 1180px

contact · 1300px

Every page has its own container width — content jumps as you navigate.

Correct

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.

Wrong

narrow viewport

At container width, text slams into the screen edge.

Correct

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.

Wrong

27" monitor

On a wide monitor, text stretches edge to edge — 40-word lines.

Correct

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.

Wrong
home.css
810px
about.css
768px
blog.css
850px

Each page switches to mobile at a different width.

Correct
home.css
--bp-tablet
about.css
--bp-tablet
blog.css
--bp-tablet

One set of breakpoints — every page switches together.

4.5Cards in a row get equal height with flex or grid — never manual pixel heights.

Wrong

Web design

Comprehensive enterprise cloud infrastructure migration services

SEO

Cards end at different heights the moment content differs.

Correct

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.

Wrong
LOGO

900px — nobody looked here

Tested only at 375px and 1440px — at 900px the nav overlaps the logo.

Correct
LOGO

checked at every width, 320 → 1920

Dragged from 320px to 1920px — every in-between width holds.

05

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.

Wrong
Add to favourites
Download report
Notifications
margin-top: -3px · 4px · vertical-align guesses

Icons nudged with magic-number margins — each one sits at a different height.

Correct
Add to favourites
Download report
Notifications
flex items-center gap-1.5 — centred every time

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.

Wrong

Your subscription renews on 12 August. You can cancel any time before the renewal date from your account settings.

items-center — icon aligns to no line at all

Icon centred against the whole paragraph — it floats next to no line in particular.

Correct

Your subscription renews on 12 August. You can cancel any time before the renewal date from your account settings.

items-start — icon locked to the first line

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.

Wrong
Search…
All
Go
40px · 28px · 34px — three heights in one row

Input, select, and button all have different heights — the row looks broken.

Correct
Search…
All
Go
one height token (36px) — a single clean strip

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.

Wrong

Basic

For individuals.

Choose

Pro

For teams that need collaboration, roles, and advanced reporting.

Choose
buttons land wherever the text ends

Buttons land wherever the text ends — a different height in every card.

Correct

Basic

For individuals.

Choose

Pro

For teams that need collaboration, roles, and advanced reporting.

Choose
flex-col + mt-auto — buttons on one straight line

mt-auto pins every button to the bottom — one straight line across the row.

06

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.

Wrong
Logo
HomeAbout
Sign up
© 2026 Company
PrivacyTerms
header 20px · content 8px · footer 32px — three different edges

Header, content, and footer each use their own padding — the left edge zig-zags down the page.

Correct
Logo
HomeAbout
Sign up
© 2026 Company
PrivacyTerms
one 20px gutter everywhere — logo, heading, cards, and footer share the edge

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.

Wrong
Logo
HomeAbout
Sign up
overflow
© 2026 Company
PrivacyTerms
card is wider than the container — breaks the shared edge, causes horizontal scroll

A card is wider than the container — it breaks out past the header edge and causes horizontal scroll.

Correct
Logo
HomeAbout
Sign up
© 2026 Company
PrivacyTerms
every block ends exactly on the container edge — no overflow, no sideways 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.

Wrong
Logo
HomeAbout
Sign up
© 2026 Company
PrivacyTerms
banner text starts 4px from the edge — ignores the 20px container everyone else uses

The banner’s text hugs the screen edge — it ignores the container the rest of the page uses.

Correct
Logo
HomeAbout
Sign up
© 2026 Company
PrivacyTerms
background runs edge-to-edge, but its text starts on the same line as the logo

Background spans edge-to-edge, but the inner content starts on the same line as the header logo.

07

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.

Wrong
Acme
HomeAboutWork
0px
top & bottom padding
logo and nav flush with header edges — looks clipped

Logo and nav sit flush with the header edges — no breathing room, looks clipped.

Correct
Acme
HomeAboutWork
12px
equal top & bottom padding — logo has clear space
consistent 12 px vertical padding — logo breathes inside the header

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.

Wrong

Plan your next trip

Discover destinations, hotels, and experiences tailored to you.

Explore
0 px
text starts at card edge
text and button touch the card border on all sides

Heading and body text begin at the card edge — feels cramped and unfinished.

Correct

Plan your next trip

Discover destinations, hotels, and experiences tailored to you.

Explore
16 px
uniform padding on all four sides
16 px inner padding — content has a safe zone inside the card

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.

Wrong

No horizontal or vertical padding

Get started free
0 px
Continue with GitHub

icon and label touch top, bottom, left, and right border

0 px padding — label and icon are pressed flat against the button border

Label fills the entire button width — text touches the left and right borders.

Correct

px-5 py-2 (20px / 8px)

label has clear space from every edge

20 px horizontal · 8 px vertical — label floats comfortably inside

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.

Wrong

0 px inner padding

Tooltip
Saved to your library
Badge
New
Sale
Popular
Dropdown
Edit profile
Settings
Sign out
text kisses every container edge — tooltip, badge, and dropdown all lack padding

Tooltip text and badge label start right at the container border — no padding at all.

Correct

Consistent inner padding

Tooltip
Saved to your library
Badge
New
Sale
Popular
Dropdown
Edit profile
Settings
Sign out
tooltip 5/10 px · badge 2/8 px · dropdown 7/12 px — every container has a safe zone

Inner padding on every container type — consistent safe zone regardless of size.

08

Text

8.1Use only the text styles from Figma — exact fonts, sizes, weights, line-heights. No in-between sizes.

Wrong

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.

Correct

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.

Wrong

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.

Correct

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.

Wrong

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.

Correct

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.

Wrong
100200300400500600700800900
9 weights downloaded, 2 used — slow page for nothing

All 9 weights loaded, 2 used — plus faux-bold where a weight is missing.

Correct
400700
only the weights in use + font-display: swap

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.

Wrong

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.

Correct

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.

Wrong
hero.jpg — text baked in

Headline baked into the JPG — blurry, unselectable, invisible to search.

Correct

Grow your business

real, selectable text

Real HTML text layered over the image — sharp, selectable, translatable.

09

Colour

9.1All colours come from tokens. Meaning-colours (success = green, error = red) are the same everywhere. Error text is never grey.

Wrong

Payment failed

Invalid email address

Session expired

one grey error + three different reds

A grey error, plus three different reds across the site.

Correct

Payment failed

Invalid email address

Session expired

one --color-error token everywhere

One 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.

Wrong

Barely readable on a bright photo

White text straight on a bright image — unreadable.

Correct

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.

Wrong

Read our latest case studies

2.3 : 1 — fails WCAG AA

Light grey on white — 2.3:1, fails WCAG AA. Built as drawn.

Correct

Read our latest case studies

7.6 : 1 — passes AA and AAA

Checked, 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.

Wrong

Email

hello@company

a red border is the only signal

The only error signal is a red border — invisible to colour-blind users.

Correct

Email

hello@company

Please enter a valid email address

Red border + icon + specific message.

10

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.

Wrong
click area = 16 × 16px

The click area is only the 16px icon itself — try hitting it.

Correct
same icon — 44 × 44px click area

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.

Wrong

Action row — 0 px between buttons

Annual report.pdf
0 px gap

Nav links — 4 px apart

Touching targets — wrong tap guaranteed on mobile

0–4 px between targets — one clumsy tap fires the wrong action

0 px between icon buttons and 4 px between nav links — a clumsy tap on Edit fires Delete instead.

Correct

Action row — 8 px between buttons

Annual report.pdf
8 px gap

Nav links — 16 px apart

Each target has its own clear zone

8–16 px between targets — each action has a safe zone, destructive action sits further apart

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.

Wrong

How we cut load time by 60%

Hovering the card does nothing…

Read more

Only the two words "Read more" are clickable.

Correct

How we cut load time by 60%

…the whole card is the link.

Read more

The 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.

Wrong
I agree to the terms — clicking this text does nothing

Clicking the text does nothing — only the tiny box works.

Correct

The label is connected — clicking the text toggles the box.

11

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.

Wrong

Nothing happens on hover, and the cursor stays an arrow.

Correct

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.

Wrong

outline: none — keyboard users navigate blind.

Correct

A clear focus ring shows exactly where you are.

11.3Active: visible pressed feedback. Disabled: greyed out, cursor: not-allowed, skipped by Tab.

Wrong
Send (a div pretending)
no pressed state · can’t disable · not keyboard reachable

A clickable div: no pressed state, "disabled" is only visual.

Correct
real button: pressed feedback + true disabled

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.

Wrong

click it a few times…

Click 5 times while waiting — 5 emails get sent.

Correct

try double-clicking

One click: spinner, disabled, one email.

12

Buttons

12.1One button system for the whole site, using the variants from Figma (primary / secondary / tertiary). No page invents its own button.

Wrong

Every page invented its own button — different radius, colour, size.

Correct

One system: primary / secondary / tertiary, used everywhere.

12.2Size, radius, padding, and font come from the Figma component. If a button is drawn shorter than the 44px click minimum, extend the click area or raise it with the designer.

Wrong
32px tall — 32px click area

A 32px button with a 32px click area — hard to tap.

Correct
still 32px visually — 44px click area (dashed)

Still 32px visually — but the click area (dashed) extends to 44px.

12.3Test with the longest realistic label — text is never squashed, cut off, or wrapped badly.

Wrong

The German label overflows and wraps to three broken lines.

Correct

The button sizes to its content — tested with the longest label.

12.4Icon-only buttons need an aria-label. Use <button> for actions and <a> for navigation. Never a clickable div.

Wrong
screen reader: "…" (nothing)

A clickable div — screen readers announce nothing useful.

Correct
screen reader: "Open menu, button"

A real button with aria-label — announced as "Open menu, button".

14

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.

Wrong

start typing…

Start typing and the "label" is gone — what was this field again?

Correct

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.

Wrong

Invalid!

Name

Email

which field? why?

A generic red "Invalid!" at the top of the page — which field? Why?

Correct

Name

Email

hello@company

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).

Wrong

Contact us

Full name

John Smith

Phone number

0501234567
spinner arrows appear — user can increment a phone number

Mobile keyboard triggered

q
w
e
r
t
y
u
i
o
p
a
s
d
f
g
h
j
k
l
z
x
c
v
b
n
m

full QWERTY — wrong for a phone number

type="number" adds spinners — increments/decrements a phone numberstrips leading zero — 0501234567 becomes 501234567full QWERTY keyboard on mobile — not a numeric pad

type="number" adds up/down spinners, strips the leading zero from 0501234567, and shows a full QWERTY keyboard on mobile.

Correct

Contact us

Full name

John Smith

Try typing "abc123" — letters are blocked

Mobile keyboard triggered

1
2
3
4
5
6
7
8
9
*
0
#

numeric keypad — correct for a phone number

type="tel" — no spinners, preserves leading zeronumeric keypad on mobile — right tool for the jobpattern validation rejects letters and flags bad formats

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.

Wrong
Your message…

click submit…

Submitted… and nothing. Did it work? The user will never know.

Correct
Your message…

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.

Wrong
✓ Message sent successfully

Inbox (0)

wrong API key — two weeks of leads lost

The success message shows — but no email ever arrives. Leads lost.

Correct
✓ Message sent successfully

Inbox (1) — Test submission received

delivery confirmed + spam protection on

A real test submission confirmed in the inbox before closing the task.

15

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.

Wrong

Three blog cards — no consistent ratio, each image a different height

h=28px
h=52px
h=80px
28
52
80
px — three different heights
hardcoded pixel heights — every card a different shape, layout looks broken

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.

Correct

Same three cards — one shared aspect-ratio, object-fit: cover

16:9
16:9
16:9
42
42
42
px — identical on every card
aspect-ratio: 16/9 + object-fit: cover — one consistent shape, any image, no distortion

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.

Wrong

head cropped off

The crop cuts the subject’s head off at this breakpoint.

Correct

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.

Wrong
8 MB

DSC_4820-original.jpg · 6000px wide

An 8 MB, 6000px original loaded into a 400px slot.

Correct
42 KB

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.

Wrong
image loads late — no space reserved

image lands → everything jumps

No reserved space — the page jumps as each image lands.

Correct
width=1600 height=900 · fetchpriority=high

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.

Wrong
alt="image"mixed styles & sizes

alt="image", and outline icons mixed with filled ones.

Correct
alt="Revenue grew 40% in Q2"one SVG set, currentColor

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.

Wrong
unmuted · 45 MB · autoplayno poster · no pause

45 MB, unmuted, autoplaying, no poster, no way to pause.

Correct
muted · playsinline · poster

Muted, playsinline, poster image, visible pause control.

16

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.

Wrong

Hover each button to feel the difference

Too fast — 50msfeels like no animation
Too slow — 1200msfeels broken / stuck
50ms gives no feedback — 1200ms makes the UI feel frozen

Too fast (50ms) feels like no animation at all. Too slow (1200ms) makes the UI feel broken.

Correct

Hover to feel the right speed

Just right — 200msresponsive & polished

Speed guide

Hover / small feedback150–200ms
Panels / drawers250–300ms
Modals / page transitions300–350ms
200ms — noticeable but never sluggish

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.

Wrong

opacity: 0 — JS failed, content gone forever

opacity: 0 in the CSS — if JS fails, the content is invisible forever.

Correct

visible by default — animation is a bonus

Visible by default — animation is an enhancement only.

17

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.

Wrong

(blank while loading)

then everything jumps 600px down

A blank void while loading — then everything jumps into place.

Correct

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.

Wrong
search: "xyzabc"

(nothing renders — looks broken)

Zero results renders… nothing. Looks broken.

Correct
search: "xyzabc"

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.

Wrong

undefined

the API failed — this is what users see

The API failed and the section renders "undefined".

Correct

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.

Wrong

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

cards have different heights — long title pushes every element down unevenly

Two blog cards with no clamp — the long title stretches the first card much taller, making the pair uneven.

Correct

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 long titles at exactly 2 lines with "…" — layout is always consistent

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.

Wrong

No fallbacks — missing image and missing bio leave visible holes

broken

Sara Ahmed

Product Designer

bio is null — empty gap

James Park

Engineer

bio is null — empty gap
broken image icon + empty bio gap — looks unfinished and broken

Broken image icon with a red cross and an empty dashed box where the bio should be — looks broken.

Correct

Initials avatar fallback + layout closes up when bio is missing

SA

Sara Ahmed

Product Designer

Designing systems that scale with the team.

JP

James Park

Engineer

initials avatar — no broken image icon, always rendersmissing bio — layout closes up, no hollow gap left behind

Initials avatar always renders instead of a broken image; missing bio simply disappears with no hollow gap.

18

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.

Wrong

375px ✓

1000px ✗ — cards overlap

1440px ✓

Fine at 375px and 1440px — broken at 1000px, where most laptops land.

Correct

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.

Wrong

hover works — tap never will

Hover-only tooltip — touch users can never see it.

Correct

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.

Wrong

Visually looks correct — but DOM order is wrong

Desktop — looks fine visually

Sidebar

Main content

DOM / screen-reader order

1
Main contentreads first in DOM
2
Sidebarreads second in DOM

Screen reader announces main content before sidebar — logical order is broken

CSS flexbox makes it look right — but the DOM feeds sidebar content to screen readers second

The sidebar appears first on screen but sits second in the DOM — screen readers announce main content before the sidebar.

Correct

DOM order matches visual order — sidebar first in HTML, pushed left with CSS

Desktop — looks the same visually

Sidebar

Main content

DOM / screen-reader order

1
Sidebarreads first — matches visual
2
Main contentreads second — matches visual

DOM order = visual order = screen-reader order — consistent for everyone

HTML order matches what the eye sees — screen readers, keyboard users, and sighted users all get the same flow

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.

Wrong

We use cookies…

chat
Submit (buried)

360px Android

Chat bubble covers the cookie banner covers the Submit button.

Correct
Submit
chat

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.

Wrong

Two common causes of horizontal scroll

Cause 1 — fixed px width exceeds container

width: 260px — overflows container
overflow

Cause 2 — slide-in panel not clipped during animation

Page content
slide-in drawer
triggers scrollbar
fixed px widths and unclipped animations both push past the viewport edge

A fixed-width element and an animating element that briefly exceeds the viewport both trigger a horizontal scrollbar.

Correct

Three rules that prevent horizontal scroll at any point

max-w-fullon every element

nothing can ever grow wider than its parent

overflow-x-hiddenon the root wrapper

clips offscreen elements mid-animation too

min-w-0on flex / grid children

stops text or images stretching items past the container

Quick test in DevTools console

document.body.scrollWidth > window.innerWidth

Returns true if you have a horizontal overflow anywhere on the page.

max-w-full + overflow-x-hidden + min-w-0 — no horizontal scroll at any viewport width or animation frame

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.

Wrong

Three independent scroll areas on one screen

Sidebar

scroll #2

scroll #1 (page)

Activity

scroll #3

page scrolls · sidebar scrolls · inner card scrolls — three scroll bars at once disorient the user

Page scrolls, sidebar scrolls, and a card inside also scrolls — three independent scroll bars on one screen.

Correct

One scroll context — only the page scrolls

Sidebar

no scroll

scroll #1 — only one on the page

Activity

grows with content — no inner scroll

When inner scroll IS acceptable

A fixed-height data table inside a panel
A chat message list (intentional UX)
A code editor with a known fixed height
only the page scrolls — inner containers grow with content, no nested scrollbars

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.

Wrong

Menu wider than viewport — page shifts sideways

9:41

width: 180px

Home
About
Services
Contact
overflows
horizontal scrollbar
fixed px menu wider than the screen — page shifts and a horizontal scrollbar appears

Mobile menu is wider than the viewport — the page shifts right and a horizontal scrollbar appears.

Correct

Menu exactly fills the viewport — no shift, no overflow

9:41

w-screen · 100dvh

Home
About
Services
Contact

stops at 100dvh

no horizontal scrollbar

Required CSS on the menu

position:fixed
inset:0
width:100vw
height:100dvh
overflow:hidden

Also set overflow: hidden on body while menu is open.

position: fixed · 100vw × 100dvh · overflow: hidden — menu never shifts or overflows the viewport

Mobile menu is exactly 100vw wide and 100dvh tall, clipped to the viewport, with overflow-hidden on the body while open.

19

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.

Wrong

Join our newsletter

SubscribeTab loops here forever · Esc dead

Focus cycles inside the popup forever — there is no way out.

Correct

Join our newsletter

Esc closes
SubscribeTab flows onward ✓

Tab 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.

Wrong
<div class="header">
<div class="menu">
<div class="content">
<div class="footer">

assistive tech sees zero structure

Anonymous divs — assistive tech sees no structure at all.

Correct
<a href="#main"> skip to content
<header>
<nav>
<main id="main">
<footer>

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.

Wrong

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.

Correct

Confirm delete?

DeleteCancel

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.

Wrong
Name — click me, nothing

announced as just "edit text"

A loose span — clicking it does nothing; screen readers announce "edit text".

Correct

announced as "Name, edit text"

Connected label — clicking focuses the field; announced as "Name, edit text".

20

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.

Wrong

Buy now — oops, you clicked the ad

content jumps as things load

Text jumps as fonts and images land — you tap the wrong thing.

Correct
space reserved

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.

Wrong
Console

✕ 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.

Correct
Console

(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.

Wrong

Browser tabs — all identical title, no favicon

Home - My Site
Home - My Site
Home - My Site
Can't tell tabs apart — no favicon, same title on every page

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 tab titles · raw 404 and 500 pages with no brand or recovery path

No favicon, identical title on every tab, raw nginx 404, stack trace on 500 — no brand, no way back.

Correct

Browser tabs — unique titles, branded favicon

A
Home | Acme
A
Pricing | Acme
A
About | Acme
Each tab has its own title — favicon makes the brand instantly recognisable

404 page — designed, branded, helpful

A

404

Hmm, that page doesn’t exist

The link may be broken or the page may have moved.

Go home
Contact us

500 page — friendly, no stack trace

A

500

Something went wrong on our end

We’re on it. Try refreshing or check back in a moment.

Refresh page
Status page
branded favicon · unique tab titles per page · designed 404 and 500 with recovery actions

Branded favicon, unique title per page, designed 404 and 500 pages with logo and clear recovery buttons.