Home / Articles

Shipping Modern CSS Safely: Anchors, Grid Lanes, Scope and light-dark()

Learn how anchor positioning, popover, grid-lanes masonry, view transitions, light-dark() and @scope replace JavaScript libraries, and how to adopt each with fallbacks.

Publisher: REACTAPP.TOP7 min read1605 words

This article is published in English.

Sources: CSS Can Already Do This. Developers Are Still Scared to Ship It.

Many front-end bundles still carry a library whose only job is working out where a tooltip should sit, such as Floating UI or its predecessor Popper, plus another that packs cards into a Pinterest-style grid. Browsers can now do both natively, along with several other jobs that used to need JavaScript, yet few teams have removed those dependencies. This guide covers five CSS features that can replace common library code, explains how each one works, and shows how to adopt them without betting on uneven browser support.

Wanted and avoided at the same time

Developer surveys capture the tension well. In the State of CSS results cited for 2026, anchor positioning was voted the new feature developers were most excited about, taking over from :has(), which is now so widely used that it no longer feels new. The same survey also asked which features people were deliberately avoiding because they did not yet trust browser support, and anchor positioning topped that list as well. Closing that gap is mostly about adopting features behind feature queries. Survey figures and support status change quickly, so treat them as a snapshot and check current compatibility data before relying on any feature.

Anchor positioning instead of a tooltip library

The JavaScript approach attaches a floating element to its trigger by measuring both and recalculating the position on every scroll and resize. Anchor positioning moves that relationship into CSS.

Below, the trigger declares a name and the menu positions itself against it. The last declaration replaces a library's collision detection:

.trigger {
  anchor-name: --menu-trigger;
}
.menu {
  position: fixed;
  position-anchor: --menu-trigger;
  top: anchor(bottom);
  left: anchor(center);
  position-try-fallbacks: flip-block, flip-inline;
}

Each piece has a specific role:

  • anchor-name gives an element a dashed identifier that others can position against.
  • position-anchor tells a positioned element which named anchor to follow.
  • anchor() reads a coordinate, such as the anchor's bottom edge or horizontal centre, directly from that element.
  • position-try-fallbacks lists alternatives the browser tries when the element would overflow the viewport; flip-block and flip-inline mirror it vertically or horizontally.

Pairing it with the popover attribute

The popover attribute, also shipped and also widely ignored, covers a dropdown's behaviour: a button with popovertarget toggles the element with the matching id, no script needed:

<button popovertarget="menu">Options</button>
<div id="menu" popover anchor-name-target="--menu-trigger">...</div>

Be careful with this snippet: anchor-name-target is not a standard HTML attribute, so do not copy it as-is. Connect the popover to its anchor in CSS with anchor-name and position-anchor as shown above. Some browsers also treat the invoking button as an implicit anchor for a popover opened through popovertarget; check current documentation before relying on that.

What popover provides natively is top-layer rendering, so the menu appears above everything without z-index battles across the app, light dismiss when the user clicks outside or presses Escape, and sensible focus handling when it opens and closes. It does not trap focus the way a modal dialog does, so for truly modal content use dialog instead.

For a wider look at browser APIs that make npm packages unnecessary, see our overview of native browser APIs replacing popular packages.

Native masonry with grid lanes

Since CSS Grid arrived in browsers in 2017, masonry layouts have still required JavaScript, which is why the Masonry library and similar packages have been installed millions of times. That is changing. The rule below is the complete layout:

.gallery {
  display: grid-lanes;
  gap: 10px;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}

Each card is placed into whichever column currently has the most space, so the items pack tightly with no ragged gaps. There is no ResizeObserver, and no rebalancing columns in script every time an image finishes loading. The syntax took years of debate between browser vendors, with Apple and Google favouring different approaches, before display: grid-lanes emerged as the compromise.

According to the support picture at the time of writing, Safari shipped it first while Chrome and Firefox kept it behind flags. That explains why so few sites use it, and why it is worth writing now. Put grid-lanes inside an @supports (display: grid-lanes) block and let a regular grid serve as the fallback.

View transitions, with real caution

Here caution is justified. View Transition API support exists but is uneven, which put it next to anchor positioning among the most avoided features, and that hesitation is reasonable.

The basic mechanism is simple: wrap a DOM update in document.startViewTransition() and the browser snapshots the old and new states and cross-fades between them, with no animation library and no manual before-and-after bookkeeping. The pseudo-elements below control that default transition, here shortening it to 0.3 seconds:

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.3s;
}

Where it can be trusted today is as progressive enhancement. A browser that lacks the API simply jumps to the new state, so nothing breaks. That makes it safe for non-critical moments such as swapping pages or reordering a list. In script, check that document.startViewTransition exists before calling it, and fall back to applying the update directly.

One palette for light and dark with light-dark()

A typical dark mode keeps two full colour systems, one inside a prefers-color-scheme: dark media query and one outside. The light-dark() function collapses each pair into a single declaration.

The snippet below first opts the document into both schemes with color-scheme, which light-dark() requires to work, and then gives each property its light and dark values side by side:

:root {
  color-scheme: light dark;
}

body {
  background: light-dark(#ffffff, #1a1a1a);
  color: light-dark(#111111, #eeeeee);
}

Once color-scheme is set, every light-dark() call resolves to the right value for the user's system preference, and you no longer duplicate media queries across the stylesheet. For other ways to implement theming, see our comparison of dark mode techniques.

Containing styles with @scope

Much of the motivation for CSS-in-JS or strict BEM naming is specificity leakage: a .card rule meant for one component also styles an unrelated .card nested deeper in someone else's markup. @scope limits styles to a subtree natively.

In the rule below, paragraph styles apply inside .card but stop at any .card-nested element:

@scope (.card) to (.card-nested) {
  p {
    color: var(--card-text);
  }
}

The first selector sets the root of the scope. The to clause sets a lower boundary, leaving a gap where a nested component can manage its own styles instead of inheriting its parent's. None of this needs a build step, generated class names or runtime JavaScript. It will not replace everything CSS-in-JS does, such as styles computed from component props, but it removes one of the main reasons teams adopt it.

The browser is the ecosystem

All five features ship in at least one real browser today, and none of them depends on React, a bundler plugin or a framework release. The distance between what CSS can do and what teams ship is largely habit: for a decade, solving a UI problem has meant installing a package.

Pick one place where your project uses a JavaScript workaround for one of these problems, replace it behind @supports, and see what breaks. Usually less than you expect.

Common questions

Is anchor positioning ready for production?

It has shipped and can be used today behind a @supports check. Its place on the most-avoided list reflects caution about uneven rollout across browsers, not a broken feature. Pairing it with a fallback, such as a simpler static placement, is the appropriate level of care.

Do you still need a masonry library?

For a new project, probably not, provided display: grid-lanes sits inside a @supports block with an ordinary grid as the fallback. For an existing production layout with a strict browser support matrix, keep the library a while longer.

Which feature should you adopt first?

light-dark(). It is a one-line change per colour value, it had the widest support of the five at the time of writing, and it removes a block of duplicated dark mode CSS with no layout risk.

Key takeaways

  • Treat each feature as progressive enhancement: native behaviour where supported, an existing solution elsewhere.
  • Anchor positioning plus popover covers most tooltip and dropdown needs, but wire the anchor in CSS and use dialog when you need a real modal.
  • grid-lanes and view transitions are the least settled; gate them with @supports or a runtime check.
  • light-dark() and @scope delete code immediately and carry the least risk.