Accessibility and compatibilityBrowser and device support

100%

Browser and device support.

"Which browsers do we support?" is a product question before it is a technical one. Railmoor answers it with a short written policy: three tiers drawn from its own traffic, plus 212 station kiosks stuck on a two-year-old engine. The code then keeps that promise the same way everywhere: start from HTML that works, ask the browser what it can do, and add the better version only where the answer is yes.

Intermediate27 minUpdated 2 Oct 2026

The idea.

Compatibility is decided twice. First as a policy, a written list of who must get what; then in code, which has to keep that promise on engines and screens you will never hold in your hand.

Railmoor sells regional rail tickets from railmoor.example. Most of its visitors arrive on a phone or a laptop running a browser that updated itself last month. A smaller group arrives in person: at 212 stations there is a ticket kiosk, a portrait touchscreen with no mouse and no typing keyboard, only a tactile arrow keypad beside the screen. The kiosk vendor froze its software on a Chromium 128 build from August 2024 and will not ship another one before the contract renews. Those kiosks produce 3.4% of Railmoor's sessions and 14% of its tickets.

Without a policy, every compatibility bug turns into an argument. Does the seat map have to animate on the kiosk? Must the site work in a browser from 2019? Is a layout glitch on a tablet in landscape a release blocker? A support policy settles these questions once. It names tiers: the browsers where everything must work and is tested on every change, the ones where the core task must work but extras may be missing, and the ones Railmoor promises nothing about. The tiers come from Railmoor's own traffic and from what each group is worth to the business, not from a list copied from another company.

The engineering half is how one codebase honours those tiers without forking. Railmoor starts every page from HTML that works on its own, asks the browser whether it has a capability before using it, and adds the richer version only where the answer is yes. The kiosk then gets the same site as everyone else, minus the few pieces its engine cannot do, plus a fallback wherever the missing piece matters.

Where Railmoor's sessions come from

Sessions (‰)
  1. Chrome / Edge, width 582, Full tier, value 58.2%, last two major versions, desktop and Android
  2. Safari, width 274, Full tier, value 27.4%, last two major versions, macOS and iOS
  3. Firefox, width 39, Full tier, value 3.9%, last two majors plus ESR
  4. Samsung, width 16, Full tier, value 1.6%, Samsung Internet, current version
  5. Kiosks, width 34, Full tier, pinned engine, value 3.4%, Chromium 128, touch plus arrow keypad; 14% of tickets
  6. Older, width 46, Functional: core task only, value 4.6%, older versions of the browsers above
  7. width 9, Unsupported, value 0.9%, everything else
  • full tier: 94.5% of sessions, from 0 to 945
  • 5.5%, from 945 to 1,000
  • Full tier
  • Full tier, pinned engine
  • Functional: core task only
  • Unsupported
Chrome / Edge
last two major versions, desktop and Android
Safari
last two major versions, macOS and iOS
Firefox
last two majors plus ESR
Samsung
Samsung Internet, current version
Kiosks
Chromium 128, touch plus arrow keypad; 14% of tickets
Older
older versions of the browsers above
rest
everything else
Thirty days of sessions, counted from server logs and grouped by engine and version rather than by brand, in parts per thousand. The kiosks are a sliver of traffic, yet they sit in the full tier because they sell one ticket in seven.

Railmoor's support matrix

ClientShareTierWhat must workHow it is tested
Chrome and Edge, last 2 majors (desktop, Android)58.2%FullEvery journey with every enhancementAutomated Chromium run on every pull request
Safari, last 2 majors (macOS, iOS)27.4%FullEvery journey with every enhancementAutomated WebKit run on every pull request; a real iPhone weekly
Firefox, last 2 majors and ESR3.9%FullEvery journey with every enhancementAutomated Firefox run on every pull request
Samsung Internet, current1.6%FullEvery journey with every enhancementReal Android phone before each release
Station kiosks: Chromium 128, portrait touchscreen, no mouse3.4%Full (pinned engine)Every journey; features newer than August 2024 through fallbacksA pinned Chromium 128 binary in CI, and one real kiosk in the office
Older versions of the browsers above4.6%FunctionalSearch, times and prices, the server-rendered checkout. No animation, plain controlsMonthly spot check
Anything else0.9%UnsupportedNothing promised. The plain HTML form usually works anyway, so no blocking wallNot tested
Was this section helpful?

