Frontend / Checklist

The fifty rules that should block a deploy

This is the cut-down list to run before every production deploy: every Critical rule from the nine discipline checklists, plus the High rules that most often show up in a launch-day retro. The nine full checklists carry everything else - the Medium and Low rules, the code samples and the edge cases this page leaves out on purpose.

50 rules in 9 groups. Adapted from the Front-End Checklist (David Dias, MIT), rewritten and re-verified September 2026.

What this checklist decides

An audit list is long because it is exhaustive; a launch list is short because it runs on a deadline. This page keeps only the rules whose failure is either visible to every user or expensive to undo once indexing, caching and real traffic have started - fifty rows, each with a check that takes about five minutes using a tool already on your machine.

The two priorities mean two different things. Critical means the deploy waits: a wrong charset, a keyboard-locked checkout or a leaked key is a rollback, not a follow-up. High means the site can ship, but a ticket is already open and assigned before it does, because every one of these gets more expensive the longer it lives in production.

HTML

Three of these are Critical because a browser that has to guess the encoding, the rendering mode or the viewport width renders a different page from the one you tested.

Rule Priority What to do
Start the document with the HTML5 doctype Critical <!doctype html> is line one of every page with nothing ahead of it - not a comment, not a byte-order mark. Check: document.compatMode in the console returns CSS1Compat; BackCompat means the page is in quirks mode.
Declare UTF-8 before anything else in head Critical <meta charset="utf-8"> is the first child of <head>, ahead of the title and every stylesheet, and the file itself is saved as UTF-8. Check: view source on the built page, then curl -I it and confirm the Content-Type header does not claim a different charset.
Set the responsive viewport on every page Critical <meta name="viewport" content="width=device-width, initial-scale=1"> in the shared layout, so the 404 page and the odd one-off template get it too. Check: DevTools device toolbar at 375 wide - a page without it renders at desktop width and shrinks the text to fit.
Set lang on the html element High <html lang="en"> (a BCP 47 tag) matching the language of the content, or screen readers pick the wrong voice and browsers offer to translate a page that is already in the reader's language. Check: Lighthouse Accessibility, "html element has a lang attribute", or grep the built HTML for <html lines without lang=.
Load every script with defer, async or type=module High No bare <script src> in head: defer for anything that needs the DOM, async for independent tags, type="module" for your own bundle (modules defer by default). Check: Lighthouse "Eliminate render-blocking resources" lists any script still holding up the parser.

Gotcha: a build that concatenates partials can push a comment or a blank line above the doctype and drop the whole site into quirks mode without an error anywhere - the console check above is the only thing that catches it. Semantic elements, forms, media, the web manifest and the i18n rules are in the full HTML checklist.

CSS

Nothing here is Critical, but three rows decide the first paint and two decide whether a keyboard user or a zoom user can use the page at all.

Rule Priority What to do
Inline the critical CSS and keep the rest from blocking High Above-the-fold rules go in a <style> block in head (the one place inline CSS is right); everything else is a single small stylesheet on your own origin so the one blocking request is fast, and anything that must load late is appended as a <link> from your bundle rather than by an inline onload handler. Check: Lighthouse "Eliminate render-blocking resources" names any stylesheet still holding up first paint.
Ship minified CSS and JavaScript High The production build runs a minifier (Lightning CSS or cssnano; esbuild or terser) over everything that deploys, and the dev build is never what gets deployed. Check: view source on one stylesheet and one bundle on the live host - if the indentation and comments are intact it is not minified; Lighthouse "Minify CSS" and "Minify JavaScript" name the files.
Remove unused CSS before it ships High A framework's full stylesheet with a fraction of it in use is the usual cause; Tailwind's content scan, PurgeCSS or a per-route split fixes it. Check: the DevTools Coverage panel (Ctrl+Shift+P, "Show Coverage") after a load - a stylesheet showing most of its bytes unused is the target.
Keep a visible focus indicator on every interactive element High Never outline: none without a replacement; style :focus-visible with an outline that reaches 3:1 contrast against whatever it sits on. Check: press Tab across the whole page - if you lose track of where focus is at any point, the rule fails.
Never disable pinch zoom High No user-scalable=no and no maximum-scale=1 in the viewport meta, and no JavaScript that cancels touchmove or the Ctrl-plus-wheel gesture. Check: grep the built HTML for user-scalable and maximum-scale, then pinch on a real phone.

