TS-18: Web GUIs

This technical standard covers the design and implementation of web-based graphical user interfaces (GUIs) for applications.

For the design of URLs, which form part of the user interface of any web application, see TS-63: URL design.

For usage of web platform APIs, see TS-37: Web platform APIs.

Performance optimization

Web GUIs SHOULD be designed to be as fast and responsive as possible.

Performance is not only a satisfaction metric. Response time changes how users use an interface: a sub-100ms response invites interactive, iterative use, and a slow tool is used differently — and less often — than a fast one that solves the same problem. See TS-15: User interfaces, "Feedback and system status", for the general response-time thresholds (100ms/400ms/1s/10s) and this effect, which applies to any interface and is not repeated here.

For the fastest possible web GUIs, the following optimizations are RECOMMENDED. This standard follows a progressive enhancement model: serve a static baseline that works without JavaScript or CSS, then layer enhancements on top of it. Anything that requires JavaScript to function MUST be injected by JavaScript, not present in the initial markup and then broken without it.

Time to First Byte

Time to First Byte (TTFB) is the time between a browser requesting a page and receiving the first byte of the response. It is a foundational performance metric — every other rendering metric (LCP, FCP) is delayed by however long TTFB takes, so optimizing rendering without addressing TTFB has a ceiling.

  • TTFB has four main contributors: network latency, request routing, the time the application takes to run and query its data sources, and (for server-rendered pages) the cost of generating the HTML response itself. Each SHOULD be measured separately to find where time is actually spent.
  • Use a CDN to reduce network latency and, where practical, cache rendered HTML at the edge — even briefly — or move dynamic logic to edge compute, so fewer requests travel all the way back to an origin server.
  • Surface a server-side timing breakdown to the front end using the Server-Timing HTTP response header. This lets performance tooling (and browser DevTools) show where server time was spent — routing, database queries, rendering — directly alongside client-side metrics, rather than treating TTFB as an opaque number.

HTTP/2 and asset delivery

  • Serve web GUI assets over HTTP/2 (or later). HTTP/2 multiplexes many requests over a single connection, removing the head-of-line blocking and per-connection overhead that made HTTP/1.x asset-bundling workarounds necessary.
  • Under HTTP/1.x, bundling many small files into fewer, larger ones reduces the number of round trips, at the cost of coarser cache invalidation — a one-line change busts the cache for the whole bundle. Under HTTP/2, serving many small, natively-cacheable modules avoids that trade-off, since multiplexing removes the per-request penalty that made bundling necessary in the first place. Choose the strategy that matches the actual transport in use, and reassess it if the transport changes.