How it works.

Six parts: tiers drawn from data, a shared vocabulary for when a feature is safe, detecting capabilities at run time, layering the page so the core works first, build targets that decide what gets transpiled or polyfilled, and input modes, the part people forget.

1

Tiers from traffic

Start from the server's access logs, not only from the analytics script. A browser the site breaks in may never run that script, so analytics under-counts exactly the users you are failing. Group requests by engine and major version, and put devices with fixed software (kiosks, TVs, in-app web views, company-managed laptops) in rows of their own, since they will not update on your schedule.

Then draw the lines. The full tier is what you test on every change: current evergreen browsers plus any group whose business value earns a place. The functional tier is where the core task (search, choose, pay) must work but extras may be missing. The unsupported tier gets no testing and no promises, which is different from being blocked: a page built in layers usually still works there, and a "browser not supported" wall turns an accidental success into a certain failure. Write the policy down, give it an owner, and revisit it every quarter, because the browsers move even when your code does not.

2

Baseline: a shared word for "safe to use"

Checking every feature against every browser table is slow, and teams used to argue about what "supported" meant. Baseline is the web platform's shared answer. It watches a core set of browsers: Chrome on desktop and Android, Edge, Firefox on desktop and Android, and Safari on macOS and iOS. A feature is newly available once the current version of every one of them supports it, and it is labelled with that year (Baseline 2025). Thirty months later it becomes widely available, meaning most sites can use it without a fallback. Before either milestone it has limited availability.

Baseline describes browsers, not your users. Railmoor's kiosks do not care what the latest Safari supports; they care what Chromium 128 supports. So Baseline is the default line for the full tier, and any row with a pinned engine gets checked by hand against the features you plan to use.

Four features Railmoor wants, on the Baseline clock

<dialog>
  1. width 2, Limited availability
  2. newly, Mar 2022, width 30, Newly available
  3. widely, Sep 2024, width 52, Widely available
  • Oct 2026, edge at 57
:has()
  1. limited, width 23, Limited availability
  2. newly, Dec 2023, width 30, Newly available
  3. widely, Jun 2026, width 31, Widely available
  • Oct 2026, edge at 57
Intl.DurationFormat
  1. limited, width 38, Limited availability
  2. newly, Mar 2025, width 30, Newly available
  3. widely, Sep 2027, width 16, Widely available
  • Oct 2026, edge at 57
Invoker commands
  1. limited, width 47, Limited availability
  2. newly, Dec 2025, width 30, Newly available
  3. widely, Jun 2028, width 7, Widely available
  • Oct 2026, edge at 57
  • Limited availability
  • Newly available
  • Widely available
Months from January 2022 to December 2028. Limited availability until the last core browser ships, newly available from then, widely available 30 months later. The marker is October 2026: :has() only just crossed into widely available, and invoker commands will not get there until mid-2028.
3

Detect, then enhance

From a need to working code, without asking which browser this is

Core that works on its ownHTML + CSS
then ask the browser, never the user-agent string
Detect the capability'x' in obj · CSS.supports() · @supports · matchMedia()
yes: use the platform; no: go on
Use the native featurenothing to download
missing, but the gap matters
Fallback or polyfillloaded only where missing
missing, and it is only polish
Skip the enhancementthe core still works
Every enhancement goes through the same four steps. The first step is what makes the rest safe: if detection says no and no fallback exists, the user still has a working page.
Rules of thumb
Detect the exact thing you use Check for showModal before calling showModal, not for some other API that happened to arrive in the same release.
Detect once, early Run the checks when the module starts and keep the answers, so the code that renders never branches on the browser again.
Cheapest fallback first Often the best fallback is doing nothing: an unstyled native control is still a working control.

