Skip to content

Rules for every developer

Design Implementation
Standard

A design file provides the exact values. This standard covers everything a design file cannot: what to verify, how interfaces behave in the real world, and the minimums we never ship below. Every rule is demonstrated with a wrong and a correct example — the difference between a website that looks finished and one that is.

01

Design Tokens

Rule: zero one-off values. Every value in code comes from a named token in Figma.

1.1Turn every Figma style (colours, fonts, shadows) into a named token in code (CSS variables or Global Colours & Fonts). If a value appears more than once it must be one token used in both places — never typed twice. About 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
#1d4ed8
#1e40af
#2563eb
#1a4fd0
#2058e0
5 near-identical blues, all typed by hand

Five slightly different blues invented across the site; button and link both typed the same hex — they drifted apart; a one-off border colour invented on the spot.

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

One named token per value — every element reads it, nothing drifts, no raw values anywhere.

02

Spacing

Elements must have breathing room — never cramped.

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

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

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

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

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

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

03

Layout & Breakpoints

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

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

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

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

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

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

04

Alignment

Elements that sit together must line up on a shared axis — nothing floats a few pixels off.

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

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

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

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

05

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.

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

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

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

06

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.

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

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

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

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

07

Text

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

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

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

7.4Headings and body text must never stretch the full width of a wide section — even if the section background is full-width. Full-width is for backgrounds; text needs a narrower "prose" max-width. Body text: 65–75 characters per line (~600–700px). Headings: up to ~90 characters (~800px). Use a prose wrapper inside full-bleed sections, or constrain the text container directly.

Wrong

Full-width text — exhausting to read

This heading stretches across the entire section width because there is no max-width constraint on the text container

This body text also stretches edge to edge. On a wide screen, each line runs over 100 characters. The eye struggles to find the start of the next line. Reading speed drops. Users give up.

~120ch
Full-width backgrounds are fine — full-width text is not

A full-width section with body text stretching edge to edge — 140 characters per line on a wide screen. The eye cannot track from the end of one line back to the start of the next.

Correct

Prose width — comfortable reading

This heading is inside a prose container

Body text sits in the same narrower container. Each line is 55–65 characters. Easy to read, easy to scan.

~65ch
Background full-width · Text constrained to prose width

Same full-width background, but the text sits inside a narrower prose container (~680px). Comfortable reading width regardless of screen size.

7.5Load 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.

7.6Test 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.

7.7Never 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.

7.8No single-word widows in headings. Apply text-wrap: balance to every heading so the last line is never a lone word hanging by itself.

Wrong

No text-wrap: balance — lone word on last line

Building Better Websites for Every
User on
Earth

single word "Earth" orphaned on line 3

A heading wraps to three lines with a single orphaned word on the last line.

Correct

text-wrap: balance — even distribution

Building Better Websites for Every User on Earth

words balanced across lines — no orphan

text-wrap: balance distributes words evenly — no orphaned last word.

7.9ALL-CAPS text must have letter-spacing of 0.04–0.1em — tight capitals are hard to read. Body text is never justified: justified text creates uneven word gaps, especially on narrow mobile columns.

Wrong

No letter-spacing on caps + justified body

Services We Offer

We design and build digital products that are beautiful, functional, and accessible to all users.

caps: 0 letter-spacing · body: justified with word gaps

ALL-CAPS with no letter-spacing, and a justified paragraph with rivers of white space.

Correct

Tracked caps + left-aligned body

Services We Offer

We design and build digital products that are beautiful, functional, and accessible to all users.

caps: 0.06em tracking · body: left-aligned, even spacing

ALL-CAPS with 0.06em tracking, and left-aligned body text with even word spacing.

7.10Capitalisation rules (house style): (1) Headings use sentence case — only the first word and proper nouns are capitalised. (2) Buttons, labels, tabs, and menu items start with a capital letter on the first word only — never title case every word. (3) Headings never end with a full stop (.). Questions may end with a ?. (4) Body text and error messages follow normal sentence punctuation.

Wrong

Title Case heading + full stop + wrong button case

<h2>

Why We Build Better Websites For You.

Title Case + full stop — both wrong

<button>

