qa-detect-css-compat
Scans CSS stylesheets for properties with known cross-browser compatibility gaps: missing vendor prefixes, Safari-specific traps, Firefox layout quirks, and properties without fallbacks
SKILL.mdskills/qa-detect-css-compat/SKILL.md
GitHubWhat it checks
Detects CSS written assuming a single browser engine. Catches patterns where Chrome renders correctly but Firefox/Safari silently fail or produce different output — without needing to actually run in those browsers.
missingWebkitBackdropFilter—backdrop-filterused without-webkit-backdrop-filter(Safari < 15.4 requires prefix)missingWebkitTextSizeAdjust—-webkit-text-size-adjustabsent from html/body (iOS Safari auto-inflates text on orientation change)overscrollBehaviorNoFallback—overscroll-behaviorused without acknowledgement (not supported Safari < 16 — bouncing/pulldown can't be controlled)cssVariableNoFallback—var(--x)used in a critical property (color, font-size, display) with no fallback value — if the variable is undefined the property is invalid in all browsersgapOnFlexNoFallback—gapused on a flex container without amarginfallback (Safari < 14.1 ignored gap on flex, not grid)aspectRatioNoFallback—aspect-ratioused without apadding-bottomfallback wrapper (Safari < 15 / older Android WebView)missingTouchAction— interactive non-button elements ([draggable], custom sliders, carousels) have notouch-actionproperty — causes 300ms tap delay or conflicts with scroll in some browsers
Probe (browser_evaluate)
() => {
const out = [];
const issues = new Map(); // type → first occurrence description
// Scan all accessible stylesheets
for (const sheet of document.styleSheets) {
let rules;
try { rules = [...sheet.cssRules]; } catch (_) { continue; } // CORS-blocked = skip
for (const rule of rules) {
if (!rule.style) continue;
const css = rule.cssText || '';
const style = rule.style;
// 1. backdrop-filter without -webkit-backdrop-filter
if (/\bbackdrop-filter\s*:/.test(css) && !/-webkit-backdrop-filter\s*:/.test(css)) {
if (!issues.has('missingWebkitBackdropFilter'))
issues.set('missingWebkitBackdropFilter', `backdrop-filter used in "${rule.selectorText || 'unknown'}" without -webkit-backdrop-filter — Safari < 15.4 will ignore it (modal/glassmorphism blur won't render)`);
}
// 2. gap on flex container without margin fallback
if (/\bgap\s*:/.test(css) || style.gap) {
const sel = rule.selectorText || '';
// Check if this selector's elements are flex containers
let el;
try { el = document.querySelector(sel); } catch (_) {}
if (el) {
const display = getComputedStyle(el).display;
if ((display === 'flex' || display === 'inline-flex') && !issues.has('gapOnFlexNoFallback')) {
// Check if any margin fallback exists in same rule
if (!/\bmargin\b/.test(css)) {
issues.set('gapOnFlexNoFallback', `gap used on flex container "${sel.slice(0,60)}" without margin fallback — Safari < 14.1 ignored flex gap; older devices will have no spacing between flex items`);
}
}
}
}
// 3. CSS custom property without fallback in critical properties
const criticalProps = ['color', 'font-size', 'display', 'background-color', 'border'];
for (const prop of criticalProps) {
const val = style.getPropertyValue(prop) || '';
if (/var\(--[^,)]+\)/.test(val) && !/var\(--[^,)]+,\s*[^)]+\)/.test(val)) {
if (!issues.has('cssVariableNoFallback'))
issues.set('cssVariableNoFallback', `var() used for "${prop}" in "${(rule.selectorText || '').slice(0,60)}" with no fallback value — if the custom property is undefined, the element becomes invisible/unstyled in all browsers`);
break;
}
}
// 4. aspect-ratio without padding-bottom wrapper
if (/\baspect-ratio\s*:/.test(css) && !issues.has('aspectRatioNoFallback')) {
issues.set('aspectRatioNoFallback', `aspect-ratio property used in "${(rule.selectorText || '').slice(0,60)}" — Safari < 15 and older Android WebView don't support it. Add a padding-bottom fallback wrapper for older device support.`);
}
}
}
// 5. -webkit-text-size-adjust on html/body (cannot reliably read from computed style)
const htmlStyle = document.documentElement.getAttribute('style') || '';
const bodyStyle = document.body.getAttribute('style') || '';
// Check via stylesheet for html/body rules
let hasTextSizeAdjust = false;
let hasWebkitTextSizeAdjust = false;
for (const sheet of document.styleSheets) {
let rules;
try { rules = [...sheet.cssRules]; } catch (_) { continue; }
for (const rule of rules) {
if (!rule.selectorText || !/^html|^body/.test(rule.selectorText.trim())) continue;
const css = rule.cssText || '';
if (/text-size-adjust/.test(css)) hasTextSizeAdjust = true;
if (/-webkit-text-size-adjust/.test(css)) hasWebkitTextSizeAdjust = true;
}
}
if (hasTextSizeAdjust && !hasWebkitTextSizeAdjust) {
issues.set('missingWebkitTextSizeAdjust', 'text-size-adjust set on html/body without -webkit-text-size-adjust — iOS Safari will auto-inflate text on orientation change, causing layout shifts');
}
// 6. overscroll-behavior usage (Safari < 16 unsupported)
for (const sheet of document.styleSheets) {
let rules;
try { rules = [...sheet.cssRules]; } catch (_) { continue; }
for (const rule of rules) {
if (!rule.cssText) continue;
if (/overscroll-behavior/.test(rule.cssText) && !issues.has('overscrollBehaviorNoFallback')) {
issues.set('overscrollBehaviorNoFallback', `overscroll-behavior used in "${(rule.selectorText||'').slice(0,60)}" — Safari < 16 does not support this property. Pull-to-refresh and scroll chaining cannot be controlled on older iPhones.`);
}
}
}
// 7. touch-action missing on draggable/interactive custom elements
const draggables = document.querySelectorAll('[draggable="true"], [class*="slider"], [class*="carousel"], [class*="swipe"]');
for (const el of draggables) {
if (out.length + issues.size >= 15) break;
const s = getComputedStyle(el);
const ta = s.touchAction || s.getPropertyValue('touch-action') || '';
if (!ta || ta === 'auto') {
const sel = `${el.tagName.toLowerCase()}${el.id ? '#' + el.id : (el.className ? '.' + String(el.className).trim().split(/\s+/)[0] : '')}`.slice(0,80);
if (!issues.has('missingTouchAction'))
issues.set('missingTouchAction', `Draggable/interactive element "${sel}" has no touch-action property — causes 300ms tap delay in some browsers and conflicts with native scroll on iOS`);
}
}
// Convert map to findings (one finding per issue type — stylesheet issues have no bbox)
for (const [issueType, description] of issues) {
out.push({ issueType, severity: 'low', selector: 'css', description, bbox: { x: 0, y: 0, w: 0, h: 0 } });
}
return out;
}
Issues
| issueType | severity | description |
|---|---|---|
| missingWebkitBackdropFilter | low | "backdrop-filter used without -webkit-backdrop-filter — Safari < 15.4 won't render blur/glass effects" |
| missingWebkitTextSizeAdjust | low | "text-size-adjust on html/body without -webkit- prefix — iOS Safari auto-inflates text on orientation change" |
| overscrollBehaviorNoFallback | low | "overscroll-behavior used — Safari < 16 ignores it; pull-to-refresh/scroll chaining uncontrollable on older iPhones" |
| cssVariableNoFallback | low | "var(--x) used for critical property without fallback — element breaks silently if variable undefined" |
| gapOnFlexNoFallback | low | "gap on flex container without margin fallback — Safari < 14.1 had no flex gap support" |
| aspectRatioNoFallback | low | "aspect-ratio used without padding-bottom fallback — Safari < 15 / older Android WebView unsupported" |
| missingTouchAction | low | "Draggable element has no touch-action — 300ms tap delay or scroll conflict on mobile browsers" |
Note: All findings are low severity — these are cross-browser compatibility risks, not definite failures. They indicate that Firefox/Safari users may experience degraded rendering even though Chrome renders correctly. Verify each finding against your browser support matrix before filing a ticket.