Three kinds of detection Railmoor uses

// Booking-summary dialog: the button has command="show-modal" commandfor="summary".
const button = document.querySelector('#review');
const dialog = document.querySelector('#summary');

if (!('command' in HTMLButtonElement.prototype)) {
  // Chromium 128 on the kiosks ignores command/commandfor, so wire it up by hand.
  button.addEventListener('click', () => dialog.showModal());
}

// Journey length "1 hr, 13 min": load the polyfill only where it is missing.
if (!('DurationFormat' in Intl)) {
  await import('./polyfills/duration-format.js');
}
/* The railcard picker: styled where the engine allows it, native elsewhere. */
@supports (appearance: base-select) {
  .railcard,
  .railcard::picker(select) {
    appearance: base-select;
  }
}

/* Seat map: highlight a carriage row that holds a selected seat. */
@supports selector(:has(a)) {
  .carriage:has(.seat[aria-selected="true"]) {
    outline: 2px solid var(--accent);
  }
}
const canStyleSelect = CSS.supports('appearance', 'base-select');
const touchFirst = matchMedia('(pointer: coarse)').matches;
4

Enhance upwards, not downwards

Progressive enhancement builds upwards. The first layer is HTML that completes the task by itself: Railmoor's timetable search is a form that sends a GET to /timetable and gets a page of results back. CSS makes it look right, and a script, if it runs and if the browser passes its checks, turns the form into an in-place search with a station combobox and a calendar. Each layer is optional to the one below it.

Graceful degradation starts from the finished experience and patches it downwards: build for the newest browsers, then find what breaks elsewhere and add workarounds. It can reach a similar result, but the core task only works in older browsers if someone remembered to test there. Both have a place. Enhancement suits pages whose core is a form, a list or a document; degradation is honest for things with no simpler form, like a live seat map drawn on a canvas, where the fallback is a plain list of free seats rather than a lesser seat map.

How one Railmoor page arrives at its final form

How one Railmoor page arrives at its final form. 5 states, 6 transitions. The table below lists them.
How one Railmoor page arrives at its final form5 states, 6 transitions. The table below lists them.

stylesheet loads

module script runs

script blocked or fails

all checks pass / use native APIs
a check fails [fallback exists] / load fallback

a check fails [no fallback] / leave the HTML alone

Server HTML arrives

Styled core page

Script checks features

Enhanced page

Core page stays

3 steps.

Every path ends in a page that works; the only question is how rich it is. The failure path (script blocked or crashed) lands on the core page, not a blank one.

Transitions of How one Railmoor page arrives at its final form
From → ToEventGuardActionActor
Server HTML arrives → Styled core pagestylesheet loads
Styled core page → Script checks featuresmodule script runs
Styled core page → Core page staysscript blocked or failsnetwork or content blocker
Script checks features → Enhanced pageall checks passuse native APIs
Script checks features → Enhanced pagea check failsfallback existsload fallback
Script checks features → Core page staysa check failsno fallbackleave the HTML alone
Server HTML arrivesstart
The form and results already work
Styled core page
CSS applied; feature queries decide the extras
Enhanced pageend
In-place search and dialogs
Core page staysend
Full page loads per search
5

Build targets decide what gets rewritten

Detection handles features at run time; the build handles them before shipping. One file, .browserslistrc, lists the browsers the full tier covers, and every tool reads it: Babel's preset-env rewrites newer syntax and, with useBuiltIns set to usage, adds core-js imports only in files that use an API the targets lack; Autoprefixer adds the CSS prefixes those targets still need; eslint-plugin-compat flags calls to APIs they do not have. Change the policy and every tool follows.

Three different gaps need three different answers. Syntax (optional chaining, class fields) can be transpiled. A missing API (Intl.DurationFormat) can often be polyfilled with script. Platform behaviour (a styleable select, the top layer, a new media feature) usually cannot be recreated at all, so it needs a fallback or nothing. The old trick of shipping two bundles, a modern one with type="module" and a legacy one with nomodule, belongs to the era when a large share of traffic could not run modules; every browser in a current full tier can, so one modern bundle plus targeted fallbacks is simpler.