Every word capitalised — title case on UI labels

error message

something went wrong

Lowercase start — not a sentence

Title Case · trailing full stop · every word capitalised · lowercase error

Heading in Title Case With A Full Stop. Button reads "Get In Touch". Error reads "something went wrong".

Correct

Sentence case heading + no full stop + correct label case

<h2>

Why we build better websites for you

Sentence case · no trailing full stop

<button>

First word only capitalised

error message

Something went wrong.

Capital start · ends with full stop

Sentence case · no heading full stop · first word only · proper error punctuation

Heading in sentence case, no full stop. Button reads "Get in touch". Error reads "Something went wrong."

08

Colour

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

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

8.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 — 7.6:1 passes AA and AAA.

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

09

Click & Tap

The design shows the visual size. The clickable area is your job.

9.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. (WCAG 2.2 SC 2.5.8 legally requires only 24×24px under AA — enforceable under the European Accessibility Act. Our 44×44px matches Apple HIG and WCAG AAA. We build to the higher bar.)

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.

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

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

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

10

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.

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

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

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

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

10.5Hover styles must not get stuck on touch screens. Wrap all :hover rules inside @media (hover: hover) so they only apply on devices with a real pointer. A tapped card on mobile must not stay highlighted after the finger lifts.

Wrong

Hover style stuck on touch — card looks selected after tap

Service A

No @media (hover: hover) guard — hover bg stays after touch

Service B

No @media (hover: hover) guard — hover bg stays after touch

tapped card retains hover highlight — looks broken or selected

After tapping a card on mobile, the hover highlight stays visible — it looks selected or broken.

Correct

Hover guard — touch devices never trigger :hover

Service A

Hover only fires inside @media (hover: hover)

Service B

Hover only fires inside @media (hover: hover)

@media (hover: hover) {

.card:hover { background: ... }

}

touch: no hover flash · mouse: hover works normally

Hover styles are wrapped in @media (hover: hover) — touch devices never trigger them.

11

Buttons

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

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

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

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

13

Forms

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

13.2Errors: shown next to the field, in the error colour plus an icon and specific text. Never show a new error while the user is still typing — check on blur or submit instead. But once a field is already in an error state, re-validate live so the error clears the moment the user fixes it. 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. Error appears on blur, clears live once fixed.

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

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

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

13.6Forms must cooperate with browser autofill. Add autocomplete attributes (name, email, tel, new-password etc.), use correct inputmode values, never block paste in password fields, and add a show/hide toggle to all password inputs.

Wrong

No autocomplete, paste blocked, no show/hide

no autocomplete attr — browser can't autofill

paste blocked, no show/hide toggle

user must type everything manually, can't paste password

No autocomplete, paste blocked in the password field, no show/hide toggle — users retype everything.

Correct

autocomplete, paste allowed, show/hide toggle

autocomplete="email" — browser fills in one tap

paste allowed + show/hide toggle

browser autofills email · paste works · password visible on demand

autocomplete="email", paste allowed, show/hide password toggle — browser fills the form in one tap.

14

Images & Icons

Images must never hide their important content.

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

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

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

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

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

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

15

Animation

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

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

15.3Respect prefers-reduced-motion. Wrap every self-moving animation (parallax, auto-play carousels, spinning loaders) in @media (prefers-reduced-motion: no-preference). Users who need reduced motion must see static content, not a frozen animation mid-frame.

Wrong

Animation plays regardless of motion preference

spin

/* No motion query — always spins */

.loader { animation: spin 1s linear infinite }

spins for every user — causes nausea for vestibular disorder sufferers

A spinning animation plays regardless of the user’s system motion preference.

Correct

Static with reduced-motion, animated without

reduced-motion: static

spin

no preference: animates

@media (prefers-reduced-motion: reduce) {

.loader { animation: none }

}

motion preference respected — safe for vestibular disorder users

With reduced motion enabled, the element is shown statically — no movement at all.

16

Beyond the Design File

Figma shows the happy path. Production must handle the rest.

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

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

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

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

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

17

Responsive

No horizontal scrollbar. At any width. Ever.

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

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

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

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