Script loading

  • Place <script> elements before the closing </body> tag, or use the defer attribute, so script downloading and execution does not block HTML parsing and rendering.
  • Use defer for scripts that must run in document order after the DOM is parsed; use async for independent scripts (such as analytics) that do not depend on DOM state and can execute as soon as they are downloaded, regardless of order.
  • Load scripts after stylesheets in the document, so style information is available before any script that might read computed styles runs.
  • Minimize the JavaScript actually shipped. Prefer a Baseline widely-available platform feature over a JavaScript reimplementation of the same capability; use a coverage tool (eg. Chrome DevTools' Coverage panel) to find and remove unused code; and periodically prune tag-manager tags that have accumulated without being removed.

Profiling

Diagnose a performance problem with browser DevTools before optimizing blind. Chrome DevTools' Performance tab records a timeline of JavaScript execution, layout, and paint work during a chosen interaction or page load, and is the direct way to confirm whether a suspected cause — a long task, a layout thrashing loop, an expensive paint — is the actual bottleneck, rather than guessing from the code alone. Its Memory tab captures heap snapshots that can be compared across a sequence of interactions to find a leak: an object that a snapshot shows growing in count or retained size across repeated cycles of the same interaction, when it should return to its starting size, points at retained references that never get released — see Memory-efficient DOM manipulation for the patterns (WeakMap/WeakRef, event-listener cleanup) that most commonly fix what this tool finds.

Reflows, repaints, and layout thrashing

  • Prefer CSS animations and transitions over JavaScript-driven animation. Where JavaScript-driven animation is unavoidable, animate an unstyled wrapper element rather than an element carrying layout-affecting styles directly, to reduce the layout work triggered by each frame.
  • Never animate or transition a CSS property that requires a layout update — margin, border, top, left, and similar. Prefer transform (eg. translateX) and opacity, which the browser can animate on the compositor without triggering layout, and which therefore cannot cause a cumulative layout shift.
  • Avoid layout thrashing: interleaving DOM reads (eg. offsetHeight) with DOM writes (eg. setting style) inside a loop forces the browser to recalculate layout on every iteration. Batch all reads together, then all writes, so layout is recalculated once.
  • Use the CSS contain property to scope layout, style, or paint calculations to a subtree, so the browser can skip recalculating the rest of the page when that subtree changes — particularly valuable for off-screen or independently-updating regions of a large page.
  • Keep the overall DOM size small. A large DOM makes every layout recalculation more expensive, independent of how carefully reads and writes are batched.
  • Use event delegation: attach one listener to a common ancestor rather than a separate listener to each of many child elements. This reduces the total number of bound listeners, which reduces both memory use and the setup cost of initializing many individual handlers. See JavaScript behaviors for how this applies to component-scoped behavior code specifically.

Interaction responsiveness

  • Track Interaction to Next Paint (INP) — the time from a user interaction to the next frame the browser paints in response — as a Core Web Vital alongside LCP and CLS.
  • Break up long-running JavaScript tasks so they yield control back to the main thread periodically, rather than blocking it for the whole task duration. Use the Scheduler API’s scheduler.yield() where available, or chunk the work manually, so user input is not delayed behind an in-flight task.

Instant navigations

  • Design pages to be eligible for the browser’s back/forward cache (bfcache): avoid setting Cache-Control: no-store on pages that should be bfcache-eligible, and avoid unload event listeners, which disqualify a page from bfcache in most browsers. A bfcache hit restores the page instantly from memory on back/forward navigation and eliminates the layout shifts a fresh navigation would otherwise cause.
  • Use the Speculation Rules API (<script type="speculationrules">) to pay forward the cost of a likely next navigation. prefetch pays the next page’s TTFB up front; prerender additionally pays FCP and LCP up front, so the navigation itself is effectively instant. Configure an eagerness level (immediate, moderate, eager) appropriate to the confidence of the prediction, and use href_matches or selector_matches predicates to scope which links qualify. This extends the hover-based HTML pre-fetch technique above with a declarative, browser-native mechanism, and SHOULD be layered with an opt-in/opt-out hook (eg. a data-prefetch attribute) so individual links can be excluded where speculative loading would be wasteful or unsafe.
  • Where a Speculation Rules cache holds stale or sensitive data — for example, after a user signs out — purge it explicitly with the Clear-Site-Data HTTP response header’s prefetchCache and prerenderCache directives.

Rendering and asset delivery

  • Server-render as much HTML as possible, preferably all of it. The browser’s native HTML engine will always be able to render HTML faster than any custom client-side JavaScript can. Where the LCP resource is an image, use a plain <img src>/srcset, not a data-src attribute that requires JavaScript to resolve, and prefer server-side rendering over client-side rendering so the image markup is present in the HTML source the browser first receives, discoverable before any JavaScript executes.
  • Pre-fetch as much HTML as possible. For example, when the user hovers over a link, the browser can pre-fetch the HTML for the linked page. Further performance optimization can be achieved by client-side JavaScript pre-fetching partial HTML for the linked page, and then dynamically inserting it into application’s shell. Global areas such as the navigation and footer do not usually require re-rendering when the user navigates to a new page.
  • Also use <link rel="preload"> in the HTML <head> section to suggest the browser preload assets such as CSS, JavaScript, and web fonts. This will reduce the number of blocking requests the browser has to make before it can do an initial render of server-side HTML. Where the LCP resource must be referenced from CSS or JavaScript rather than plain HTML markup, preload it explicitly this way so the browser does not have to wait to discover it.
  • Set fetchpriority="high" on the <img> element identified as the page’s LCP resource, to raise its fetch priority above other images competing for bandwidth early in the page load.
  • Use <link rel="dns-prefetch"> to suggest the browser pre-fetch DNS records for third-party domains that are used by the application. This can reduce the time it takes to resolve domain names to servers. Use this for your CDN, any third-party services called from the client-side code, and for any other assets or resources that are not served from the same domain as the web page document.
  • Use a CDN to store and serve static assets.
  • Use a proxy tool such as Squid to cache dynamic content that is pre-rendered by the server.
  • Use client-side HTTP caching aggressively. For example, use the Cache-Control header to specify how long a resource should be cached by the browser.
  • On the client-side, use a service worker to cache pre-rendered HTML and other dynamically-fetched assets. The service worker intercepts requests and serves up a cached version of the resource, if it has one. This is also helpful to provide offline support.
  • Inline CSS in a <style> tag in the HTML <head> section, uglified. This SHOULD be restricted to just your critical CSS, which is the minimum CSS required to do an initial render of a page. Additional CSS, required only for an optional enhanced user experience, should be deferred to after the page is rendered. If the overall size of the CSS is small, it can all be inlined. This means the browser can start to render the page as soon as HTML is received, without waiting for separate CSS resources to be downloaded. This technique will give you the fastest possible time to first paint – a metric known as the (Largest Contentful Paint (LCP), which is the time is takes the browser to fully render the largest element on the page (ie. after all styles, fonts, images, and other dependencies are fetched).
  • Similarly, for JavaScript, try to serve only the subset of code that is required to enable dynamic functionality on the current page. This is known as code splitting and tools are available to do this automatically (at compile time or dynamically on the server-side). Avoid loading all your JavaScript on all your pages. Which strategy code splitting should target depends on the HTTP protocol in service — see HTTP/2 and asset delivery. Code splitting decides what to serve; a build’s module bundler decides how it is packaged for delivery, and the two decisions are related but distinct.
  • Consider lazy loading additional JavaScript that enhances the user experience but is not required to enable the core functionality.

Images

Choose an image format for what the image contains, not by habit.

  • Use JPEG for photographs and other complex images with continuous color gradients. Its compression is lossy — quality degrades a little on every save — so keep an uncompressed source file and re-export from it rather than re-saving the same JPEG repeatedly.
  • Use PNG for line art, logos, and other images with sharp edges and flat areas of color, where lossless compression keeps the result crisp. PNG MUST be preferred over GIF for this purpose: it compresses losslessly and matches or beats GIF’s file size, with no reason to prefer GIF’s legacy 256-color palette today.
  • Use SVG for icons, logos, and other simple vector shapes. SVG remains crisp at any size and resolution, so it needs no @2x/@3x variants for high-density screens, unlike a raster format. See Icon fonts for why SVG, not an icon font, is this standard’s required choice for interface icons, and 1. Perceivable for the accessibility markup an SVG image itself needs.
  • Test formats empirically rather than applying these defaults rigidly — an image’s actual content determines which format compresses it best, and the guidance above is a starting point, not an absolute rule.

Give every image a text alternative appropriate to its role, per 1. Perceivable: a functional image (a button, an icon acting as a control) takes an alt that names the action ("Play", "Close"), a complex chart or diagram takes a caption or an accompanying accessible data table rather than relying on alt alone, and a purely decorative image takes an empty alt="".

Serve the right image size for the viewport, using the srcset and sizes attributes on <img>:

  • Use srcset with w descriptors (image-400.jpg 400w, image-800.jpg 800w) plus a sizes attribute stating the image’s expected rendered width, to let the browser pick the smallest source that satisfies the layout at the current viewport and device pixel ratio. This is the general case for a variable-width image whose display size changes with the viewport.
  • Use srcset with x descriptors (image.jpg 1x, image@2x.jpg 2x) instead where the image’s display size is fixed regardless of viewport — a logo or icon at a constant CSS size — and only the resolution needs to vary with device pixel ratio.
  • Use the <picture> element, with multiple <source> elements plus a fallback <img>, for art direction: serving a genuinely different crop or composition at different breakpoints, not just the same image at a different quality level. Reach for srcset/sizes alone when only the resolution should change; reach for <picture> only when the image itself should change.
  • Put fixed width and height attributes on images, or use an inline style attribute to set the width and height CSS properties of the images' containers. This allows the browser to allocate space for an image before it is downloaded, which prevents the page from jumping around as images are loaded – which counts as another re-render. For elements whose width is responsive but whose aspect ratio is fixed, use the CSS aspect-ratio property to reserve the correct space without hardcoding pixel dimensions. Where the final size cannot be known in advance (eg. user-generated content of variable length), set a min-height as a fallback to at least bound how severe the layout shift can be.
  • Use the native loading="lazy" attribute on below-the-fold <img> elements to defer their download until they approach the viewport. MUST NOT be applied to the page’s LCP image — lazy-loading the LCP resource delays the very metric this section is optimizing for.
  • Don’t be afraid to use age-old techniques such as image sprites to reduce the number of requests required to fetch things like product thumbnails, icons, and other small images.

Web accessibility

Web GUIs MUST be designed to be accessible to all users, including those with visual, auditory, motor, and cognitive disabilities. All web GUIs SHOULD aim to conform with the Web Content Accessibility Guidelines (WCAG), the international standard for web content accessibility. The most recent version is WCAG 2.2. Conformance is measured at three levels: Level A (minimum), Level AA (standard), and Level AAA (enhanced). Level AA is the target RECOMMENDED by this standard, and is required by law in many jurisdictions.

Level AA is a floor, not a ceiling: it is the minimum every web GUI covered by this standard MUST meet, and this standard’s normative requirements below are drawn from it. Exceeding it where practical is encouraged, and Beyond Level AA, at the end of this section, names specific Level AAA practices worth adopting as stretch goals above that floor.

WCAG 2.2 defines success criteria organized under four principles.

1. Perceivable

All content MUST be presentable to users in ways they can perceive.

Text alternatives:

  • All images and other non-text content — including icons, charts, audio, and controls — MUST have a descriptive text alternative that conveys their meaning.
  • Purely decorative images SHOULD use an empty alt="" attribute, and optionally role="presentation", so assistive technologies can skip them.
  • An inline SVG image MUST carry its own text alternative — an alt attribute has no equivalent inside <svg>. Give it a <title> element as its first child, playing the same role as <img alt>, and a <desc> element where the graphic needs a longer explanation than a title can carry (e.g. a chart or a multi-step diagram). Because SVG 1.1’s own accessibility elements are not exposed consistently across browsers and screen readers, also give the root <svg> element role="img" and aria-labelledby referencing the <title>/<desc> IDs, so the browser accessibility tree carries the same information reliably:
    <svg viewBox="0 0 500 300" role="img" aria-labelledby="chart-title chart-desc">
      <title id="chart-title">Chemical Reaction</title>
      <desc id="chart-desc">Animated illustration showing the stages of a
      chemical reaction in a laboratory.</desc>
    </svg>

    Where the SVG contains an interactive element, such as a link, omit role="img" from the root — a role of img tells assistive technology to treat the whole graphic as a single non-interactive image, which hides the interactive element inside it. See Icon fonts for why SVG is this standard’s required format for interface icons in the first place.

Time-based media:

  • Video MUST NOT autoplay. Playback MUST be explicitly initiated by the user, who MUST be given the video’s title, a description, and its duration before choosing to start it, so they can decide whether and when to play it rather than having audio or motion sprung on them unexpectedly.
  • Pre-recorded videos with audio MUST have synchronized captions that cover all speech and relevant sound effects.
  • Pre-recorded audio-only content MUST have a text transcript.
  • Pre-recorded video-only content MUST have an audio description or text alternative.
  • Live video with audio MUST include real-time captions.
  • Implement captions, transcripts, and audio descriptions using the HTML <track> element with WebVTT (.vtt) files, rather than burning captions into the video itself. A <track kind="captions"> element lets users toggle captions and lets assistive technology navigate them as text; a <track kind="descriptions"> element carries a machine-readable audio description.
<video controls>
  <source src="video.mp4" type="video/mp4">
  <track kind="captions" src="captions-en.vtt" srclang="en" label="English">
  <track kind="descriptions" src="descriptions-en.vtt" srclang="en">
</video>

Adaptable:

  • Visual information and relationships — such as headings, labels, and groupings — MUST be communicated in the code using semantic HTML elements (eg. <label>, <ul>, <h1>) or ARIA attributes, so that assistive technologies can understand the page structure.
  • Content MUST appear in a logical reading order in the source, regardless of how it is visually presented.
  • Instructions MUST NOT rely solely on sensory properties such as color, shape, size, or position to convey meaning.
  • Content MUST remain readable and usable in both portrait and landscape orientations.
  • Common form fields (such as name, email, and address) SHOULD use the autocomplete attribute to enable browser autofill.
  • HTML markup MUST be valid, validated with the W3C Markup Validation Service or equivalent tooling. Valid markup is a baseline for reliable assistive-technology interpretation — see 4. Robust — not merely a code-quality nicety.

Distinguishable:

  • Color MUST NOT be the only means of conveying information. Always pair color with a supplementary cue such as a text label, icon, underline, or pattern.
  • Audio that plays automatically for more than 3 seconds MUST be pausable or stoppable without relying on system-wide volume controls. Auto-playing video and interactive content — including games — SHOULD be paused by default rather than merely pausable, so the burden is not on the user to notice and stop it.
  • Normal-sized text MUST have a contrast ratio of at least 4.5:1 against its background. Large text (over 24px regular, or over 19px bold) requires a ratio of at least 3:1.
  • Text MUST remain readable when zoomed to 200%.
  • Text SHOULD be real text, not images of text (except for logotypes and other essential visual treatments).
  • Content MUST reflow to a single column at a viewport width of 320px without requiring horizontal scrolling.
  • Interactive controls and meaningful graphics MUST have a contrast ratio of at least 3:1 against adjacent colors.
  • Layout MUST NOT break when custom text spacing is applied (increased line height, letter spacing, and word spacing).
  • Tooltip-style content that appears on hover or keyboard focus MUST be dismissible (eg. via the Escape key), hoverable, and persistent until the user dismisses it. A tooltip is text-only and non-interactive; use a dialog instead for any popup containing interactive content. Label an icon trigger with aria-labelledby, provide the tooltip’s content with aria-describedby, and mark the tooltip itself with role="tooltip" — do not combine role="tooltip" with aria-haspopup, and do not use the title attribute, which is inconsistently exposed to assistive technology and inaccessible on touch devices. Because tooltips have no reliable touch equivalent, prefer a visible label over a tooltip wherever the interface will be used on touch devices. For an informational popup that must work on touch, use a toggletip instead — a <button> that reveals content in a role="status" live region on tap — rather than relying on hover.

2. Operable

All UI components and navigation MUST be operable by all users.

Keyboard accessible:

  • All functionality MUST be operable using a keyboard alone, unless the task inherently requires freehand input (eg. drawing).
  • Focus MUST never become trapped in a UI component. It MUST always be possible to move focus in and out using standard keyboard controls.
  • Single-character keyboard shortcuts, if used, MUST be remappable to include a modifier key or disableable entirely.

Enough time:

  • Time limits SHOULD be avoided unless essential to the task.
  • Where a time limit is used, users MUST be able to turn it off, adjust it to at least 10x the default, or extend it on request. Users MUST be warned at least 20 seconds before expiry, with a simple, one-action way to extend (eg. a single "Continue session" button), so the warning itself does not become another obstacle to clear.
  • Moving, scrolling, blinking, or auto-updating content that persists for more than 5 seconds MUST be pausable, stoppable, or hideable.

Seizures and physical reactions:

  • Content MUST NOT flash or flicker more than three times per second, unless the flash falls within safe size and luminance thresholds.
  • Animations triggered by user interaction SHOULD be suppressible via the prefers-reduced-motion CSS media query or a site-level toggle.

Navigable: A skip-navigation mechanism MUST be provided so keyboard users can bypass repeated header and navigation blocks and jump directly to the main content.

  • Implement the skip link as the first focusable element on the page, hidden visually until it receives keyboard focus, at which point it MUST become visible. Pair it with a "back to top" link at the end of the main content region so keyboard users can return without re-traversing the whole page.
<a class="skip-link" href="#main-content">Skip to main content</a>
<!-- ... header, navigation ... -->
<main id="main-content">
  <!-- ... page content ... -->
  <a href="#top">Back to top</a>
</main>
  • Use the <link rel="index|next|prev|contents"> elements in the document <head> to expose document-level navigation metadata — the site index, and the previous/next page in a sequence such as a paginated article or a documentation series — for user agents and assistive technology that surface this metadata as navigation shortcuts.
  • Every page MUST have a unique and descriptive <title>.
  • Keyboard focus order MUST follow a logical and meaningful sequence that matches the reading order of the page.
  • The purpose of each link MUST be clear from the link text alone, or in combination with its surrounding context.
  • At least two methods MUST be available to locate pages or content within the site (eg. a navigation menu and a site search). Breadcrumbs and a sitemap are two further, complementary ways to satisfy this: breadcrumbs give the user their current location within the site hierarchy on every page they visit, and a sitemap gives an overview of the whole hierarchy at once.
  • Headings and form labels MUST be descriptive.
  • A visible focus indicator MUST always be shown when navigating via keyboard.
  • Focused elements MUST NOT be fully obscured by sticky headers, banners, or other overlapping content.

Input modalities:

  • Functionality that relies on multi-point or path-based gestures (such as swiping or pinching) MUST also have an alternative that works with a single pointer (such as a tap or click).
  • Actions MUST trigger on pointer release (mouse-up or finger lift), not on press, so that accidental activations can be cancelled by moving the pointer away before releasing.
  • The visible label text of a button, link, or form field MUST also be present in its accessible (programmatic) name in the code, so voice control users can activate it by speaking the visible label.
  • Functionality triggered by device motion (such as shaking or tilting) MUST also be achievable without motion, and motion-based input MUST be disableable. Touch and click targets MUST be at least 24x24px.

3. Understandable

Content and UI behavior MUST be understandable by all users.

Readable:

  • Every page MUST identify its primary language using the lang attribute on the <html> element.
  • Passages of content in a different language MUST be marked with the correct lang attribute on the containing element.

Predictable:

  • No unexpected context change MUST occur when an element receives focus (eg. auto-opening a popup or navigating away).
  • Changing the value of a form field MUST NOT trigger unexpected context changes such as auto-submitting the form or reloading the page.
  • Navigation MUST appear in a consistent location and order across pages.
  • Elements that perform the same function MUST be labeled and behave consistently across the site.
  • Help options — such as a contact link or support widget — MUST appear in the same location across pages.
  • Use <noscript> only to surface a message explaining that content genuinely cannot work without JavaScript — it MUST NOT be used to fork the experience into separate JS and no-JS versions. This follows directly from the progressive-enhancement model stated in Performance optimization: the baseline already works without JavaScript, so <noscript> is only ever needed for the rare feature that has no non-JS equivalent at all.

Input assistance:

  • All form fields MUST have clear, descriptive labels or instructions. Group related fields in a long form with <fieldset> and <legend>, so assistive technology can navigate the form by group rather than as one undifferentiated sequence of fields.
  • Errors and validation failures MUST be identified and described in text, not just by color or visual styling alone.
  • Error messages MUST include a suggestion for how to fix the problem where possible.
  • Use the native HTML constraint validation API as the first line of validation: the required attribute, typed inputs (type="email", type="number", type="url"), the pattern attribute, and maxlength. These give the browser’s own accessible, keyboard- and screen-reader- compatible validation UI for free. Use the setCustomValidity() method to surface custom or asynchronous validation results (eg. a username availability check) through that same native UI, rather than building a parallel one. See TS-15: User interfaces, "Error messages", for how the resulting messages should be worded. General HTTP API request validation is covered by TS-21: HTTP APIs; this item is about the client-side input experience specifically.
  • Before submitting forms that trigger consequential actions (such as payments or legal submissions), users MUST be able to review, correct, or confirm their input.
  • Users MUST NOT be required to re-enter information they have already provided earlier in the same process.
  • Authentication MUST NOT rely solely on memorized information. Copy-paste, password managers, and alternative authentication methods (such as email magic links) MUST be supported.

4. Robust

Content MUST be robust enough to be reliably interpreted by current and future assistive technologies.

Compatible:

  • All interactive elements MUST expose an accessible name (what the element is), the correct semantic role (what it does), and any current value or state, so that assistive technologies such as screen readers can correctly identify and interact with them.
  • Use semantic HTML elements wherever possible — supplemented by ARIA roles and properties only where native semantics are insufficient. Identify the page’s major regions with ARIA landmark roles — banner for the site header, navigation for each navigation block, main for the primary content — and give each navigation landmark a distinguishing aria-label where a page has more than one, so assistive-technology users can jump directly between regions rather than reading through all of them in sequence.
  • Status messages — such as form confirmation notices, error summaries, or live content updates — MUST be coded using appropriate ARIA live-region roles (such as role="status" or role="alert"), so that assistive technologies announce them without requiring keyboard focus to move to the message element.

Structured data:

  • Mark up machine-readable metadata about a page’s content — an article’s author and publish date, a product’s price and availability, an event’s date and location — using schema.org vocabulary in JSON-LD, placed in a <script type="application/ld+json"> element in the document <head>. JSON-LD is preferred over RDFa or Microdata: it is a separate, self-contained block rather than attributes threaded through the visible markup, so it can be added, changed, or removed without touching the page’s HTML structure at all.
  • Structured data is consumed primarily by search engines and other automated agents, not by assistive technology directly — see TS-19: SEO for how it affects search-result presentation. It is included here because the markup itself is a web-GUI implementation concern: it lives in the page the GUI renders, and gets the same care as any other machine-readable page metadata.

5. Neurodiversity

WCAG’s four principles set a compliance floor; they do not, on their own, address the reading, attention, and processing differences that come under neurodiversity — dyslexia, dyscalculia, ADHD, autism, and related profiles. This section’s guidance goes beyond WCAG AA in places, consistent with this standard’s own opening claim to cover cognitive disabilities.

Typography and font:

  • Prefer a sans-serif, humanist typeface with open apertures and distinct character widths over a serif or grotesque one. Favor a typeface whose uppercase "I," lowercase "l," and digit "1" remain visually distinct, and whose lowercase "a" and "g" use the single-storey forms closer to handwriting — both reduce the letter-confusion that compounds reading difficulty for dyslexic users. Treat "dyslexia-friendly" marketing claims for a specific typeface with skepticism where they cite no supporting research; the shape properties above, not the branding, are what to select for.
  • Set body text no smaller than 1rem (16px), with headings at least 20% larger than body text and in a heavier weight. Set line height to 150–170% of the font size, and increase letter spacing in labels and captions by roughly 35% of the average letter width. These reduce the "river" and "swirl" crowding effects that increase reading effort for dyslexic readers, freeing cognitive capacity for comprehending the content rather than parsing the text.

Colour:

  • Prefer a 7:1 contrast ratio (WCAG AAA) for body text and meaningful graphics over the 4.5:1 AA minimum required elsewhere in this standard — low contrast disproportionately affects users with dyslexia and dyscalculia, not only low vision. Where pure black-on-white produces visible glare or eyestrain for some users, offer an alternative colour-overlay theme rather than only a light/dark toggle; overlay themes are a documented mitigation for visual stress linked to Irlen syndrome, dyslexia, and autism.
  • Use colour consistently to mark function across the interface — the same colour for every "submit" action, another for every "cancel" or neutral action, another for every destructive action. An inconsistent colour vocabulary forces the user to re-learn the interface on every screen, which disrupts working memory more than it does for neurotypical users.

Interactive elements:

  • Make every clickable element look clickable through visual styling alone — do not rely on placement or surrounding context to signal interactivity. Give links a colour difference and an underline distinct from body text, and give every interactive element a visually distinct hover, active, and focus state. Favor a larger click target over a compact one, consistent with Fitts’s Law, to reduce the precision demanded of users with fine motor-control difficulties.

Interface layout:

  • Keep navigation, primary content, and footer regions in a consistent position across the application, and show only the elements essential to the current page’s purpose — an interface that hides secondary and decorative elements by default reduces the choice overload that disproportionately affects users with ADHD.

Numbers:

  • Segment long numeric sequences — phone numbers, card numbers, reference codes — into grouped parts rather than one unbroken run of digits, and choose a typeface that keeps zero ("0") and the letter "O" visually distinct. Auto-format numeric input fields to strip irrelevant characters and correct common substitutions (such as a typed "o" where a digit is expected) rather than rejecting the input outright. Pair a number with a visual representation — a progress bar, a chart, a relative-time expression such as "in 3 days" alongside a date — rather than presenting the bare figure alone; this holds particularly for users with dyscalculia.

Animation:

  • Restrict any animation to at most one-third of the viewport, and never let it track the full screen height or width — this is in addition to, not instead of, honoring prefers-reduced-motion per 2. Operable. Avoid parallax effects, scroll-jacking, and auto-looping video, which can trigger vertigo, disorientation, or migraine in users with vestibular disorders. Prefer a lateral or fade transition over a diagonal or otherwise unpredictable direction of movement, and require the user to initiate any non-essential animation rather than starting it automatically.

Written communication:

  • Keep instructional and system messaging brief, and hold a term’s meaning and wording constant everywhere it appears in the interface — a renamed label or an inconsistent term for the same concept erodes trust and adds avoidable cognitive load. Write error messages as polite and solution-focused rather than accusatory, and break a high-stakes, multi-step process (such as enrollment or payment) into a clear sequence of discrete steps rather than scattering instructions across several screens or channels.

Beyond Level AA

Level AA, above, is this standard’s required floor. The following Level AAA practices are OPTIONAL stretch goals, worth adopting where the effort is proportionate to the audience and content — they are not requirements, and a GUI that does not implement them is still conformant.

  • Enhanced contrast. A 7:1 contrast ratio for normal text (4.5:1 for large text), rather than the 4.5:1/3:1 minimums required in 1. Perceivable. This is the same ratio recommended for all body text and meaningful graphics in 5. Neurodiversity, where it addresses low vision as well as the reading difficulties covered there.
  • Sign-language interpretation. A sign-language video track alongside pre-recorded audio content, for users who are deaf and for whom the written captions required at Level AA are a second language rather than a first one. Implement it as an additional <track> (or a picture-in-picture video overlay) alongside the captions and audio description already required in 1. Perceivable, "Time-based media".
  • Lower-secondary reading level. Where content is not inherently technical, write it at a reading level no higher than lower-secondary education, supplementing rather than replacing more advanced text where full precision requires it. This benefits users with cognitive or learning disabilities beyond what the plain, solution-focused microcopy already required in 5. Neurodiversity covers, and benefits any reader unfamiliar with the subject matter.

Fonts

Web fonts are part of the critical rendering path and directly affect performance metrics such as largest contentful paint (LCP) and cumulative layout shift (CLS). They MUST be treated with the same care as any other performance-critical asset.

Format

  • WOFF2 is the only web font format that SHOULD be served in modern web applications. It has universal browser support and is the most compressed and efficient web font format. Legacy formats — including WOFF, TTF, OTF, EOT, and SVG fonts — SHOULD NOT be served, as they impose a performance cost on every visitor with no benefit for modern browsers.

Hosting

  • Fonts SHOULD be self-hosted rather than loaded from third-party CDNs such as Google Fonts. Third-party font services add DNS lookups and network latency, leak visitor data to the third party (a GDPR concern in many jurisdictions), and provide no practical caching benefit (modern browsers partition caches per origin, so a font fetched on one site is never reused on another).
  • Font files SHOULD be given long Cache-Control lifetimes (months up to a year), with versioned file names used for cache-invalidation when fonts change.

Subsetting

  • Fonts SHOULD be subsetted so that only the glyphs actually needed by the application are served. A complete font family can be several hundred kilobytes or more; most of those glyphs are typically never rendered. Tools such as fonttools (pyftsubset), Glyphhanger, and Subfont can automate subsetting.
  • Use the unicode-range descriptor in @font-face declarations to declare separate @font-face blocks per script (eg. Latin, Latin Extended, Cyrillic). The browser will only download the subsets it needs for the characters on the current page.
  • Be conservative when subsetting non-Latin scripts such as Arabic, Devanagari, and CJK. These scripts rely on shaping tables (GSUB/GPOS) and contextual forms, so aggressive subsetting can break word rendering entirely. Test non-Latin subsets thoroughly.

Loading strategy

  • Inline @font-face declarations in a <style> block in the HTML <head>, rather than placing them in an external stylesheet. Fonts declared in external CSS are not discovered until that stylesheet is downloaded and parsed, delaying font requests unnecessarily.
  • Never use @import to load fonts or font stylesheets. Each @import adds a sequential round trip before the font can be discovered, pushing font requests very late in the render waterfall.
  • Preload critical fonts using <link rel="preload" as="font" type="font/woff2" crossorigin> in the <head>. This instructs the browser to begin fetching the font immediately, rather than waiting to encounter the @font-face rule.
  • Preload only the subset(s) needed for above-the-fold content; preloading every subset defeats the purpose by forcing all of them to download regardless of whether they are needed.
  • Use the font-display descriptor in every @font-face rule to control rendering during font load. font-display: swap is the RECOMMENDED default. It renders fallback text immediately and swaps to the custom font when it arrives, preventing invisible text. Consider font-display: optional for decorative or non-critical fonts. It permits the browser to skip the custom font entirely on slow connections. Without font-display, a browser’s default behavior is either FOIT (Flash of Invisible Text — the browser hides text until the custom font arrives or a timeout expires) or FOUT (Flash of Unstyled Text — it renders the fallback immediately and swaps in the custom font later). font-display: swap is, in effect, a browser-native way to guarantee FOUT behavior and avoid FOIT. Before font-display existed, the same FOUT behavior was achieved manually with the Font Face Observer library and a JavaScript class toggled on the document root once the font’s load() promise resolved; this pattern predates font-display and is a legacy fallback for the rare case of needing finer control than the CSS descriptor gives.
  • When loading a whole font family (multiple weights or styles), load the group together — for example, with Promise.all() over each face’s FontFace.load() — rather than letting each weight resolve independently. Loading the family as a unit avoids showing faux-bold or faux-italic fallback styles for weights that have not yet arrived, and collapses what would otherwise be several separate reflows (one per weight swap) into a single reflow when the whole group is ready.
  • Where a page loads more than one custom font, consider prioritised (sequential) loading: load a small, critical font first — for example, a body-text face — and gate the load of a larger, secondary font (eg. a decorative display face) on the first one succeeding. This gets critical text rendering with its intended font sooner, without making the whole page wait on the largest font file.
  • Cache font-loaded state in sessionStorage once a font has successfully loaded, and check it before applying `font-display: swap’s fallback behavior on subsequent page views within the same session. This lets repeat views within a session render directly in the custom font — since the browser’s own HTTP cache already guarantees the font file itself does not need re-fetching — avoiding a visible FOUT flash on every navigation within one visit.
  • Consider using HTTP 103 Early Hints to push critical font preload hints before the main HTML response is delivered, reducing time-to-text on the first round trip.

Fallbacks

  • Design a robust system font stack as the fallback for every custom font. Fonts SHOULD be treated as progressive enhancement: the page MUST be fully legible and usable even if a custom font never loads.
  • Tune fallback metrics to minimize CLS when a custom font swaps in. Use the size-adjust, ascent-override, descent-override, and line-gap-override descriptors inside @font-face to align the dimensions of the custom font with those of the fallback, so that text does not reflow visibly when the swap occurs.

Variable fonts

  • Variable fonts — which encode multiple weights, widths, and styles in a single file — SHOULD be used when they genuinely reduce payload compared to loading multiple static font files. They are not a universal win. If only one or two weights are needed, separate static WOFF2 files subsetted to the required glyphs may be smaller. Audit and measure the payload before committing to a variable font.
  • Variable fonts SHOULD be subsetted and scoped using unicode-range in the same way as static fonts.

Icon fonts

  • Icon fonts MUST NOT be used. They are inaccessible (screen readers announce their private-use Unicode characters as gibberish), fragile if the font file fails to load, and wasteful in that the entire font file must be downloaded even when only a handful of icons are used. Use inline SVGs or SVG sprites instead. They are semantic, accessible, styleable with CSS, and can be loaded on demand — see 1. Perceivable for the <title>/<desc>/ARIA markup an inline SVG needs to actually be accessible.

JavaScript behaviors

Client-side JavaScript that adds interactive behavior to a server-rendered page SHOULD be organized around the same component behaviors the GUI itself is built from, rather than as a monolithic script that reaches across the whole page.

Component behaviors

  • Think in component behaviors. A piece of client-side JavaScript SHOULD affect exactly one DOM subtree — a component — and SHOULD be kept in its own file, dedicated to that component’s behavior. This mirrors how the GUI is already decomposed visually into components; the JavaScript that animates a component follows the same boundary.
  • One behavior file per component. Keep each self-contained behavior file in a dedicated behaviors/ directory, named after the CSS selector or data attribute it targets, so a reader can find a component’s behavior file from its markup alone.
  • Mark hooks with a data-js- attribute*, not a class or ID, to disambiguate a JavaScript hook from a CSS styling hook. Where a class must be used for a JavaScript hook instead, prefix it js- and MUST NOT also carry styling — restyling a component then cannot silently break its behavior, and the origin of any given behavior stays unambiguous from the class name alone.
  • Load all behaviors on every page. Because each behavior is scoped to its own selector, it is safe to concatenate all of them into one bundle loaded site-wide; a behavior with no matching element on the current page simply does nothing. This avoids maintaining a per-page manifest of which scripts to include. Where the resulting bundle grows large enough to affect load performance, apply the code-splitting guidance in Performance optimization instead — the two approaches address different scales of the same problem.
  • Bind on document-ready, and guard against absence. Initialize a behavior inside the DOMContentLoaded handler, so its target element is guaranteed to exist in the DOM before the behavior runs. Bail out immediately if the target is absent (eg. if (!el) return;), so the behavior has no effect, and throws no error, on pages that do not use it.
  • Re-initialize behaviors bound to dynamic content. Where new DOM is injected after the initial page load (eg. by an AJAX-loaded modal), the behaviors relevant to that new content MUST be re-run against it. Use an idempotent include-guard — for example, marking an element with a data-js-initialized attribute once its behavior has run — so re-initialization skips elements that are already set up rather than double-binding them.
  • Organize shared helpers separately. A function reused by more than one behavior belongs in a dedicated helpers/ directory and a shared namespace, not duplicated into each behavior file that needs it.
  • Integrate third-party scripts as behaviors too. A third-party widget (eg. a calendar picker or a payments SDK) SHOULD be initialized through the same component-behavior convention as first-party code — bound to a dedicated hook, scoped to its own subtree — rather than initialized ad hoc wherever it happens to be needed. This keeps every piece of interactive behavior discoverable the same way, first-party or not.

Memory-efficient DOM manipulation

  • Prefer showing and hiding existing elements over recreating them. Where a piece of server-rendered markup can be toggled visible or hidden, do that instead of destroying and rebuilding the equivalent DOM with JavaScript. This keeps the DOM mostly static, which avoids both repeated garbage-collection churn and the layout cost of rebuilding subtrees.
  • Read with textContent, not innerText. innerText is layout-aware — reading it forces the browser to compute the element’s current rendered styles, triggering a reflow. textContent returns the raw text content without touching layout.
  • Insert with insertAdjacentHTML, not innerHTML. Assigning to innerHTML destroys and recreates the entire existing subtree before inserting the new content; insertAdjacentHTML only affects the position it targets, leaving surrounding DOM (and any state or listeners attached to it) intact.
  • Use <template> for creating and inserting new nodes. Where new DOM genuinely must be created, clone a <template> element’s content and insert it with appendChild or insertAdjacentElement. This is the fastest browser-native pattern for producing fully-formed DOM nodes from a string, faster than building elements individually with createElement calls.
  • Batch multiple insertions with createDocumentFragment. Where several nodes must be inserted at once, assemble them into a DocumentFragment first, then insert the fragment in a single operation. This triggers one reflow instead of one per node.
  • Use WeakMap/WeakRef to associate data with DOM nodes. Storing per-element data in a plain object or Map keyed by the element keeps that element (and its associated data) alive even after it is removed from the DOM, because the map still holds a strong reference to it. A WeakMap or WeakRef lets the node and its associated data be garbage-collected together once the node is removed and no other reference to it remains.
  • Clean up event listeners. Remove a listener with removeEventListener when it is no longer needed; use the addEventListener once option for a listener that should fire only once and then detach itself; and use an AbortController, passed as the signal option to multiple addEventListener calls, to unbind a whole group of listeners in one abort() call — useful when tearing down a component behavior bound to content that has since been removed.

Form submission integrity

A user who double-clicks a submit button, or resubmits a form after a slow response, can trigger the same consequential action — a payment, an order — twice. Prevent this with a client-side idempotency key:

  • When a form is first rendered, include a hidden <input type="hidden" name="idempotency_key"> populated with a unique value generated at render time. This value MUST stay the same across repeated submissions of the same form instance, even if the user submits it more than once.
  • The server uses this key to deduplicate the request, so a second submission carrying the same key does not repeat the side effect. See the idempotency-key guidance in TS-21: HTTP APIs, "Safeness and idempotency", for the server-side half of this pattern.
  • This technique complements, but does not replace, disabling the submit button immediately after the first click (to prevent the double-click itself) and following the POST-redirect-GET pattern (so a page reload does not resubmit the form). Use the idempotency key as the authoritative safeguard, since the client-side disable can be bypassed (eg. by a slow network causing a user to resubmit from a second tab) in a way the server-side key cannot.

This is distinct from the WCAG requirement that a consequential submission be reviewable and confirmable before it commits — see 3. Understandable, "Input assistance" — which addresses informed consent, not duplicate submission.

DOM interaction conventions

These conventions govern how a component behavior touches the DOM and reads its inputs. They are implementation-agnostic conventions about what a behavior does; for the platform APIs that carry them out — the event propagation model, fetch, CORS — see TS-37: Web platform APIs, "DOM events and HTTP requests".

  • Only apply interactive behavior to semantically interactive elements<a>, <button>, <input>, <select>, and <textarea>. These elements carry built-in keyboard operability and assistive-technology semantics for free; any other element made clickable or tappable requires the behavior to reimplement that support by hand (a tabindex, a role, and keyboard handlers, per 2. Operable). Where a non-interactive container needs to become interactive — for example, making a table’s header cells sortable — inject a <button> into the cell and bind the behavior to the button, rather than binding it to the <th> itself.
  • Attach behaviors to data-js- hooks, never to HTML event attributes.* onclick, onchange, and the other inline event-handler attributes MUST NOT be used. This restates, for the specific case of event attributes, the data-js-*-hook convention in Component behaviors: a behavior discovered only by reading a JavaScript file, not one scattered through the markup as inline attribute values, keeps behavior and markup in one place each.
  • Prefer requestAnimationFrame over setTimeout/setInterval for any animation a behavior drives directly. requestAnimationFrame synchronizes the callback with the browser’s own repaint cycle, so it produces smoother results and lower power consumption than timer-based polling. This applies only to the rare case where an animation cannot be expressed in CSS; see repaints, and layout thrashing for why a CSS animation SHOULD be preferred in the first place.

Pattern libraries and living style guides

A pattern library (or living style guide) is a standalone reference page, built from the application’s own component markup and styles, that catalogs every reusable UI component — button, form field, card, modal — alongside its variants, states, and usage guidance. "Living" distinguishes it from a static design document: because it renders the application’s actual components rather than a design tool’s approximation of them, it cannot drift out of sync with what ships.

  • Build the pattern library from the same markup, CSS, and component behaviors the application itself uses — via a shared component-template system, or a tool that extracts and isolates each component for standalone display — rather than maintaining a second, hand-copied set of examples. A hand-copied example is a snapshot that starts going stale the moment it is written; a pattern library built from the real components updates automatically as those components change.
  • Document each component’s states (default, hover, focus, disabled, error) and variants (size, color, layout options), not only its default appearance. A component’s edge-case states are exactly the ones a new contributor is least likely to discover by reading the page the component normally appears on.
  • Treat the pattern library as the canonical reference for whether a new UI need can be met by an existing component before a new one is built. Checking it first is what keeps a component set from accumulating near-duplicate components that differ only by an oversight.
  • Name design tokens by semantic role, not by literal value. A design token — a named CSS custom property standing in for a raw color, spacing, or typography value — SHOULD be named for what it means in the interface (--color-bg-danger, --color-text-brand-hover), not for what it literally is (--color-red-500). A hierarchical --<category>-<subcategory>-<state> naming scheme (for example: category bg or text, subcategory a semantic intent such as brand or danger, state a suffix such as hover or disabled) keeps a large token set navigable and lets the underlying value change — a rebrand, a dark-mode variant — without renaming every place the token is used. This is the naming convention Shopify’s Polaris design system uses for its own token set, and it generalizes past that one system: a semantic name is what makes a token library a design system rather than a values list.

CSS layout and typography

Fluid typography

Use clamp() with viewport units to size type fluidly between a minimum and a maximum, rather than jumping between fixed sizes at breakpoints:

h1 {
  /* Scales smoothly between 1.5rem and 3rem across the viewport. */
  font-size: clamp(1.5rem, 1rem + 2vw, 3rem);
}

This produces type that scales continuously with the viewport, avoiding the visible, stepped jumps a fixed set of breakpoint-specific sizes produces.

Container queries

Where a component’s layout or type size should respond to the space it has available, rather than the viewport as a whole, use CSS container queries instead of a media query:

.card-container {
  container-type: inline-size;
}

.card-title {
  font-size: 1rem;
}

@container (min-width: 30rem) {
  .card-title {
    font-size: 1.5rem;
  }
}

The cqi unit (a percentage of the query container’s inline size) can be used in place of viewport units for sizing that should scale with the container rather than the viewport. Wrap @container rules in an @supports check, or otherwise ensure the component still renders acceptably in a browser without container-query support, as progressive enhancement.

A component that must adapt to its container, independent of where it is placed on the page, is the situation container queries are for; a page-level layout decision that genuinely depends on the whole viewport is still a media query’s job.

Intrinsic layouts

Prefer an intrinsic layout — one that adapts to its content and available space through the layout algorithm itself — over a fixed set of breakpoint-specific layouts, wherever the content allows it. The canonical example is the "Sidebar" pattern: a flex or grid layout where a sidebar has a fixed or minimum width and the main content area is allowed to wrap onto its own line once the available width drops below what both columns need, with no explicit breakpoint declared at all:

.layout {
  display: flex;
  flex-wrap: wrap;
}

.sidebar {
  flex-basis: 20rem;
  flex-grow: 1;
}

.main {
  flex-basis: 0;
  flex-grow: 999;
  min-inline-size: 50%;
}

Where content-driven wrapping alone is not sufficient — for example, an element that should be hidden entirely below a certain container size, rather than reflowed — combine the intrinsic layout with a container query that hides or shows specific elements, rather than abandoning the intrinsic approach altogether.

Readable measure and heading balance

  • Use the CSS ch unit to constrain body text to a readable line length — typically in the range of 60-75 characters (max-inline-size: 60ch). Unconstrained text on a wide viewport produces line lengths that are measurably harder to read.
  • Use text-wrap: balance on headings to distribute their text evenly across lines, avoiding a short, awkward final line (an "orphan"). Reserve it for headings and other short text — the browser’s balancing algorithm is not intended, and SHOULD NOT be used, for body-length paragraphs.

Responsive design

A web GUI MUST adapt to the full range of viewport sizes it will be viewed on, from a small phone screen to a large desktop display, without requiring horizontal scrolling or losing functionality. See 1. Perceivable for the WCAG reflow requirement at 320px; this section covers the broader methodology for achieving it.

  • Design and build mobile-first: write the base styles for the smallest supported viewport, then layer on larger-viewport styles with min-width media queries. This produces a simpler cascade than the reverse (desktop-first with max-width overrides), because each media query only ever adds complexity, never subtracts it.
  • Base breakpoints on the content, not on the dimensions of any specific device. Resize the viewport until the current layout starts to look awkward — text lines grow too long, or elements start to crowd — and place a breakpoint there. A breakpoint chosen to match a popular phone or tablet today is a breakpoint tuned to a device that will not be popular indefinitely.
  • Set media query breakpoints in rem, not px. A rem-based breakpoint scales with a user’s browser font-size setting; a px-based one does not, so a user who has increased their default font size for readability can be shown the desktop layout on a viewport that is, relative to their actual text size, no wider than a mobile one.
  • Prefer container queries over a viewport media query wherever a component’s layout should respond to the space it has available rather than the viewport as a whole — see Container queries for the syntax. A component that must look right regardless of where it is placed on the page needs a container query; a page-level layout decision that genuinely depends on the whole viewport still needs a media query.
  • Every page MUST include the viewport meta tag, so mobile browsers render the page at the device’s actual width rather than a virtual desktop-sized viewport scaled down:
    <meta name="viewport" content="width=device-width, initial-scale=1">

Push notifications

Browser push notifications are a high-friction permission to request, and a poorly-timed or poorly-explained request trains users to decline it reflexively — including for GUIs where the notification would genuinely have been useful.

  • MUST NOT request notification permission on page load, or before the user has taken any action that establishes why notifications would be useful to them. Ask in context: immediately after the user performs an action whose natural follow-up is a notification (eg. placing an order, starting a long-running job), not as a generic first-visit prompt.
  • MUST NOT show a custom "Would you like to enable notifications?" prompt immediately followed by the browser’s own native permission prompt. This double permission pattern adds a second decision point that only postpones the same friction, and a user who has already said yes once is primed to resent being asked again immediately after.
  • SHOULD explain, in the interface, what the user will receive notifications about and roughly how often, before requesting permission — a user who cannot predict what they are agreeing to is more likely to decline out of caution.
  • Notification timing SHOULD be relevant to the user’s own activity (eg. "the item you were watching is back in stock") rather than to the sender’s schedule (eg. a fixed daily digest sent to every user at the same time). Precision matters as much as timing: a vague or generic notification body trains the user to dismiss the next one unread.
  • MUST give users an easy, discoverable way to adjust or revoke notification preferences after granting permission, without having to navigate the browser’s own site-permissions UI. Losing this control is a common reason users revoke notification access entirely, for every category of notification the GUI sends, rather than for the one that annoyed them.

Browser support

Browser support policy

  • Define browser support as a market-share threshold, not a fixed list of named browsers: support any browser with more than roughly 1% of the application’s own traffic, measured from real analytics data rather than assumed from general industry figures. A threshold defined against the application’s actual audience adapts automatically as browser usage shifts, where a fixed list goes stale the moment it is written.
  • Support the last two major versions of each browser that clears the threshold. Users of an evergreen browser update within weeks of a new release, so supporting further back than two versions accumulates maintenance cost for a shrinking, largely inactive population.
  • MUST NOT test against pre-release beta or canary builds as a baseline for support decisions. Their behavior is unstable and unrepresentative of what the released version will do, and workarounds written against beta behavior are liable to become unnecessary — or wrong — the moment the browser ships.

Feature detection and polyfilling

  • Prefer feature detection over user-agent (browser) detection. Test directly for the capability the code depends on (eg. if ('IntersectionObserver' in window), or @supports (gap: 1rem) in CSS) rather than inferring capability from a parsed navigator.userAgent string. A user-agent string can be spoofed, varies across a browser’s own versions, and says nothing about a capability that was added or removed independently of the version number it reports.
  • Use @supports for CSS feature detection, wrapping any declaration that depends on a feature not yet universally available:
    @supports (container-type: inline-size) {
      .card-container {
        container-type: inline-size;
      }
    }

    See Container queries for a worked example of this pattern applied to container queries specifically.

  • Load polyfills dynamically, only for browsers that fail the relevant feature-detection check, rather than shipping the polyfill unconditionally to every browser. A browser that already supports the feature pays the download and parse cost of a polyfill it will never execute if the polyfill is bundled in unconditionally.
  • Prefer a Baseline widely-available platform feature over a polyfilled one wherever the target browser support allows it — see the "Minimize the JavaScript actually shipped" guidance in Script loading. A polyfill is a temporary bridge to a feature the platform will eventually support natively, not a permanent substitute for it; revisit and remove polyfills as the underlying feature’s baseline support catches up.

References