Railmoor's build targets

# Full tier: what Baseline calls widely available today…
baseline widely available

# …plus the station kiosks, written out so the line above can move on without them.
chrome 128
{
  "presets": [
    ["@babel/preset-env", { "useBuiltIns": "usage", "corejs": "3.40" }]
  ]
}

Which gap, which fix

Kind of gapRailmoor exampleFixCost
New syntaxa?.b, class fieldsTranspiled at build time from the browserslist targetsSlightly larger code, paid by every browser in the targets
Missing APIIntl.DurationFormatPolyfill, loaded only when a check finds it missingBytes and a request, only on the browsers that need it
Missing declarative wiringcommand / commandforA few lines of script that do the same job on clickCode to keep until the fallback can be deleted
Platform behaviourappearance: base-selectNo polyfill is possible; gate the styling with @supports and keep the native controlNothing, except a plainer look
6

Input modes, not device names

Screen size says nothing reliable about input. A laptop can have a touchscreen, a tablet can have a keyboard and trackpad, and the kiosk is a large screen whose only pointer is a finger. CSS media features describe the input directly. pointer tells you how precise the primary pointing device is (fine for a mouse, coarse for a finger, none), and hover tells you whether it can hover conveniently. any-pointer and any-hover ask the same about every input attached, which is how you notice the stylus on a touch laptop. In script, pointer events report pointerType as mouse, pen or touch per event, so one handler can serve all three.

The rule that follows: hover may add a hint, but it must never be the only way to reach an action. Anything revealed on hover also needs to appear on keyboard focus, and on a device with hover: none it should simply be visible. Tooltips shown on hover or focus have their own WCAG 2.2 rule (1.4.13): they must be dismissible, stay up while the pointer moves onto them, and stay visible until the pointer or focus leaves, the user dismisses them, or they stop being relevant. Target size and zoom are covered with the other perceivable-UI requirements in Announcing changes and respecting preferences.

What each query says about the Railmoor clients

QueryAsks aboutLaptop with mousePhoneKiosk
pointerThe primary input's precisionfinecoarsecoarse
hoverWhether the primary input can hoverhovernonenone
any-pointer: fineAny attached input is precisematchesno matchno match
any-hover: hoverAny attached input can hovermatchesno matchno match

Actions that do not depend on hover

/* Row actions (Seat map, Save) are visible by default… */
.departure .actions { opacity: 1; }

/* …and tucked away only where hovering is easy, never hidden from focus. */
@media (hover: hover) and (pointer: fine) {
  .departure:not(:hover, :focus-within) .actions { opacity: 0; }
}

/* Bigger hit areas for fingers. */
@media (pointer: coarse) {
  .departure .actions button { min-block-size: 44px; }
}
Was this section helpful?

In practice.

Railmoor's booking journey on three clients: a laptop on a current browser, a phone, and a station kiosk frozen on Chromium 128. Same code, same URLs, different layers switched on.

What the kiosk's Chromium 128 can and cannot do

Feature Railmoor usesBaselineIn Chromium 128?What Railmoor does
<dialog> with showModal() for the booking summaryWidely available (Baseline since March 2022)yesUse it as is
:has() to highlight a carriage with a chosen seatWidely available since June 2026yesUse it as is
popover for the fare-rules hintBaseline 2025 (January 2025)yes, since Chromium 114Use it as is: Chromium had it long before the other engines
command / commandfor to open the summaryBaseline 2025 (December 2025)no, Chrome 135A click listener calls showModal() when the check fails
Intl.DurationFormat for "1 hr, 13 min"Baseline 2025 (March 2025)no, Chrome 129A polyfill, loaded only when the check fails
appearance: base-select for the railcard pickerChromium first, from Chrome 135noNothing: @supports skips the styling and the native select remains