17.5No horizontal scroll — ever. Not on page load, not mid-animation, not on any viewport width. Fix the cause: use max-w-full on wide elements and min-w-0 on flex children. overflow-x: hidden on the root is a band-aid that masks the bug instead of fixing it — and it breaks position: sticky on descendants in some browsers. Reserve it only as a documented last-resort safety net, never as the primary fix.

Wrong

overflow-x-hidden masks the bug — it doesn't fix it

Band-aid: overflow-x-hidden on the root

width: 260px — still overflows, just hidden
clipped, not fixed

Side effect: breaks position: sticky

overflow-x: hiddenon parent
position: sticky← stops working

Sticky elements inside an overflow-x-hidden container lose their stickiness in some browsers.

overflow-x-hidden hides the symptom, breaks sticky, and the overflow bug still exists

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

Correct

Fix the cause — prevent overflow at the source

max-w-fullon wide elements

nothing can grow wider than its parent — prevents fixed-width overflow

min-w-0on flex / grid children

stops content from stretching items past the container

!
overflow-x-hiddenlast resort only

band-aid that masks bugs and breaks position: sticky — use only if the real fix is blocked

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 + min-w-0 fix the cause — overflow-x-hidden is a last resort, not the solution

max-w-full + min-w-0 on flex children — the overflow is prevented at the source, no horizontal scroll at any point.

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

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

17.8Long words and unbreakable URLs must never push past the container edge. Apply overflow-wrap: break-word (or Tailwind’s break-words) to any element that may receive user-generated text, URLs, or translated content.

Wrong

Long URL breaks layout — no overflow-wrap

User bio

Visit my portfolio at https://www.some-extremely-long-domain-name-that-breaks-layout.com/projects/2025

← text overflows past the card edge

URL pushes past container — page scrolls horizontally

A long URL or unbroken word forces the container to widen and creates horizontal scroll.

Correct

overflow-wrap: break-word — URL wraps inside container

User bio

Visit my portfolio at https://www.some-extremely-long-domain-name-that-breaks-layout.com/projects/2025

break-words — stays inside the box

overflow-wrap: break-word — no overflow at any width

overflow-wrap: break-word wraps the URL inside the container — no overflow at any width.

17.9Tables must survive on mobile. Wrap every table in a horizontally scrollable container with a visible scroll hint (a fade or shadow on the right edge). For simpler data, consider a card-stack layout on mobile instead.

Wrong

Wide table with no scroll container — breaks page layout

NameEmailRoleJoinedStatus
Sara A.sara@co.comDesignerJan 2024Active

table overflows — page scrolls horizontally

table wider than container — full-page horizontal scroll on mobile

A wide table breaks out of its container and triggers a full-page horizontal scrollbar.

Correct

Table inside overflow-x-auto container with scroll hint

NameEmailRoleJoinedStatus
Sara A.sara@co.comDesignerJan 2024Active

table scrolls in its own box — page stays still

overflow-x-auto on wrapper + right-edge fade hint — page never scrolls

The table scrolls inside its own box with a visible right-edge fade hint — the page never scrolls horizontally.

18

Accessibility

Contrast, click size, and focus outlines are covered above. Also:

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

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

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

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

18.5Never disable zoom. The meta viewport tag must not contain user-scalable=no or maximum-scale=1. Users who need to zoom in for readability — especially low-vision users — must be able to do so. Content must reflow cleanly at 200% zoom and at 320px width (WCAG 1.4.10 Reflow).

Wrong

user-scalable=no blocks pinch-to-zoom

<meta name="viewport"

content="width=device-width,

user-scalable=no,

maximum-scale=1" />

Low-vision users cannot enlarge text. Fails WCAG 1.4.4 (AA) — enforceable under EAA.

zoom disabled — illegal under WCAG AA and the European Accessibility Act

user-scalable=no prevents pinch-to-zoom — low-vision users cannot enlarge text. Fails WCAG 1.4.4.

Correct

No zoom restriction — content reflows at 320px

<meta name="viewport"

content="width=device-width,

initial-scale=1" />

Users can pinch-to-zoom freely. Content reflows at 200% zoom without horizontal scroll (WCAG 1.4.10 Reflow).

