Home›Journal›This post

CSS :has() vs JavaScript: Who Owns UI State?

Classify derived, persistent, async, and authorization state; test specificity, fallbacks, and stale mirrors; then give every interface fact one owner.

JP
JP Casabianca
AI Engineer and Product Designer · full-stack delivery · Bogotá

CSS :has() can derive a style from present structure, but it cannot own persistence, asynchronous lifecycle, authorization, or business truth. This CSS :has() vs JavaScript guide applies that boundary to concrete fixtures, fallbacks, stale mirrors, and an exportable state-ownership matrix.

CSS :has() vs JavaScript begins with state ownership

Start by naming the fact, not the technology. A checked input, focused descendant, or currently invalid native control already exists in the present DOM and can support derived styling. A checkout step restored after reload, a request's pending lifecycle, or authorization from a server needs a durable owner outside selectors.

That is the CSS :has() vs JavaScript decision: can the interface derive a visual response from structure that is true now, or must application logic remember, transition, validate, or secure the fact? This derived UI state is where the parent selector nickname can be useful, as long as CSS only observes relationships. It cannot own persistence, asynchronous ordering, authorization, or business truth.

The distinction prevents mirrored state. The foundations for interaction states help name visible hover, focus, selected, disabled, and error treatments before assigning ownership. If JavaScript toggles an is-checked class beside a checkbox, two sources can drift. When the native control is the truth, style the relationship directly. When a reducer owns a request, expose one deliberate status attribute or rendered message from that reducer; do not infer success because a child node happens to exist.

The lab contains four bounded fixtures: local derived presentation, persisted or URL state, async pending/success/error, and business authorization. Each fixture names its owner, fallback, semantic control, and test oracle. CSS :has() vs JavaScript is not scored as a feature contest. The useful outcome is one owner per fact and no selector that pretends presentation created meaning.

Derived relationships and owned state take different railsA DOM facts rail flows into selectors and style, while events, reducers, storage, and server policy flow through JavaScript before a deliberate UI projection.DERIVED NOWOWNED OVER TIMEDOM fact:has()visual styleevent / serverreducer / policyUI projectionPRODUCTUIcheckedpendingauthorized?one fact · one owner · one tested projection
Derived relationships and owned state take different rails. The boundary is temporal and semantic: selectors derive what is present; state machines own what must endure or transition.
  • Present DOM relationships may flow through :has() directly into local styling.
  • Persistence, async transitions, and business policy flow through JavaScript or the server.
  • Application-owned state exposes one deliberate UI projection for CSS.
  • Both rails reach the same product interface without duplicating ownership.

Reading rule: labels, values, shapes, order, and line styles carry the conclusion; color is supplementary.

Understand relative-selector scope before styling

The relational pseudo-class matches an element when at least one relative selector is satisfied against that element. The Selectors Level 4 definition also defines important constraints: :has() cannot be nested, pseudo-elements are generally excluded unless separately allowed, and specificity comes from the most specific complex selector in its arguments.

Scope is therefore part of correctness. A component rule such as .field:has(> input:invalid) expresses an immediate relationship. Widening it to .field:has(input:invalid) may begin reacting to nested controls owned by another subcomponent. The widened-descendant mutant is killed by a fixture containing an intentionally invalid nested helper form.

Use child combinators, component roots, and explicit named parts when structure allows it. Keep selectors readable enough that a reviewer can say which descendant fact triggers which visual response. The goal is not to demonstrate every possible relational selector; it is to keep a local relationship local.

For isolation techniques after ownership is decided, CSS scope for component boundaries addresses selector reach. CSS :has() vs JavaScript comes first because scoping a duplicated or incorrectly owned fact only makes the wrong model tidier. Ownership decides the truth source; scope limits where its visual consequence can travel.

Derive checked, focus, and validation presentation

Native controls already expose useful current facts. A disclosure row can emphasize itself when its checkbox is checked. A field group can lift its border when focus is within. Validation hints can react when a required input is invalid and has been meaningfully engaged, while the control remains an actual labeled input.

The HTML standard defines selector-checked behavior for native controls in its checked-state matching section. The selector observes that control state; it does not create the checked behavior, accessible name, keyboard interaction, or form semantics. Those remain HTML responsibilities.