The audit held two surprises. The features the team feared most (the modal dialog and :has()) were already in the kiosk's engine; Baseline dates track the slowest core browser, and Chromium had often shipped years earlier. The features that were actually missing were small and recent, and each had a cheap answer: a few lines of script, one lazily loaded polyfill, and one styling rule that simply does not apply.

The timetable search on three clients

On the laptop every check passes. From and To are comboboxes, Date opens a calendar, Railcard is a styled select, and results replace the form's lower half without a page load.

Fully enhanced. Laptop · current Chrome: Form “Find a train”. Fields: From = “Ashcombe Junction”; To = “Kestrel Bay”; Date = “Wed 14 Oct”; Railcard = “None” (focused). Button: “Find trains”. Links: Plan with more options. Note on railcard: styled with appearance: base-select Note on from: station combobox (script) Station kiosk · Chromium 128, touch: Form “Find a train”. Fields: From = “Ashcombe Junction”; To = “Kestrel Bay”; Date = “Wed 14 Oct”; Railcard = “None”. Button: “Find trains”. Links: Plan with more options. Phone · script did not load: Form “Find a train”. Fields: From = “Ashcombe Junction”; To = “Kestrel Bay”; Date = “Wed 14 Oct”; Railcard = “None”. Button: “Find trains”. Links: Plan with more options.

  1. Enhanced only where @supports allows
  2. A real submit button, not a click handler on a div

One URL, two ways to fetch it

One URL, two ways to fetch it, as an ordered list of steps:
One URL, two ways to fetch it10 steps between Traveller, Page in the browser, railmoor.example. The steps are listed as text after the diagram.railmoor.examplePage in the browserTravellerWithout script: the form does all the workWith script: the same request, interceptedsubmit handler: preventDefault(), show a busy stateswap the results in; history.pushState(/timetable?…) so Back and shared links worktaps Find trains1GET /timetable?from=ashcombe-jn&to=kestrel-bay&date=2026-10-142200 full HTML page with 8 trains3taps Find trains4fetch(): GET /timetable?…&partial=results5200 results fragment6
  1. Note over Page in the browser and railmoor.example: Without script: the form does all the work
  2. Traveller → Page in the browser: taps Find trains
  3. Page in the browser → railmoor.example: GET /timetable?from=ashcombe-jn&to=kestrel-bay&date=2026-10-14
  4. railmoor.example → Page in the browser (reply): 200 full HTML page with 8 trains
  5. Note over Page in the browser and railmoor.example: With script: the same request, intercepted
  6. Traveller → Page in the browser: taps Find trains
  7. Note over Page in the browser: submit handler: preventDefault(), show a busy state
  8. Page in the browser → railmoor.example: fetch(): GET /timetable?…&partial=results
  9. railmoor.example → Page in the browser (reply): 200 results fragment
  10. Note over Page in the browser: swap the results in; history.pushState(/timetable?…) so Back and shared links work

Row actions on a mouse and on a finger

The pointer rests on the 08:40 row, so its actions fade in. With a mouse this is a tidy list; the same actions also appear when the row gets keyboard focus.

Hover on the laptop. Laptop · mouse: List “Ashcombe Junction to Kestrel Bay”: 3 items. Sort: Departs. Count: 8 trains. 1. 08:12 → 09:25 (Direct · 1 hr, 13 min) · Platform 3 2. 08:40 → 10:02 (1 change · 1 hr, 22 min) · Platform 1 Seat map, Save. Hovered: overlay actions shown. 3. 09:05 → 10:18 (Direct · 1 hr, 13 min) · Platform 3 Kiosk · touch: List “Ashcombe Junction to Kestrel Bay”: 3 items. Sort: Departs. Count: 8 trains. 1. 08:12 → 09:25 (Direct · 1 hr, 13 min) · Platform 3 2. 08:40 → 10:02 (1 change · 1 hr, 22 min) · Platform 1 3. 09:05 → 10:18 (Direct · 1 hr, 13 min) · Platform 3

  1. Actions: visible unless hover is easy