Gotcha: :focus and :focus-visible are not interchangeable - styling only :focus paints a ring on every mouse click, which is why teams reach for outline: none and take it away from keyboard users too. Style :focus-visible and leave :focus alone. Tokens, layout, dark mode, containment and print rules are in the full CSS checklist.

JavaScript

One Critical and four Highs, all cheap to check because every one of them leaves a trace in the built bundle or in the console.

Rule Priority What to do
Never use eval or its cousins Critical No eval(), no new Function(), no string arguments to setTimeout or setInterval, and no innerHTML fed with anything a user typed. Check: grep the built bundle for eval( and new Function(; a CSP without 'unsafe-eval' turns any survivor into a console error on first load.
Handle errors where they happen and add error boundaries High Every fetch and every await sits inside a try/catch that does something useful, and each route sits under an error boundary (a React ErrorBoundary, Vue's onErrorCaptured, Svelte's <svelte:boundary>) so one broken component does not blank the page. Check: throw inside one component on a branch and confirm the rest of the page survives.
Validate external data at runtime High Every API response, URL parameter, localStorage read and postMessage payload goes through a schema (Zod, Valibot or ArkType) before the code trusts its shape - TypeScript types stop existing at the compiler. Check: grep for JSON.parse( and .json() and confirm each result meets a parse() or safeParse(), not a type assertion.
Split the bundle by route High Routes and heavy widgets load through dynamic import() so the first screen ships only its own code; every meta-framework does this by default, a hand-rolled Vite or webpack app does not. Check: the Network panel on a cold load - one JavaScript file the size of the whole app is the failure, and Lighthouse "Reduce unused JavaScript" puts a number on it.
No inline JavaScript High No onclick attributes and no <script> block with code inside it; handlers attach with addEventListener from your bundle, which is also what a hashed or nonce-based CSP demands. Check: grep the built HTML for onclick, onload and any <script> without a src.

Gotcha: JSON.parse on a response that came back as an HTML error page is the classic production exception - an error-handling miss and a schema miss in one, and it only shows up when the API is having a bad day. Modules, TypeScript strictness, event delegation, storage and memory-leak rules are in the full JavaScript checklist.

Images

Images are the heaviest bytes on most pages and the usual cause of a failed LCP or CLS score, so four of the five rows here are performance rules in disguise.

Rule Priority What to do
Every image has honest alt text Critical Content images describe what the picture conveys, decorative ones carry alt="" so screen readers skip them, and <input type="image"> gets an alt too because it is a button. Check: Lighthouse Accessibility "Image elements have [alt] attributes", then read the alts aloud - "image" and the filename both fail.
Width and height on every img High The intrinsic width and height attributes on each <img> (plus aspect-ratio in CSS where the rendered size varies) so the browser reserves the box before the bytes arrive. Check: DevTools Rendering panel, "Layout Shift Regions" - an image that flashes a shift as it loads is missing its dimensions.
Lazy load offscreen images High loading="lazy" on every <img> below the fold and never on the hero or the LCP image; decoding="async" alongside it costs nothing. Check: the Network panel with the page freshly loaded at the top - offscreen images should not have been requested yet.
srcset and sizes on content images High Each content <img> carries a srcset with two or three widths and a sizes attribute that matches the CSS layout, so a phone does not download the desktop file. Check: Lighthouse "Properly size images", then hover an image in the Elements panel at 375 wide and read its currentSrc.
Modern formats with a fallback High AVIF or WebP for photographic content inside <picture> with a JPEG or PNG <img> fallback; PNG stays right for flat-color UI art and screenshots, SVG for icons. Check: the Network panel's Type column sorted by size - large JPEG entries are the candidates, and Lighthouse "Serve images in next-gen formats" lists them.

Gotcha: loading="lazy" on the hero image is the most common self-inflicted LCP regression - the browser politely defers the one image it needed first. Compression, SVG hygiene, CDN resizing, retina and caption rules are in the full images checklist.

Performance

The three Core Web Vitals are measured on real users and reported in Search Console, which is why LCP is the one Critical row on this page that is not about correctness.

Rule Priority What to do
LCP under 2.5 seconds Critical Find the LCP element in the PageSpeed Insights report, then make it cheap: fetchpriority="high" on the image (or <link rel="preload" as="image"> when CSS loads it), never lazy loaded, and present in the HTML rather than painted by JavaScript after hydration. Check: PageSpeed Insights field data at the 75th percentile at or under 2.5 s.
CLS under 0.1 High Reserve space for images, ads, embeds and web fonts, and never insert content above what the reader is looking at unless they asked for it. Check: the Performance panel's "Layout Shifts" track, or the Rendering panel's "Layout Shift Regions" while the page loads; PageSpeed Insights field data at or under 0.1.
INP under 200 milliseconds High Keep every input handler short - no synchronous layout reads inside a click handler, heavy work moved to scheduler.yield(), requestIdleCallback or a Web Worker, and hydration finished before the first tap can land. Check: the Performance panel's interactions track with CPU throttling on; PageSpeed Insights field data at or under 200 ms.
Text compression on High HTML, CSS, JavaScript, JSON and SVG leave the server with Content-Encoding: br (or gzip where Brotli is unavailable); every CDN and most hosts do this by default, custom origins often do not. Check: curl -sI --compressed against the live URL and read the content-encoding header.
Cache headers that match each file's lifetime High Hashed assets get Cache-Control: public, max-age=31536000, immutable; HTML gets a short max-age or no-cache with an ETag so a deploy shows up; nothing gets no-store unless it is genuinely private. Check: curl -I one stylesheet and one HTML page on the live host and compare the two values.
Third-party scripts async and off the critical path High Every analytics, chat, ads or experiment tag loads with async (or through a tag manager that does) after your own bundle, with a <link rel="preconnect"> for its origin, and nothing third-party sits in head as a blocking script. Check: Lighthouse "Reduce the impact of third-party code" lists each origin with its main-thread cost.
Web fonts without invisible text High Self-host WOFF2, subset to the characters you use, set font-display: swap (or optional for body text you can live without), and preload the one or two files used above the fold with <link rel="preload" as="font" crossorigin>. Check: Lighthouse "Ensure text remains visible during webfont load", then watch for the text jump in the Performance panel's film strip.

Gotcha: a lab score is not field data - Lighthouse is one synthetic run on one machine, while Core Web Vitals come from real users on real phones over a 28-day window. A green Lighthouse and a red Search Console report can both be right at once, and the field number is the one that counts. Resource hints, bfcache, virtualization and page-weight rules are in the full performance checklist.

Accessibility

Seven of the eight rows here are Critical because each one locks a whole class of users out of the page rather than making it slower or uglier.

Rule Priority What to do
Heading hierarchy with no skipped levels Critical One <h1> per page, an <h2> before any <h3>, and never a heading level chosen for its font size - style it instead. Check: the headings view in the axe DevTools or WAVE extension, or run document.querySelectorAll("h1,h2,h3,h4,h5,h6") in the console and read the levels in order.
Everything works from the keyboard Critical Every link, button, menu, dialog, tab, slider and custom widget is reachable with Tab, activates with Enter or Space and closes with Escape; a <div> with a click handler is not a button, <button> is. Check: unplug the mouse and complete the primary task - sign-up, checkout, search - start to finish.
Every form control has a label Critical A <label for> matching the control's id, or the control wrapped inside the <label>; placeholder text is not a label because it disappears the moment typing starts. Check: click each visible label and watch focus jump into its field; Lighthouse Accessibility "Form elements have associated labels" catches the rest.
Every input and button has an accessible name Critical Icon-only buttons get aria-label or visually hidden text, search boxes get a label or aria-labelledby, and custom controls announce what they do rather than "button". Check: the Accessibility pane in the Elements panel shows a Name for each control - an empty one is the failure.
No aria-hidden on the body Critical aria-hidden="true" never lands on <body> or on the wrapper that holds the main content; a modal library that hides the page behind a dialog and forgets to undo it on close is the usual source. Check: open and close every modal, then run document.querySelectorAll("[aria-hidden='true']") and confirm nothing structural is in the list.
Content reflows at 400% zoom Critical At 320 CSS pixels wide (a desktop browser at 400% zoom) the page shows one column with no horizontal scrollbar and no clipped text; fixed pixel widths, white-space: nowrap on prose and overflow: hidden on containers are the usual causes. Check: resize the DevTools viewport to 320 wide and scroll to the bottom of the longest page.
Nothing flashes more than three times a second Critical No animation, video, GIF or loading indicator flashes more than three times in any one-second window, and anything that comes close gets a pause control and honors prefers-reduced-motion. Check: play every animated asset on the page once and count; the Photosensitive Epilepsy Analysis Tool (PEAT) does it for video.
Text meets the contrast minimum High Body text at 4.5:1 against its background, large text and control boundaries at 3:1, and placeholder text is not exempt. Check: Lighthouse Accessibility "Background and foreground colors have a sufficient contrast ratio", plus the contrast line in the Elements panel's color picker for anything sitting on a gradient or a photo.

Gotcha: automated tools only catch the failures that have a mechanical definition - axe and Lighthouse cannot tell whether the alt text is honest, the focus order makes sense or the keyboard flow reaches checkout, which is why most of the Critical checks above end with a human at a keyboard. Reduced motion, skip links, focus management, live regions, captions and the rest of the ARIA rules are in the full accessibility checklist.

Security and privacy

The three Criticals here are transport rules - the page, the form and the secrets - and all three can be proven from a terminal before the DNS change goes live.

Rule Priority What to do
Serve every page over HTTPS and redirect HTTP to it Critical A valid certificate on every host the site answers on, a 301 from every http:// URL to its https:// twin, no mixed content, and once the redirect is proven, Strict-Transport-Security: max-age=31536000; includeSubDomains so browsers stop asking. Check: curl -I http://your-host/ returns a 301 with an https Location, and the same call on the https URL shows the HSTS header.
Forms submit over HTTPS Critical Every form action and every fetch() a form triggers points at an https:// endpoint - a relative action inherits the page's scheme, an absolute http:// one hands the payload to the network in clear text. Check: grep the built HTML and bundle for http:// endpoints; browsers also stamp "Not secure" on the address bar when such a form has focus.
No secrets in the shipped bundle Critical Only variables prefixed VITE_, NEXT_PUBLIC_ or your framework's equivalent reach client code, and none of them is a private key, a database URL or a server-side API key - publishable keys (Stripe, Turnstile site keys) are public by design. Check: grep the built output for sk_live, SECRET, PRIVATE_KEY, DATABASE_URL and the first characters of every value in your .env.
A Content Security Policy is in place High A Content-Security-Policy header with default-src 'self', a script-src built from hashes or a nonce rather than 'unsafe-inline', frame-ancestors 'none' (or the hosts allowed to embed you) in place of X-Frame-Options, and X-Content-Type-Options: nosniff beside it. Check: curl -I the live page and paste the policy into Google's CSP Evaluator; the console lists every violation on first load.
Session cookies carry Secure, HttpOnly and SameSite High The session or auth token lives in a cookie set with Secure; HttpOnly; SameSite=Lax (or Strict), never in localStorage or sessionStorage where any injected script can read it. Check: the Application panel, Cookies - all three columns are ticked, and Local Storage holds nothing token-shaped.
Dependency audit is clean High npm audit --audit-level=high (or pnpm audit, yarn npm audit) passes in CI, and a critical advisory in a package that ships to the browser blocks the merge until it is patched or replaced. Check: run it against the lockfile you are about to deploy, not the one on a laptop from last week.
Privacy policy linked, consent asked only when needed High A privacy policy link in the footer of every page, and a consent banner only if you set non-essential cookies or load trackers before consent - cookieless analytics needs no banner, and a banner nobody can decline is worse than the tracking it fronts. Check: open the site in a fresh private window with the Network panel filtered to third-party origins and confirm nothing tracking-related fires before a choice is made.

Gotcha: on a static host nothing upstream sets headers for you - if the deploy does not ship a _headers file (Cloudflare Pages, Netlify) or a vercel.json headers block, HSTS, CSP and the rest do not exist, however carefully they were written in the ticket. Referrer-Policy, Permissions-Policy, cross-origin isolation, password fields, Turnstile and data-deletion rules are in the full security checklist.

SEO

Nothing here is Critical - a site with bad SEO still works - but each row is a launch-day finding that costs weeks of indexing to undo, and every one is checkable from the HTML.

Rule Priority What to do
One canonical per page, pointing at itself High <link rel="canonical"> on every page with the absolute final URL - one scheme, one host, one trailing-slash convention - and the same URL in og:url and the sitemap. Check: view source on three pages, including one paginated or filtered URL, and confirm the canonical is the clean version of the page you are on.
A unique, descriptive title on every page High A <title> that says what this page is and differs from every other page on the site; templates that emit the site name alone, or "Home", are the usual failure. Check: crawl the built site (Screaming Frog, or grep -rh "<title>" over the output piped to sort | uniq -d) - any line that comes back is a bug.
A meta description on every page High <meta name="description"> with one or two sentences that summarize the page and belong to it alone; Google rewrites weak ones, but a missing one hands the snippet to whatever text it finds first. Check: the same crawl - empty and duplicate descriptions sort to the top of the list.
robots.txt allows the crawl and points at a valid sitemap High /robots.txt exists, no longer carries the staging Disallow: / line, and ends with a Sitemap: line pointing at an XML sitemap that lists only indexable, 200-status URLs on the live host. Check: curl both files on the live host after the DNS change, then submit the sitemap in Search Console and read the discovered count.
No broken internal links High Every internal href resolves to a 200 without a redirect chain - moved pages get a 301 to the new URL, deleted pages get a real 404, and nothing still links to the old path. Check: a link checker over the built output (linkinator, Screaming Frog, or the crawler in your build) reporting zero 404s and zero 3xx on internal links.

Gotcha: a staging <meta name="robots" content="noindex"> or a Disallow: / that survives into production is the most expensive SEO bug a launch can ship, because nothing visibly breaks - the site simply never appears. Open Graph, structured data, URL hygiene, redirects and content-quality rules are in the full SEO checklist.

Testing

Three rows, all High, because a launch without them is a launch that finds out from its users.

Rule Priority What to do
Real-time error monitoring in production High Sentry, Bugsnag, Datadog RUM or an equivalent wired in before the first real user, with source maps uploaded so a minified stack resolves to a file and line, and a release tag that matches the deploy. Check: throw a test error from the console on the live site and watch it arrive in the dashboard with a readable stack.
End-to-end tests on the critical path High Playwright or Cypress covers the journeys that make money or make the site pointless without them - sign-up, sign-in, checkout, search, the contact form - and runs in CI on every merge to the production branch. Check: the pipeline log for the last deploy shows the suite ran and passed; a suite skipped "just this once" is not a gate.
Accessibility tests in CI High @axe-core/playwright (or cypress-axe, jest-axe) runs against every route in the sitemap and fails the build on any serious or critical violation. Check: the same pipeline log - and once, on a branch, delete an alt to prove the gate actually fails.

Gotcha: an axe pass proves the absence of the failures axe can detect and nothing more - pair it with the keyboard walk in the accessibility group above, or the checkout that passes every automated check will still be unusable. Unit, integration, visual regression, real-device and performance-budget rules are in the full testing checklist.

Keep going

Fifty rows is the floor, not the audit. The full checklists - HTML, CSS, JavaScript, images, performance, accessibility, security and privacy, SEO and testing - carry the Medium and Low rules, the code samples and the edge cases this page skipped, and each one links back here.

Most of these checks belong in a pipeline, not in a person. The CI/CD pipeline guide covers where the Lighthouse, axe, link-check and audit steps go, and the monitoring roundup covers what to run once the site is live. If the site is static, the static site stack shows a build that already clears the HTML, SEO and header rows for you.

Back to the frontend hub for the decision guides, or start with choosing a framework if the launch is still some way off.