Replacing complex JavaScript logic with concise, modern CSS features has become one of the most satisfying code refactoring trends in contemporary web development. Dropping dozens of lines of imperative script in favor of a declarative container query or a :has() pseudo-class feels like an instant win for clean code. Pull request diffs shrink, line counts drop, and code reviews move quickly. However, as web developer Gabriel Shoyombo explores in a detailed architectural analysis, swapping JavaScript for CSS can simplify isolated code fragments while simultaneously making the overarching front-end system significantly harder to reason about, maintain, and debug.
Building on his previous work regarding the expanding intelligence of CSS, Shoyombo addresses critical structural questions facing modern engineering teams: What actually happens to application stability when logic is divided between CSS and JavaScript? And how do developers efficiently debug complex conditional styles when the underlying triggers are obscured across declarative stylesheets and script files?
The fundamental challenge stems from a failure to account for system interactions. Modern CSS capabilities do not operate in a vacuum; they integrate directly into an ecosystem composed of existing JavaScript runtime logic, layout rendering rules, the CSS Cascade, and browser rendering engines. While replacing script with style can achieve localized simplicity, it often introduces unintended system complexity if the broader architectural connections are overlooked.
Local Simplicity Versus System Complexity
To illustrate the relationship between new CSS specifications and existing codebases, Shoyombo draws an analogy to a tenant moving into a shared apartment. Even a quiet and tidy resident must share a kitchen and common areas with the current occupants. In the context of front-end engineering, modern CSS features become "roommates" with legacy JavaScript, established component layout models, cascade precedence rules, and the browser’s underlying layout engine. The stability of the application depends far less on the elegance of the individual CSS property and far more on how harmoniously these interconnected parts co-exist.
A standard code review or pull request diff typically measures local simplicity—highlighting removed lines of script and short additions to a stylesheet. What it rarely reveals is system complexity: how those declarative additions affect render invalidation, layout containment, selector scope, and execution boundaries elsewhere in the application architecture.
This debate over front-end architecture reached a broader audience following an essay by developer Igor Roztropiński titled "Modern Frontend Complexity: essential or accidental?" Roztropiński questioned whether web developers could rethink application architecture from scratch using modern browser evolution to achieve genuine simplicity.
The resulting industry discussion highlighted sharp divisions among engineers. Countering the narrative that modern browser standards automatically simplify software, one developer, posting under the pseudonym Kwantuum, noted that many minimalist advocates do not actually accomplish tasks with less code, but rather accomplish fewer requirements. Conversely, advocates for native web capabilities, including developer kayinfire, pointed out that front-end frameworks frequently introduce far more accidental complexity than they resolve, arguing that massive improvements in native CSS capabilities are frequently underutilized by teams over-reliant on JavaScript ecosystems.
Shoyombo locates the primary architectural trap directly in the space between these perspectives. Native features reduce complexity only when they completely replace existing functionality without destabilizing the surrounding component ecosystem. When partial migrations leave behavior split across paradigms, subtle systemic bugs emerge.
Scenario 1: The Problem of Dual Ownership
The first major failure mode during modern CSS migrations occurs when legacy JavaScript logic is only partially removed, leaving a single UI behavior managed by two distinct paradigms.
Consider a responsive UI card component designed to switch from a single-column structure to a two-column grid once sufficient inline space is available. Historically, developers implemented this using a JavaScript ResizeObserver to monitor the element’s bounding rect and toggle a CSS class such as .card--wide. Under this legacy model, both layout rules and typography scaling key off that single DOM class.
With modern CSS Container Queries, developers can establish an inline-size container directly in CSS and apply layout changes via @container rules without firing a single line of JavaScript:
.card
container-type: inline-size;
@container (width > 480px)
.card__body
display: grid;
grid-template-columns: 160px 1fr;
While this refactoring appears straightforward, real-world codebases rarely undergo instantaneous, full-sweep migrations. A developer might add the @container declaration and remove the grid-related utility class, yet overlook a legacy script registered in a separate component module or fail to delete auxiliary selectors like .card--wide .card__title.
If both the legacy script and the new container query target the same 480-pixel threshold, the layout will function correctly under initial testing. However, if a design update subsequently shifts the container query threshold to 600 pixels while leaving the legacy JavaScript or class-based typography styles untouched, the component breaks. Between 480 pixels and 600 pixels, the component enters an unprogrammed intermediate state—displaying a single-column body alongside a wide-layout title header.