zoom unrestricted · reflows at 320px · passes WCAG 1.4.4 and 1.4.10

No zoom restriction. Content reflows at 320px. Users can zoom to 200% without horizontal scroll.

18.6A visible "Skip to content" link must be the first focusable element on every page. It can be visually hidden until focused. This lets keyboard and screen-reader users bypass the navigation and jump straight to the main content.

Wrong

No skip link — keyboard must Tab through all nav

HomeAboutServicesBlogContactCareers

First Tab: Home · Second Tab: About · Third Tab: Services...

Keyboard user must press Tab 6+ times before reaching any content.

6 tabs before first piece of content — unusable for keyboard users

Keyboard users must Tab through all nav links on every page before reaching any content.

Correct

Skip link as first Tab stop — jumps past nav instantly

Skip to main contentTab #1
HomeAboutServicesBlogContact

Pressing Enter on the skip link jumps focus straight to <main>.

Tab #1 = skip link — one keystroke to bypass entire navigation

The first Tab stop is a "Skip to main content" link that jumps past the nav when activated.

18.7Every icon-only interactive element needs an aria-label. This includes hamburger menus, close buttons, search icons, and carousel arrows. A button with only an icon and no text is silent to screen readers without it.

Wrong

Icon buttons with no aria-label — silent to screen readers

Screen reader announces: "button · button · button · button"

four identical "button" announcements — no purpose communicated

A hamburger button with no aria-label — announced as "button" with no purpose.

Correct

Every icon button has aria-label

Open navigation, button

Close dialog, button

Search, button

Next slide, button

each button announces its exact purpose

aria-label="Open navigation" — announced as "Open navigation, button".

19

Performance

19.1Core Web Vitals must pass before handover: LCP ≤ 2.5s, INP ≤ 200ms, CLS ≤ 0.1. Verify in PageSpeed Insights — field data (CrUX p75) is what Google measures, not lab data alone. INP is a JavaScript problem: excessive main-thread work blocks interaction — see rule 20.4 on third-party scripts.

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. CLS: 0.38, LCP: 6.1s, INP: 540ms.

Correct
space reserved

Buy now — exactly where you aimed

CLS = 0

Space reserved for everything — nothing ever moves. CLS: 0.02, LCP: 1.8s, INP: 80ms.

19.2Zero console errors, no broken assets, no dead links, no href="#" in production. All tel: and mailto: links correct. New-tab links always use target="_blank" rel="noopener" — open new tabs only for external links and PDFs, never for internal navigation. Modern browsers apply noopener implicitly to target="_blank", but include it explicitly for legacy browser support. Do not use noreferrer unless you deliberately want to hide the referrer — it strips the Referer header and kills analytics attribution for your partners and clients.

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, and external links missing rel="noopener".

Correct
Console

(empty — no errors, no warnings)

✓ all assets resolve

✓ tel:+971501234567 · real hrefs everywhere

Clean console, every asset resolves, every link works, external links open safely in new tabs with rel="noopener".

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

19.4Third-party scripts (Google Tag Manager, analytics, chat widgets) must not block the page. Load them with defer or async, never in the <head> without an async attribute. Blocking scripts are the most common cause of poor INP. Measure before and after adding any third-party tag.

Wrong

Sync script in <head> blocks page render

<head>

<script src="gtm.js"></script>

<script src="chat.js"></script>

</head>

LCP
5.8s
INP
620ms
sync scripts block render — LCP and INP both fail

GTM loaded synchronously in <head> — the whole page waits for the script to download before rendering.

Correct

async/defer — page renders first, scripts load after

<head>

<script src="gtm.js" async></script>

<script src="chat.js" defer></script>

</head>

LCP
1.6s
INP
90ms
async/defer — page renders instantly, both CWV pass

GTM loaded with async — page renders immediately. INP stays below 200ms.

19.5Every page must have Open Graph and Twitter Card meta tags, a 1200×630px share image, and a canonical URL. Test the unfurl in WhatsApp and LinkedIn before launch — social previews are part of the delivered product.

Wrong

No OG tags — blank social preview

makeuser.co

No title

No description

WhatsApp / LinkedIn shows blank card — no image, no title