01
How the kiosks get features their engine lacks
Chosen:One codebase, per-feature detection and fallbacks
  • Pro:Kiosks get every fix and feature the day everyone else does
  • Pro:Each fallback is small and can be deleted when the kiosks update
  • Pro:Other old browsers benefit from the same checks for free
Downside we accept:
  • Con:A pinned Chromium 128 must stay in the CI matrix
  • Con:Fallback code needs an owner and a removal date
Ruled out:A separate kiosk build with its own lower target

Two builds to test and deploy, which drift apart; Sniffing which build to serve brings back user-agent checks

Ruled out:Ask the vendor to update the kiosk browser

Out of Railmoor's control until the contract renews; Kiosks would fail in the meantime

Ruled out:Use only features Chromium 128 has

Every user pays for the oldest client; the web moves on without you

Saying it in an interview

Q1
ClarifyWhat should you ask about compatibility at the start of a frontend design round?
Which browsers and devices must be fully supported, whether any clients have fixed software (kiosks, TVs, embedded web views, managed laptops), which input modes matter (touch, mouse, keyboard, stylus), and whether the core task must work without JavaScript. One minute of questions, then state the tiers you will assume.
Q2
ArchitectureWhere does compatibility show up in the architecture you draw?
In three places: server-rendered HTML as the base layer (the form or list that works alone), a small capability module that runs checks once at start-up and exposes flags to the components, and a build step whose targets come from one browserslist file. Name the fallback for anything newer than your floor.
Q3
Deep diveThe interviewer says 20% of users are on a two-year-old in-car browser. What changes?
That client becomes its own row in the matrix, probably full tier given its share. Audit the features you use against its engine version, not against Baseline; transpile syntax for it, polyfill missing APIs only where the check fails, and keep platform-only features as enhancements. Add it to CI as a pinned binary.
Q4
PitfallWhat answer loses points?
"We'll detect Safari and serve a different bundle", or hover-only actions on a design that must also run on tablets. Both show the design is keyed to device names instead of capabilities.

Where the system flows use this

Airbnb map search
List and map highlight each other on hover. On touch there is no hover, so the sync moves to tap and to the list row in view; the hover link is an extra, not the only link.
Netflix resume and previews
A muted preview starts after the pointer rests on a title. Under hover: none the preview needs a different trigger or none at all, and the details must be reachable by tap and by keyboard focus.
Figma select and transform
Pointer events with pointerType tell mouse, pen and touch apart in one handler: pen pressure, two-finger pan on touch, precise handles for a mouse.
Airbnb date-range picker
A custom range calendar is an enhancement over two native date inputs; the dates and their formatting (Intl) must work even when the calendar script does not.
Was this section helpful?

Trade-offs.

Reach costs bytes, test time and code you must later delete. The choices below are where Railmoor paid those costs and where it declined to.

01
How the page decides what the browser can do
Chosen:Feature detection at run time
  • Pro:Answers the real question (does this API exist here?)
  • Pro:Right for browsers you have never heard of, including new ones
  • Pro:Keeps working when a browser fixes or adds a feature
Downside we accept:
  • Con:A check can pass while the feature is buggy; those cases need a test, not a guess
  • Con:Some things (a buggy layout, a rendering quirk) cannot be detected
Ruled out:User-agent sniffing

Browsers include each other's names in their UA strings; Version rules go stale with every release; Browsers not in the rules get the worst branch even when capable

Ruled out:User-Agent Client Hints

Chromium-based browsers only; Still describes the brand, not the capability; fine for analytics, not for deciding features

Main bundle size by build target