In the CSS :has() vs JavaScript lab, the derived fixture compares actual computed style with an independent relationship oracle reading checked, validity, and focus relationships. The oracle does not parse the stylesheet or inspect the expected class. A matrix of user actions toggles the checkbox, changes the value, moves focus, and resets the form. Every snapshot must agree.

This is where :has() earns its keep: fewer mirrored classes, less event wiring, and a stylesheet that responds to a semantic fact already present. It is not evidence that CSS is always faster, more accessible, or universally appropriate. WebKit's implementation examples demonstrate practical form and parent-selection patterns, while product semantics still require an explicit local decision.

Control specificity and containment

Relational power can turn a small selector into a broad dependency. A rule containing an ID selector inside :has() can gain ID-level specificity. A page-level :has() can react to distant descendants. Neither outcome is automatically wrong, but both should be visible in review and covered by a fixture.

Prefer low-specificity wrappers such as :where() around styling hooks when the cascade should stay easy to override. Keep the relational part narrow and let cascade layers express system order. The uncontrolled-specificity mutant injects an ID into the argument and must lose a frozen override test. The nested-:has() mutant is rejected as invalid syntax rather than quietly rewritten.

Containment is architectural as well as syntactic. A card may derive “contains a selected option” from its direct controls, but a dashboard should not derive global workflow state from any selected control anywhere below it. The wider the selector, the less obvious the owner and the larger the invalidation surface.

CSS :has() vs JavaScript does not produce a universal performance verdict. This is a correctness lab in the executing browser, not a selector benchmark. Use cascade layers in design systems to make override order explicit, then profile a production-shaped DOM if selector cost matters. Feature support and semantic fitness are separate questions.

Assign each fact an owner, fallback, and test

A semantic matrix compares local visual derivation, persisted steps, async lifecycle, and authorization without declaring a technology winner.

Assign each fact an owner, fallback, and test reading guideRead each row from declared input and threshold through terminal result and independent agreement.
FactOwnerReasonFallbackTest
Checked descendantDOM + CSSPresent relationshipControl still worksComputed style vs relationship oracle
Restored stepJavaScriptOutlives snapshotVisible step textReducer restoration
Request statusJavaScriptOrdered lifecycleStatus messageGeneration reducer
AuthorizationServer policyBusiness truthDenied explanationServer enforcement
Similar visual states can have different owners because lifetime, event order, and security obligations differ.
  1. Native controls remain semantic controls in every lane.
  2. Persistence occurs only where the product contract promises it.
  3. Async terminal states are one-way and generation-aware.
  4. Visibility never grants authorization.

Reading rule: the row labels, numeric values, and pass words carry the conclusion; color is supplementary.

Keep persisted and URL state in JavaScript

A multi-step flow must survive more than the current descendant tree. If the active step belongs in the URL, router or application logic owns parsing, validation, navigation, and restoration. If a preference persists, storage policy and migration rules belong to code. A selector cannot distinguish intentional restoration from a stale node left in the DOM.

The persisted fixture uses a small reducer with explicit NEXT, BACK, RESTORE, and RESET events. It renders the current step to a data attribute and visible text. CSS consumes that projection, but JavaScript owns the transition and the receipt records whether restoration was promised. The unsupported-:has() path remains fully operable because buttons, text, and reducer do not depend on relational styling.

CSS :has() vs JavaScript is easiest when the line is drawn before implementation. Ask whether the state must outlive a DOM snapshot, be serialized, participate in browser history, or support undo. Any yes answer points to an application owner even if :has() can create a tempting visual shortcut in the current markup.

Avoid double ownership. Do not keep both currentStep=2 and a hand-maintained .step-two class. Render one projection from the reducer and test that projection. The stale-mirror mutant skips one class update after rapid navigation; the invariant catches divergence between reducer state, visible status, and styling hook.

Keep async lifecycle and authorization out of selectors

Pending, success, error, and cancellation form an event-ordered lifecycle. A spinner child may be present while a stale request finishes; an error message may remain during retry for context. Inferring state from whichever child exists reverses ownership and makes races look like styling problems.

The async fixture uses a reducer keyed by request generation. START creates a new pending generation; only that generation may commit SUCCESS or ERROR; a terminal state cannot transition again. Rapid sequences include late results from older generations, reset, and retry. The async-from-child mutant commits based on markup presence and fails the reducer oracle.