Because responsibility for the component’s state is split across script execution and CSS evaluation, identifying the root cause requires engineers to audit both systems simultaneously. To prevent dual ownership, teams migrating to native CSS logic must fully remove obsolete observers, state classes, and legacy selectors across the entire code tree.
Scenario 2: Unintended Layout Side Effects and Containment
The second architectural challenge involves structural side effects introduced by new CSS properties. When developers declare properties like container-type: inline-size, they intend to enable size queries for descendant elements. However, the browser interprets this directive as a request to alter the element’s fundamental layout behavior.
According to technical specifications outlined by the Mozilla Developer Network (MDN), setting a container-type other than normal establishes a new Block Formatting Context (BFC) while applying style and inline-size containment to the container. Establishing a new BFC forces the element to internalize margin calculations, clear internal floats, and isolate its layout bounds from surrounding elements.
This single CSS declaration systematically alters several layout mechanisms:
- Margin Collapsing Prevention: In standard block layouts, top and bottom margins between adjacent elements collapse into a single space. Once a element establishes a BFC via
container-type, vertical margins no longer collapse across its boundary. A heading’s top margin that previously merged with an external paragraph’s bottom margin becomes trapped inside the container, expanding the overall visual height of the component. - Float Interaction Changes: Standard block elements wrap around preceding floated elements, allowing child text to flow dynamically. Establishing a BFC forces the entire container to align adjacent to floats rather than wrapping around them, abruptly altering component alignment and available inline width.
- Size Containment and Element Collapse: Under inline-size containment, an element is strictly forbidden from deriving its width from its child contents. If an element lacks an explicit width or parent-derived inline layout constraint—such as a component rendered inside flex containers with content-based sizing, inline-block elements, auto grid tracks, or
fit-contentboxes—applying size containment causes the element to instantly collapse to a width of zero pixels, effectively causing the UI component to vanish from the viewport.
These side effects demonstrate how a localized attempt to add responsiveness can unknowingly disrupt layout mechanics, component dimensions, and spacing systems established elsewhere in the application layout.
Scenario 3: Specificity Surprises and Cascade Misalignments
The third failure mode emerges from the CSS Cascade and selector mechanics. Modern features like relational pseudo-classes (:has()) and native CSS nesting enable incredibly expressive selectors, but they can inadvertently expand selector reach and alter specificity calculations.
For example, a developer aiming to strip paragraph margins inside card components that contain a hero image might write:
.card:has(img) p
margin: 0;
If that card component houses nested structures—such as a user comment widget containing an author avatar image—the selector overreaches. Because the descendant selector p targets all matching paragraphs inside the card, paragraph elements within the comment widget lose their spacing as well. Furthermore, if an avatar image is rendered dynamically upon user interaction, the layout shifts unexpectedly at runtime.
Constraining selector scope using direct child combinators (e.g., .card:has(> img) > p) prevents selectors from unintentionally targeting deeply nested subcomponents.
A similar specificity trap exists within native CSS nesting. As specified in browser implementations, the parent reference operator (&) inherits the highest specificity across an entire selector list, behaving identically to the :is() pseudo-class.
Consider the following nested structure:
.card, #featured
& a
color: blue;
.button
color: red;
Even if an HTML element uses a simple class configuration like <a class="button"> inside a standard .card, the resulting link displays as blue rather than red. Because the selector list includes the high-specificity #featured ID selector, the nested link selector inherits an elevated specificity weight of 1-0-1, silently overriding standalone class rules (0-1-0) defined elsewhere.
To mitigate this specificity inflation, developers can wrap multi-selector lists inside the :where() pseudo-class (e.g., :where(.card, #featured)), which forcefully resets the specificity weight of the enclosed list to zero (0-0-0).

Additionally, engineering teams often turn to Cascade Layers (@layer) to organize modern CSS architectures. However, as MDN cascade specifications dictate, unlayered styles automatically take precedence over layered styles regardless of specificity, unless the !important flag is invoked. If a development team places modern component styles inside @layer components while legacy stylesheets remain unlayered, the unlayered legacy rules will consistently override the modern styles, rendering specificity calculations ineffective unless the entire stylesheet codebase is fully migrated into layers.
This dynamic mirrors historical issues observed in CSS-in-JS architectures. As developers in front-end communities have highlighted, refactoring component import orders in JavaScript frequently broke runtime CSS output due to unexpected changes in stylesheet output order. Whether using CSS-in-JS or native modern selectors, failing to govern the order of appearance and specificity leads to persistent runtime styling bugs.
Scenario 4: Performance Realities and Rendering Engine Costs
A widespread assumption among front-end engineers is that delegating state handling or layout responsiveness to native CSS guarantees superior runtime performance compared to JavaScript. However, shifting logic to CSS merely changes which browser subsystem performs the work; it does not inherently reduce the volume of work required by the rendering engine.
The performance characteristics of complex selectors like :has() have historically drawn caution from browser vendors. As W3C CSS Working Group discussions reveal, browser implementers designed :has() with specific execution limits to avoid severe layout performance footguns.
To evaluate the actual performance footprint of native CSS logic versus script-driven class mutations, benchmark tests were conducted in Chrome 153 on a Mac desktop environment. The benchmark evaluated a structured DOM tree containing 5,000 list rows, toggling an .is-selected state class 300 consecutive times and triggering synchronous style and layout updates on each iteration:
- Baseline Script Manipulation: A pure JavaScript event handler toggling an active class on a single toolbar element completed each layout pass in under 0.1ms, recalculating styles for exactly 1 element.
- Global Relational Rule: Adding a broad
:has()selector bound to the document root (body:has(.row.is-selected) .toolbar) increased style calculation time to approximately 1.4ms per toggle, still successfully limiting style invalidation to 1 element. - Isolated Subtree Rule: A targeted structural rule (
.list:has(.cell > b) .row), operating alongside JavaScript toolbar handling, ran in under 0.1ms, invalidating styles for 1 element. - Combined Rule Invalidation (The Performance Interaction Bug): When the global root
:has()rule and the targeted list:has()rule were included in the exact same active stylesheet simultaneously, toggling a single class on one row caused the browser to invalidate and recalculate styles for all 5,001 elements on every single frame. Execution time surged to approximately 7.0ms per toggle. - Comparative Script Iteration: For comparison, executing a brute-force JavaScript loop that manually updated class names across all 5,000 rows required 6.0ms per toggle, invalidating 5,001 elements.
When the test environment was expanded to 20,000 rows, the unexpected interaction between the two CSS rules forced style invalidation across 20,001 elements, taking 31ms per toggle—exceeding the standard 16.7ms budget of a 60Hz display frame and dropping visible frames. This specific behavior, where an unrelated global :has() rule forces sweeping style recalculations across static subtrees, has been documented and submitted to the Chromium issue tracker under Issue 573208178.
These benchmarks highlight a crucial architectural lesson: two seemingly performant, isolated CSS selectors can interact within the browser’s engine to produce exponential invalidation costs.
A similar rule applies to CSS transitions and animations. As performance guidelines on web.dev and MDN specify, animating properties such as width, height, left, or margin forces the engine to execute costly layout calculations on every frame. Conversely, animating transform or opacity skips layout and paint operations, executing entirely on the compositor thread. Translating an animation from JavaScript to CSS yields no performance benefit if the underlying property being animated triggers layout recalculations.
Evaluating Migrations in Complex Codebases
The primary disconnect between modern CSS tutorials and production engineering lies in the distinction between greenfield development and legacy migration. In a brand-new project, modern CSS specifications function predictably. In an established enterprise application, new CSS rules must coexist with legacy scripts, third-party libraries, complex layout trees, and historic cascade decisions.
Before replacing existing JavaScript state mechanisms with modern CSS features, engineering teams should evaluate their pull requests against six core criteria:
- Behavioral Ownership: Has the legacy JavaScript state observer, event listener, and corresponding state class been entirely eliminated across all modules?
- Layout Context: Does the target element rely on content-based inline sizing, float wrapping, or collapsing margins that will be altered by establishing a new Block Formatting Context?
- Container Boundaries: Is the query container explicitly sized by a parent layout, ensuring it cannot collapse to zero dimensions under dynamic rendering conditions?
- Selector Reach: Are relational selectors like
:has()tightly scoped using direct child combinators to prevent accidental style leaks into nested child components? - Cascade Integration: Does the specificity and layer placement (
@layer) of the new selector function correctly alongside legacy unlayered stylesheets? - Invalidation Profiling: Has the refactored layout been profiled in browser developer tools under scaled DOM loads to confirm that style invalidation scope remains contained?
Modern CSS specifications provide unprecedented declarative power, allowing developers to eliminate vast amounts of historical script boilerplate. However, achieving a resilient front-end architecture requires evaluating new layout capabilities not merely by the lines of code they delete, but by how predictably they integrate into the execution ecosystem of the wider application.