Main bundle size by build targetAdding the kiosks' Chromium 128 to the targets costs nothing, while shipping the polyfill to everyone adds 9 KB and dropping to ES5 adds 54 KB.050100150200Baseline widely available+ chrome 128+ polyfill for everyoneES5 with full core-js142 KB142 KB151 KB196 KBBuild targetMain bundle (KB)Main bundle size by build targetAdding the kiosks' Chromium 128 to the targets costs nothing, while shipping the polyfill to everyone adds 9 KB and dropping to ES5 adds 54 KB.050100150200Baseline widely a…Baseline widely available+ chrome 128+ polyfill for ev…+ polyfill for everyoneES5 with full cor…ES5 with full core-js142 KB142 KB151 KB196 KBBuild targetMain bundle (KB)
Railmoor's main JavaScript bundle, gzip, illustrative measurements. The kiosk costs no syntax transforms because Chromium 128 already runs modern syntax; its only extra is the 9 KB polyfill, which Railmoor loads on demand instead of bundling.
Data
Build targetKB gzip
Baseline widely available142
+ chrome 128142
+ polyfill for everyone151
ES5 with full core-js196

What the test matrix costs

Assumptions
Engine targets
4Chromium, Firefox, WebKit, pinned Chromium 128
Viewports
3phone, laptop, portrait kiosk
Input modes
2mouse + keyboard, touch
Critical journeys
8search, seat choice, pay, change booking…
Minutes per journey run
1.5
Parallel CI workers
8
Working
  1. Every combination4 × 3 × 2 × 8192 runsfrom Engine targets, Viewports, Input modes and Critical journeys
  2. Serial time for all of them192 × 1.5288 minfrom Every combination and Minutes per journey run
  3. Combinations that exist (phone + touch and laptop + mouse on 3 engines, plus the kiosk)3 × 2 + 17 combinationsfrom Engine targets, Viewports and Input modes
  4. Runs per pull request7 × 856 runsfrom Combinations that exist (phone + touch and laptop + mouse on 3 engines, plus the kiosk) and Critical journeys
  5. Wall-clock on 8 workers56 × 1.5 / 810.5 minfrom Runs per pull request, Minutes per journey run and Parallel CI workers
What it means
  • Testing the product of every dimension wastes most of the effort on combinations nobody uses; test the pairings your traffic shows.
  • Automated engines are not real devices. Keep a weekly pass on a real iPhone and on the office kiosk for the things emulation misses: the on-screen keyboard, touch latency, a glare-bright screen.

How compatibility breaks after launch

FailureImpactDetectionMitigationMeanwhile
A check passes but the feature misbehavesThe native path runs and a flow breaks in one engine versionError reports grouped by engine and version; the pinned engine in CITest the behaviour, not only its presence; add a targeted check for the known bugOther engines unaffected
Hover-only affordance shipsTouch and keyboard users cannot reach an actionA touch run and a keyboard-only run of every journey in CIActions visible by default, hidden only under (hover: hover) and (pointer: fine)Mouse users never notice, which is why it ships
The Baseline query moves past a pinned clientNew syntax reaches the kiosks and the script fails to parseThe pinned Chromium 128 CI run goes redName pinned engines explicitly in .browserslistrcThe core HTML page still works while it is fixed
Fallbacks never get deletedDead code and polyfills ship for yearsA quarterly policy review against the traffic tableGive every fallback an owner and a removal condition (kiosks updated, share under 0.5%)Nothing breaks; the bundle just grows

"Works" is not "looks the same"

Must hold in every supported tier
The traveller can search, choose and pay
Every action is reachable by touch, mouse and keyboard
Text is readable and nothing overlaps
Errors are shown and can be fixed
May differ between browsers
A styled select versus the native onebase-select only where supported
In-place results versus a page loadthe no-script path
Animations and transitionsalso off under reduced motion
Pixel-identical spacing and fontsnever a goal across engines
This topic
Support tiers from traffic and value
Baseline, feature detection and build targets
Progressive enhancement and input modes
Elsewhere
How fast pages must be on weak devices and networksTarget devices and networks
Roles, names and the accessibility treeSemantics and the accessibility tree
Target size, reflow, reduced motion and contrastAnnouncing changes and respecting preferences
Server rendering, hydration and islandsRendering strategies
Right-to-left layoutRight-to-left and text layout
Was this section helpful?
Related
Semantics and the accessibility tree
Read next