Sharing the URL on WhatsApp shows a blank preview — no image, no title, no description.

Correct

OG tags set — rich preview on every platform

M
MakeUser

makeuser.co

Web Development Guideline v1.2

Standards we never break. Built at MakeUser.

og:image (1200×630) · og:title · og:description · twitter:card all set

og:title, og:description, og:image (1200×630), twitter:card all set — rich preview in every platform.

19.6The footer copyright year is generated by code, never typed by hand. A hardcoded year goes stale the moment January 1st arrives. Use new Date().getFullYear() or the server equivalent.

Wrong

Hardcoded year goes stale on Jan 1st

<p>Copyright © 2023 MakeUser</p>

Nobody updates this. By February it already reads last year.

Copyright 2023 — wrong year, zero maintenance

Copyright © 2023 MakeUser — hardcoded and immediately outdated.

Correct

Generated year — always current, zero maintenance

<p>Copyright © {new Date().getFullYear()} MakeUser</p>

Renders as: Copyright © 2026 MakeUser

new Date().getFullYear() — always the correct year

Copyright © {new Date().getFullYear()} MakeUser — always current, zero maintenance.

20

RTL & Multilingual

We build sites in English, Persian, and Arabic. RTL is not a mirror filter applied at the end — it is built in from the first line of CSS.

20.1Use logical CSS properties everywhere: margin-inline-start instead of margin-left, padding-inline-end instead of padding-right, and so on. One dir="rtl" on <html> will then flip the entire layout correctly — no per-component RTL overrides.

Wrong

Physical properties break in RTL

LTR (en)

Menu item

RTL (fa/ar)

مورد منو
margin-left hard-coded — gap on wrong side in RTL

margin-left: 16px is hard-coded — in RTL, the gap appears on the wrong side and must be manually overridden.

Correct

Logical properties flip automatically

LTR (en)

Menu item

RTL (fa/ar)

مورد منو

margin-inline-start: 1rem

one rule — correct for both directions

margin-inline-start flips automatically — one rule, both directions

margin-inline-start: 16px — the browser places it on the correct side for both LTR and RTL automatically.

20.2Declare direction and font in the HTML, not in CSS hacks. Set <html lang="fa" dir="rtl"> for Persian pages and load a proper RTL typeface (Vazirmatn for Persian/Arabic). Use consistent number and date conventions for the locale — never mix Western and Eastern-Arabic numerals on the same page.

Wrong

No lang/dir + Latin font for Persian text

<html lang="en">

no dir attr, Latin font loaded

ما وب‌سایت‌هایی می‌سازیم که زیبا و کارآمد هستند.

Persian glyphs rendered with Latin serif — incorrect letterforms

wrong font + wrong lang — screen reader uses English voice for Persian text

Latin font used for Persian text — glyphs render incorrectly. lang and dir not set, so screen readers use the wrong voice.

Correct

lang="fa" dir="rtl" + Vazirmatn

<html lang="fa" dir="rtl">

Vazirmatn font loaded for Persian

ما وب‌سایت‌هایی می‌سازیم که زیبا و کارآمد هستند.

Correct Persian letterforms, RTL text flow, correct screen-reader voice

lang="fa" dir="rtl" + Vazirmatn — correct rendering and correct voice

<html lang="fa" dir="rtl"> + Vazirmatn loaded — correct rendering, correct screen-reader voice.

20.3Mirror layout direction in RTL, not visual meaning. Navigation arrows, back/forward icons, and reading-order indicators flip in RTL. Logos, checkmarks, play buttons, and warning icons never flip — their meaning is direction-neutral.

Wrong

Directional arrow not flipped in RTL — wrong meaning

LTR — correct

RTL — wrong: → still points right (= back in RTL)

→ in RTL means going back — wrong directional meaning

A "next" arrow pointing right in LTR still points right in RTL — now it signals going back.

Correct

Arrow flips in RTL — correct directional meaning preserved

LTR — → for "next"

RTL — ← for "next" (correct in RTL)

direction arrows flip · logo stays · ✓ stays

layout direction flipped — meaning unchanged

The "next" arrow flips to point left in RTL — correct direction. The logo and checkmark stay unchanged.