Authorization is stricter. A hidden button is not a denied capability, and a visible button is not permission. Server policy owns the business fact. Application logic may render an explanation and disable an affordance, while the server still enforces the operation. The authorization-as-style mutant grants based on a descendant badge and must be rejected.

CSS :has() vs JavaScript therefore has a hard truth boundary. Styling can communicate application state, but it cannot manufacture it. For coordinated UI motion after data transitions, React data-state animation shows how to animate a declared state without turning animation into the source. The same principle applies here: projections may be visual; ownership remains explicit.

A mirrored class drifts from the checked factA four-step timeline shows a checkbox becoming checked while an is-checked class misses the update, then compares direct derivation and a single reducer projection.1uncheckedclass off2checkedclass on3uncheckedclass ON4resetclass offDRIFT: two truths disagreeDERIVE LOCALLY.row:has(input:checked)PROJECT OWNED STATEreducer → data-state
A mirrored class drifts from the checked fact. Deleting stale mirrors requires choosing the real owner, not merely choosing a shorter syntax.
  1. Step 1: DOM and mirrored class both say unchecked.
  2. Step 2: both update to checked.
  3. Step 3: DOM resets, but the class remains on; the sources drift.
  4. For a present relationship, derive from the control.
  5. For durable state, render one projection from the reducer.

Reading rule: labels, values, shapes, order, and line styles carry the conclusion; color is supplementary.

Design a readable fallback and test the matrix

Feature-detect with CSS.supports("selector(:has(*))"). If support is absent, keep semantic controls, labels, visible state text, reducer behavior, and server enforcement intact. The enhanced outline or parent emphasis may disappear, but understanding and operation must remain. Feature detection is not an accessibility certificate.

The lab runs no more than 24 fixed event sequences across supported and simulated-unsupported lanes. Derived cases compare computed style to a fresh DOM-relationship oracle. Owned cases compare reducer output to a separately written transition oracle. Reset, rapid event ordering, restoration, nested descendants, and stale generations are included.

Seven invariants summarize the contract: controls remain semantic controls; style creates neither accessible names nor authorization; derived lanes match the current snapshot; mirrored classes never drift; persistence occurs only where promised; async terminal transitions are one-way; and the unsupported lane stays understandable and operable. JSON and CSV exports preserve every event, snapshot, owner, oracle result, feature state, and canonical hash.

The feature-support-mistaken-for-semantics mutant marks the component valid merely because :has() parses. It fails because support says nothing about names, keyboard behavior, error explanation, or policy. CSS :has() vs JavaScript is tested as ownership plus progressive enhancement, not as a compatibility checkbox.

Audit one component and delete stale mirrors

Inventory a real component as a state ledger. For each fact, write its source, lifetime, transitions, serialization need, security relevance, semantic representation, style projection, fallback, and test. “Selected because this checkbox is checked now” is derived. “Selected because the account saved this preference” is owned. Similar pixels can hide different obligations.

Then search for mirrors: classes toggled beside native state, data attributes updated by multiple listeners, child nodes treated as request status, and visibility treated as permission. Delete a mirror only after a test proves the remaining source drives every required state. Where code owns the fact, keep one explicit projection for CSS rather than asking selectors to reverse-engineer the reducer.

The stale-mirror timeline is the most practical CSS :has() vs JavaScript test. Trigger a rapid sequence, reset, restore, and unsupported mode. At every point compare DOM fact, reducer state, visible explanation, computed style, and oracle. A mismatch names both the broken surface and the rightful owner.

This lab is not a cross-browser performance benchmark or accessibility certification. It proves a bounded ownership matrix in the executing browser. The release decision is smaller and more useful: use :has() for local present relationships, JavaScript for durable or event-driven truth, server policy for authorization, and one documented projection between owners. That removes clever code where CSS can derive honestly and prevents clever CSS from impersonating a state machine.

Runnable local artifact — This is a state-ownership decision lab in the executing browser, not a cross-browser performance benchmark, accessibility certification, or authorization mechanism.

Plain text1 line
Run at most 24 fixed sequences across derived, persisted, async, and policy fixtures; compare native output with independent oracles, inspect fallback behavior, kill mutants, and validate exports.