DEV Community

Alex Cloudstar
Alex Cloudstar

Posted on • Originally published at alexcloudstar.com

Modern CSS in 2026: The JavaScript You Can Finally Delete

I opened an old project last week and found a 90-line JavaScript file whose only job was to add a class to a form's parent element when an input inside it was invalid. A MutationObserver, an event listener, a bit of state, and a cleanup function. All to turn a border red when something upstream changed.

In modern CSS that is one selector. form:has(input:invalid). No JavaScript, no observer, no cleanup, no bug where the listener leaks after the component unmounts.

That moment is the whole story of CSS in 2026. The features that were "coming soon" for years are now shipped, supported, and in many cases at 100 percent across browsers. A surprising amount of the JavaScript we write for UI behavior is now legacy code that exists only because the platform could not do it when we wrote it. This is a tour of the JavaScript you can delete, the modern CSS that replaces it, and the honest support caveats so you do not ship something that breaks in Safari.


:has() Is the Parent Selector We Waited 20 Years For

For the entire history of CSS, selectors only looked downward. You could style a child based on its parent, never the reverse. Every "style the parent based on its children" problem became a JavaScript problem. Toggle a class, observe the DOM, manage the state.

:has() ended that, and as of 2026 it has 100 percent support across all major browsers. It is safe to use in production without a fallback. This is not a "progressive enhancement, test carefully" feature anymore. It just works.

The mental shift is that :has() lets a selector be conditional on what it contains.

/* Card that contains an image looks different from a text-only card */
.card:has(img) {
  grid-template-columns: 120px 1fr;
}

/* Label turns red when its input is invalid */
label:has(+ input:invalid) {
  color: var(--color-danger);
}

/* Form disables its submit area while any field is in error */
form:has(:invalid) button[type='submit'] {
  opacity: 0.5;
  pointer-events: none;
}
Enter fullscreen mode Exit fullscreen mode

Think about how much JavaScript that last one replaces. The old version listens for input events, validates the form state, finds the submit button, and toggles its disabled styling. The CSS version is declarative and has no lifecycle to manage.

:has() also composes with quantity. :has(> :nth-child(5)) lets you style a container differently once it has five or more children, which used to require counting elements in JavaScript. Combined with the new sibling-index() and sibling-count() functions, layouts that adapt to the number of items in them are now a pure CSS concern.

This is the single highest-leverage modern CSS feature to learn first, because it deletes the most JavaScript per line and the support is total.


Container Queries Killed the Breakpoint-Per-Page Model

Media queries ask the wrong question. They ask how big the viewport is. What you usually want to know is how big the space this specific component lives in is. A card in a sidebar and the same card in a full-width hero should lay out differently, and the viewport cannot tell you which one you are looking at.

Container queries fix this, and at roughly 92 percent global support they are a foundational feature in 2026, not an experiment. The shift people are calling "intrinsic design" is real: components style themselves based on their own container, which makes them genuinely portable. Drop the same component into a narrow column or a wide panel and it adapts on its own.

.card-list {
  container-type: inline-size;
  container-name: cards;
}

@container cards (min-width: 400px) {
  .card {
    display: grid;
    grid-template-columns: 1fr 2fr;
  }
}
Enter fullscreen mode Exit fullscreen mode

The practical payoff is that you stop maintaining a giant pile of viewport breakpoints that all have to agree with each other. A component carries its own responsive logic. You move it, the logic moves with it. This is the kind of structural improvement that does not show up in a demo but quietly removes a whole category of "this looks broken in this one layout" bugs.

If you build with a component model already, whether that is React, Svelte, or anything else, container queries are the CSS feature that matches how you already think. I cover the component-model side of that in the Svelte 5 vs React comparison, and container queries are the styling layer that makes truly portable components possible regardless of which framework you picked.


View Transitions Make Page Changes Feel Native

The reason native apps feel smoother than websites is rarely raw speed. It is that native apps animate between states, and websites historically snapped from one hard cut to the next. Bridging that gap meant heavy JavaScript animation libraries, manual coordination of enter and exit states, and a lot of code that broke the moment the DOM structure changed.

The View Transitions API moves that into the platform. Same-document transitions reached Baseline in 2025, so animating between UI states within a single page is safe to ship now.

