How do you ensure consistent rendering and layout across browsers and devices?
Use a normalize/reset stylesheet, modern layout primitives (flex/grid), feature detection (@supports), autoprefixer + browserslist, polyfills only where needed, and test on real Safari/iOS plus BrowserStack matrix.
Cross-browser bugs are usually one of three things: CSS vendor differences, JS API gaps, or Safari/iOS being Safari/iOS. The senior answer is process-driven (browserslist + autoprefixer + CI testing) layered with knowledge of common foot-guns.
1. Define your support matrix. browserslist in package.json drives autoprefixer, Babel, and modern bundlers. A typical SaaS:
"browserslist": [
">0.5%",
"last 2 versions",
"not dead",
"not IE 11"
]Everything downstream — vendor prefixes, transpilation level, polyfill set — derives from this list.
2. Reset / normalize. Browsers ship different default styles. Pick one of: (a) Eric Meyer reset (zeroes everything), (b) normalize.css (preserves useful defaults, fixes inconsistencies), (c) Tailwind preflight (modern reset). Never ship without one.
3. Use modern layout, not floats/hacks. Flexbox (95%+ support including Safari) and Grid (95%+) cover almost every layout. Hand-rolled positioning is where browsers diverge most.
4. Vendor prefixes via autoprefixer. Plug into PostCSS; it reads browserslist and adds -webkit-, -moz- etc. only where needed. Don't write prefixes by hand.
5. Feature detection over user-agent sniffing.
@supports (backdrop-filter: blur(10px)) {
.modal-bg { backdrop-filter: blur(10px); }
}if ("IntersectionObserver" in window) { /* use it */ } else { /* fallback */ }UA strings lie (Edge claims to be Chrome and Safari), break with new browser releases, and miss the actual question.
6. Polyfills only where needed. Don't ship 150KB of core-js to modern browsers. Use:
@babel/preset-envwithuseBuiltIns: "usage"— adds polyfills only for syntax/APIs the code uses.<script type="module">for modern browsers,nomodulefallback for old — the module/nomodule pattern.- Native
Promise,fetch,URLare everywhere now; don't polyfill in 2024+.
7. The Safari / iOS tax. Safari often lags 6–18 months on web platform features and has its own bugs:
- 100vh on iOS includes the address bar — use
100dvh(dynamic viewport). - Date input styling is locked.
position: stickyglitches inside scroll containers.- Touch event quirks; momentum scroll on
overflow: scrollcontainers. <details>/<dialog>shipped late.
Test on a real iOS device or Xcode Simulator. Chrome DevTools "iPhone" mode does not catch these.
8. Responsive design. Mobile-first media queries, clamp() for fluid type, container queries (@container) for component-level responsiveness (Safari 16+). Test at break points: 360, 768, 1024, 1440.
9. Testing.
- Unit/integration: jsdom (limited; no layout).
- Visual regression: Chromatic, Percy, or Playwright screenshots — catch layout drift across browsers.
- BrowserStack / Sauce Labs / Playwright cloud for the full matrix in CI.
- Real-device testing for iOS Safari at minimum.
10. Source map your bug reports. Sentry / Datadog with browser metadata helps you see "this layout shift only happens on Safari 16.0 on iOS 16" — instead of guessing.
Common bug patterns to know.
- Different default
<button>styles → reset. - Flexbox
gapworked in Chrome before Safari (Safari 14.1+). - iOS rubber-band scroll exposes a "blank" backdrop — set
overscroll-behavior: contain. - Rendering subpixel differences in SVG between Firefox and Chrome — usually fine, sometimes hairline gaps;
shape-rendering: crispEdgeshelps. - Rounded corners on
<select>only on macOS Chrome — workaround with custom dropdown.
Code
Follow-up questions
- •Why feature-detect instead of UA-sniff?
- •What's the module/nomodule pattern?
- •How do you handle iOS Safari's 100vh issue?
- •How do you set up visual regression testing?
Common mistakes
- •Hard-coding vendor prefixes — autoprefixer does it correctly.
- •Sniffing user agent — breaks on new browser releases.
- •Polyfilling everything indiscriminately — bloats modern bundles.
- •Skipping real iOS testing because 'Chrome DevTools iPhone mode is fine.'
Performance considerations
- •Module/nomodule sends modern code to modern browsers — half the bundle size.
- •Avoid CSS hacks that force expensive recalcs (e.g., generic * selectors).
Edge cases
- •Safari rounds subpixel layout differently — pixel-perfect designs need rem-based sizing.
- •Flex `gap` not in older Safari — fall back to margin until you can drop support.
- •Container queries are Safari 16+ — use @supports to feature-test before relying.
Real-world examples
- •Stripe Elements ships polyfills targeted at their browserslist; Vercel uses Playwright cloud for cross-browser regression.