Developer Tools

CSS :has() in 2026 for Static HTML, Safari Sign-Off, and Cloud Mac QA

MacHTML Lab2026.04.02 14 min read

Static marketing pages and documentation sites still ship mountains of hand-authored HTML. Until recently, styling a parent based on the state of a child meant extra JavaScript or awkward wrapper classes. The :has() relational pseudo-class flips that: a section can change its border when any checkbox inside is checked, or a form row can highlight when an input is invalid—without a bundler and often without a single line of JS. In 2026, Safari 15.4+ coverage is wide enough for many teams to adopt :has() for visual affordances, provided you validate in real WebKit and respect selector complexity. This guide covers practical patterns, a decision matrix against JS and container queries, and how to fold checks into a Safari QA workflow on a rented Mac mini.

What :has() fixes on static pages

Classic CSS could style descendants based on ancestors (.theme-dark .card) but not the reverse. Product teams hacked around the limitation with duplicate classes toggled by React, or by inflating HTML with data-* mirrors of child state. On zero-JS static sites—legal disclaimers, API reference microsites, conference microsites—that friction blocked subtle UX polish. :has() expresses intent directly: “this card is in an error state because it contains .error-text.” Accessibility teams still expect visible labels and ARIA where applicable; :has() handles decoration, not semantics replacement.

Because :has() is declarative, designers can prototype in CodePen and ship the same selectors through Eleventy or Hugo without a hydration step. The tradeoff is mental: relational selectors are powerful and easy to over-nest. Establish a house rule such as “no more than two combinators after :has() inside marketing templates” so future maintainers can grep confidently.

Syntax patterns you can paste today

Start with self-contained components. A field group that should glow when any control is focused:

.field-group:has(:focus-visible) {
  box-shadow: 0 0 0 3px rgba(0, 113, 227, 0.35);
}

A table row that flags missing translations when a cell contains a placeholder token:

tr:has(td[data-missing="true"]) {
  background: rgba(255, 59, 48, 0.08);
}

Combine with :not() for exclusionary states—e.g., disable a submit affordance until required inputs are non-empty—while remembering that empty detection in pure CSS is limited compared to the Constraint Validation API. When business rules exceed what attribute selectors can express, pair a tiny script with class hooks instead of forcing a ten-clause :has() chain.

Safari support timeline and testing notes

Concrete compatibility facts for your matrix:

  • Safari 15.4 (March 2022) shipped :has() on macOS 12.3 and iOS/iPadOS 15.4 alongside Chromium 105-class support in other engines.
  • Enterprise policies that freeze macOS on Monterey pre-12.3 still exist; if analytics show even 3–4% of revenue from those builds, ship a fallback border color without :has().
  • WebKit fixes occasionally land in Safari Technology Preview first; compare stable vs STP when you see a bug report about nth-child inside :has(), then retest each monthly static deploy.

Document the minimum Safari in your README next to Node and pnpm versions so contractors do not “fix” layouts by stripping :has() during crunch week. Revisit that line every six months as enterprise fleets refresh macOS hardware.

Decision matrix: :has(), JS, or @container

NeedPreferWhy
Highlight parent when child invalid:has(:invalid)Zero JS, works offline in static HTML.
Reorder grid tracks when sidebar width changes@containerWidth-driven layout belongs to container queries, not :has().
Post form JSON with server errorsJavaScriptNetwork and ARIA live updates exceed CSS scope.
Show icon when any checkbox ticked:has(:checked)Declarative and accessible if labels exist.
Throttle analytics on scroll depthJavaScriptCSS cannot safely fire beacons.

When both :has() and @container apply, split responsibilities: use containers for macro layout and :has() for micro state inside the component tree. Mixing both on the same element rarely helps readability.

Performance and selector hygiene

Browsers must re-evaluate relational selectors when descendants change. On a long single-page site with 4,000+ DOM nodes, a careless body:has(.modal[open]) { overflow: hidden; } pattern is usually fine, but chaining multiple :has() selectors on high-frequency animations is not. WebKit’s style invalidation is efficient for localized subtrees; keep :has() roots close to cards and form sections. If Performance recordings show purple style recalc bars wider than 2–3ms on mid-tier laptops during hover storms, refactor to a single class on a wrapper toggled by one delegated listener.

Linters such as Stylelint with selector-max-specificity rules help prevent “selector soup.” Add a CI step on your cloud Mac runner so macOS-only regressions surface before merge; renting Apple Silicon hardware for CI avoids buying a fifth MacBook for a two-week campaign.

Security note: :has() cannot read arbitrary text content for matching—only structure and pseudo-classes—so it is not a data exfiltration vector by itself. Still, avoid logging full selector strings with user-generated class names into third-party analytics.

When migrating legacy BEM modifiers such as .card--error, schedule a two-sprint overlap: keep the class for analytics hooks while :has() drives visuals, then delete the modifier once event tracking moves to data attributes. That pattern prevents dashboard charts from breaking while designers delete redundant markup. Capture before/after bundle sizes—static sites often drop 2–4KB gzip once redundant toggle scripts disappear.

QA checklist on a cloud Mac mini

Rent a macOS host with Safari stable, load your static build over file:// or a local static server, and walk through three flows: keyboard-only form navigation, forced invalid states, and a zoomed 200% view to catch clipping. Capture HAR-less screen recordings at 1280×720 for bug bashes. If your team lacks local Macs, SSH plus occasional VNC for visual checks mirrors what we describe for other Safari articles—same playbook, different feature flag.

Apple Silicon Mac mini nodes stay quiet under parallel lint + style recalc profiling, which matters when you batch twenty HTML templates nightly. Cloud access lets contractors share one machine instead of shipping laptops across borders.

Add one automated smoke step: load the page, toggle each sample input through valid and invalid states, and screenshot the computed styles panel for :has() roots. Storing those PNGs next to your release tag costs almost no disk yet saves roughly 25–35 minutes of regression debate when WebKit differs from Chromium.

FAQ

Which Safari version ships :has()?

Safari 15.4 on macOS 12.3 and iOS 15.4 (March 2022) added support. Teams that still support Safari 14 need a non-:has fallback.

Can :has() replace JavaScript for forms?

For pure styling cues (borders, icons) often yes; for validation messages, ARIA live regions, or server round-trips you still need JS or HTML semantics beyond CSS.

Does :has() hurt performance?

Deeply nested :has() chains on huge pages can increase style recalculation cost. Keep selectors local, avoid coupling :has() to every hover on a 5k-node marketing page, and measure in Web Inspector.

Used with discipline, :has() removes boilerplate from static HTML while keeping bundles slim. Pair it with real Safari sessions—not only Chromium—to catch WebKit-specific invalidation quirks. A Mac mini on Apple Silicon delivers native WebKit, low idle power, and silent operation for long manual QA afternoons. MacHTML rents physical Mac mini machines with SSH/VNC so you can stand up a WebKit lab for a release window, then scale down when the campaign ends—no CAPEX for hardware that sits idle between launches.

Need Safari hardware for :has() QA?

Rent an Apple Silicon Mac mini, run Web Inspector on real WebKit, and keep shipping static HTML from your current editor.

WebKit QA on cloud Mac
From $16.9/Day