// Same-document transition: wrap the DOM update
document.startViewTransition(() => {
  updateTheList(); // your normal DOM mutation
});
Enter fullscreen mode Exit fullscreen mode
/* Then describe the animation in CSS */
::view-transition-old(root) {
  animation: fade-out 0.2s ease;
}
::view-transition-new(root) {
  animation: fade-in 0.2s ease;
}
Enter fullscreen mode Exit fullscreen mode

The browser snapshots the before and after states and animates between them for you. You are not manually tracking which elements entered, left, or moved. You name the transition and write the keyframes, and the platform does the choreography.

Here is the honest caveat. Same-document transitions are solid and Baseline. Cross-document transitions, the ones that animate between full page navigations in a multi-page site, are newer and browsers are still landing consistent implementations. They are spectacular when they work and the perfect fit for content sites and anything using an MPA architecture, but treat them as progressive enhancement for now. The page should be completely usable if the transition does not run, and with view transitions that graceful degradation is automatic: no support means an instant cut, which is exactly what you had before.


Anchor Positioning: Tooltips and Popovers Without the Geometry Math

Positioning a tooltip or dropdown next to its trigger is one of those problems that looks trivial and is not. You measure the trigger, measure the viewport, calculate whether the popover fits below or needs to flip above, recalculate on scroll and resize, and handle the edge cases where it collides with the screen edge. Entire libraries exist for nothing but this.

CSS Anchor Positioning lets you describe the relationship declaratively. You say which element is the anchor and which sides connect, and the browser handles the geometry, including flipping the popover when it would overflow.

.trigger {
  anchor-name: --menu-button;
}

.dropdown {
  position: absolute;
  position-anchor: --menu-button;
  /* attach the dropdown's top to the anchor's bottom */
  top: anchor(bottom);
  left: anchor(left);
  /* automatically flip if it would overflow the viewport */
  position-try-fallbacks: flip-block;
}
Enter fullscreen mode Exit fullscreen mode

Pair this with the native popover attribute and you can build accessible menus, tooltips, and dialogs with positioning logic that used to require a dedicated dependency, and now requires none.

The caveat here is the biggest one in this article, so read it carefully. Anchor positioning shipped in Chromium first and browser support is still uneven in 2026. It is not at the safe-everywhere level of :has() or container queries. If you use it, you need a sensible fallback for browsers that do not support it yet, or you scope it to environments where you control the browser. It is the most exciting feature on this list and the one most likely to bite you if you assume universal support. Check the current numbers before you commit, because this one is moving fast.


Scroll-Driven Animations Without a Single Scroll Listener

Scroll listeners are a performance trap. They fire constantly, they run on the main thread, and the naive version causes jank on exactly the low-end devices you most need to support. Doing scroll-linked animation well in JavaScript means throttling, requestAnimationFrame, and intersection observers, and it is still easy to get wrong.

CSS scroll-driven animations move this off the main thread entirely. You tie an animation's progress to scroll position or to an element's visibility, declaratively, and the browser runs it on the compositor.

@keyframes reveal {
  from {
    opacity: 0;
    transform: translateY(2rem);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.section {
  animation: reveal linear both;
  animation-timeline: view();
  animation-range: entry 0% cover 40%;
}
Enter fullscreen mode Exit fullscreen mode

That fades and lifts each section as it scrolls into view, with no JavaScript and no scroll listener. A reading-progress bar at the top of an article, parallax effects, elements that animate as they enter the viewport, all of it becomes a few lines of CSS that the browser runs efficiently.

Support is good in Chromium and improving elsewhere. Because these are animations, they degrade gracefully by nature: a browser that does not support the timeline simply shows the end state, so your content is never hidden behind an animation that did not run. That makes scroll-driven animations safe to add as an enhancement even where support is not universal.


The Quality-of-Life Features That Add Up

Beyond the headline features, 2026 CSS shipped a long list of smaller things that each delete a little friction. Individually they are minor. Together they change how much you reach for JavaScript or preprocessors.

Native nesting. You can nest selectors directly in CSS now, no Sass required. For a lot of projects this removes the build-step preprocessor entirely, which is one less thing in your toolchain. I went down the build-simplification path when I moved my portfolio to Astro, and native CSS nesting is part of why so much tooling that used to feel mandatory is now optional.

.card {
  padding: 1rem;

  & .title {
    font-weight: 600;
  }

  &:hover {
    background: var(--surface-hover);
  }
}
Enter fullscreen mode Exit fullscreen mode

Cascade layers (@layer). Specificity wars are the reason so many codebases drown in !important. Cascade layers let you define explicit priority order between groups of styles, so your base, components, and utilities never fight unpredictably. This is the feature that makes large CSS codebases maintainable instead of a game of specificity whack-a-mole.

@scope. Scoped styles without a build tool or CSS-in-JS runtime. You can limit a block of styles to a subtree of the DOM and even set a lower boundary, which gets you component-style encapsulation natively.

Modern color with oklch(). Perceptually uniform color, which means generating consistent tints and shades for a design system actually works instead of producing muddy mid-tones. Combined with color-mix(), you can build an entire palette from a couple of base colors in pure CSS.

text-wrap: balance and text-wrap: pretty. Headlines that wrap evenly instead of leaving one orphaned word on the last line, and body text that avoids ugly typographic orphans. This used to require a JavaScript library that measured and inserted line breaks. Now it is one declaration.

field-sizing: content. Textareas and inputs that grow to fit their content automatically. The auto-resizing textarea was a rite of passage JavaScript snippet for a decade. It is now one line of CSS.

None of these is a headline on its own. The cumulative effect is that a modern stylesheet does things in 2026 that genuinely required JavaScript or a preprocessor two years ago, and the code that remains is smaller and easier to reason about.


How to Actually Adopt This Without Breaking Things

The temptation after reading a list like this is to rewrite everything. Do not. The right approach is to know which tier each feature is in and treat them accordingly.

Ship today, no fallback needed: :has(), container queries, native nesting, cascade layers, oklch(), text-wrap. These are at or near universal support. Use them like any other CSS.

Ship as progressive enhancement: same-document view transitions, scroll-driven animations, field-sizing. These degrade gracefully on their own. The page works without them and gets nicer with them, so you can add them now and let support fill in.

Check current numbers first, provide a fallback: anchor positioning and cross-document view transitions. These are real and worth using in the right context, but they are not safe-everywhere yet. Scope them to controlled environments or pair them with a sensible default.

The way to make this concrete is to use a feature query. @supports lets you write the modern version and fall back cleanly when it is missing.

.dropdown {
  /* fallback positioning */
  top: 100%;
  left: 0;
}

@supports (anchor-name: --x) {
  .dropdown {
    position-anchor: --menu-button;
    top: anchor(bottom);
  }
}
Enter fullscreen mode Exit fullscreen mode

Two minutes of @supports buys you the new feature where it exists and a working experience everywhere else. That is the entire risk-management strategy.


The Real Shift Is Where Logic Lives

Step back from the individual features and there is a pattern. For 15 years, the answer to "the platform cannot do this" was JavaScript. State that depended on the DOM, layout that depended on context, animation that depended on scroll, positioning that depended on geometry. We pushed all of it into scripts because CSS could not express it. That code accumulated, and it is a large fraction of the JavaScript a typical site ships.

Modern CSS is pulling that logic back into the declarative layer where it belongs. Declarative code is smaller, it has no lifecycle to manage, it does not leak listeners, and it runs in the browser's optimized paths instead of on the main thread. The same way the React Compiler removed the manual memoization we used to hand-write, modern CSS is removing the manual DOM coordination we used to script. The platform got good enough that the workaround became the liability.

This pairs with the broader trend of the web platform absorbing what used to be library territory, the same way the latest JavaScript language features keep replacing utilities we used to install. The lesson is the same in both: before you reach for a dependency or write a clever script, check whether the platform already does it. In 2026, for a surprising amount of UI behavior, the answer is yes.

The next time you are about to add a scroll listener, a MutationObserver, a positioning library, or a class-toggling effect, pause and ask whether CSS can do it now. More often than you would expect, it can, and the version that lives in your stylesheet will outlast the version that lived in your bundle. That old 90-line file I deleted is not coming back. Most of yours can go the same way.

Top comments (0)