Css
-
Three CSS Features That Finally Let Us Delete the JavaScript
Every few years, CSS quietly absorbs something we used to need a JavaScript library for. This year, three big ones are landing at once: masonry layouts, scroll-driven animations, and styleable
<select>elements. All three have a long history of clunky workarounds (Masonry.js, scroll event listeners,div-based fake dropdowns) and watching them become native CSS features is genuinely surprising progress for a single year.Here’s what each one does.
Native Masonry Layouts
Pinterest-style grids, where items of varying heights pack together without awkward vertical gaps, used to require Masonry.js or one of its cousins. Those libraries measure every element, calculate positions, and use absolute positioning to slot things in. The result works but is expensive, fragile on resize, and hard to make truly responsive.
The native version is
display: grid-lanes:.masonry-grid { display: grid-lanes; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; }Set up your columns the way you would for any responsive grid, switch the display value to
grid-lanes, and the browser handles packing. There’s also aflow-toleranceproperty that controls how aggressively items can shift to fill gaps.The naming has a history. The CSS Working Group debated for years between an earlier
grid-template-rows: masonryproposal (which would have layered onto existing Grid) and a separatedisplay: masonryvalue. The resolution, in early 2026, was a third option: a newgrid-lanesdisplay value that’s distinct from Grid but borrows its column/row sizing. So if you read older articles showinggrid-template-rows: masonry, that syntax did not ship.Safari was first to ship
grid-lanes(Safari 26). Chrome and Firefox have it behind experimental flags as of early 2026.Scroll-Driven Animations
Reading progress bars, fade-ins as elements enter the viewport, parallax effects. All of these used to require listening to the window’s
scrollevent and updating styles in JavaScript. Scroll events fire dozens of times per second, and if your main thread is busy doing literally anything else, the animation stutters. It’s the most common source of jank on the modern web.animation-timeline: scroll()ties a standard CSS keyframe animation to scroll position instead of a time duration. The browser runs it on the compositor thread, completely independent of your JavaScript. Buttery smooth, even when the main thread is on fire.Here’s a reading progress bar in pure CSS:
@keyframes grow-progress { from { transform: scaleX(0); } to { transform: scaleX(1); } } .progress-bar { position: fixed; top: 0; left: 0; width: 100%; height: 8px; background: linear-gradient(to right, #ff416c, #ff4b2b); transform-origin: 0 50%; animation: grow-progress linear; animation-timeline: scroll(root block); }No event listeners. No
requestAnimationFrame. No throttling logic. The animation is just bound to scroll position and the browser figures out the rest.Customizable
<select>This is the one with the longest backstory. The native
<select>element is rendered by the operating system, which means whatever the OS decides to give you is what you get. The dropdown picker, option styling, hover states inside the list, none of it has been styleable from CSS.The workaround was
div-based fake dropdowns built with JavaScript. Most of them broke keyboard navigation, screen reader support, or both. Accessibility regression in exchange for visual polish, every time.appearance: base-selectopts you out of the OS-rendered version and into a fully styleable structure with new pseudo-elements for the popup. To opt the popup itself in, you also applyappearance: base-selectto::picker(select):.custom-dropdown { appearance: base-select; padding: 10px; border-radius: 8px; border: 1px solid #ccc; } .custom-dropdown::picker(select) { appearance: base-select; background-color: #1e1e1e; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.2); padding: 8px; } .custom-dropdown option:hover { background-color: #333; cursor: pointer; }The
::picker(select)pseudo-element is the headline feature. The popup container is the thing every custom dropdown library was reinventing from scratch, and now it’s a styleable part of a real<select>, so native keyboard and accessibility behavior comes along for free.Browser support is Chromium-only as of early 2026 (Chrome, Edge, Opera 135+). Firefox and Safari haven’t shipped it yet.
A Note on Adoption
Support varies by feature. Scroll-driven animations are the most settled, with stable support in Chrome and Safari and a flag-gated implementation in Firefox.
display: grid-lanesshipped first in Safari 26 and is behind flags in Chrome and Firefox.appearance: base-selectis Chromium-only, with no Firefox or Safari implementation yet. For all three, a@supportsblock with a sane fallback is the right pattern. Check caniuse.com before you ship.But the direction is clear. Three of the most common reasons we used to pull in JavaScript are becoming native CSS features. Better performance, less code, and accessibility you don’t have to rebuild from scratch.
I’ll take that trade.
Sources
- Introducing CSS Grid Lanes — WebKit
- Masonry layout — MDN
- CSS scroll-driven animations — MDN
- The
<select>element can now be customized with CSS — Chrome for Developers - Customizable Select Element explainer — Open UI
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
/ Web development / Css / Front-end
-
CSS Finally Got Inline Conditionals
I’ve been digging into some of the newer features landing in CSS, and inline conditionals with the
if()function jumped out as one of the more interesting ones. The idea of writing a condition right next to the property it affects, instead of opening a separate@mediablock ten lines away, sounds useful. So this post is my attempt at summarizing what I found. I hope it helps if you’re trying to figure out whatif()actually does and where it fits.What It Actually Does
The shape is condition, colon, value, with an optional
elsebranch:property: if(<condition>: <value>; else: <fallback>;);You evaluate something, you get back a value, and the property uses that value. The
else:branch is optional. If you leave it off and the condition fails, the property falls back to its inherited or initial value.That’s it.
What makes it powerful is what you can put in that condition slot. Three kinds of queries are supported:
media()for viewport and device conditions,style()for custom property values on the element, andsupports()for feature detection. The same things you’ve always been able to ask CSS, but now you can ask them inline, right next to the property they affect.Theming Without the Yo-Yo
Dark mode is the most obvious win.
.card { background-color: white; color: black; } @media (prefers-color-scheme: dark) { .card { background-color: #333; color: white; } }And here’s the same thing with
if():.card { background-color: if(media(prefers-color-scheme: dark): #333; else: white;); color: if(media(prefers-color-scheme: dark): white; else: black;); }The logic lives next to the property. You don’t have to hunt down a separate block to figure out what happens in dark mode. When you change the light color, the dark color is right there staring at you. That’s a real maintenance win, especially in a stylesheet that’s been touched by five different people.
Variants Without Modifier Classes
This is the example that makes the case best. Component libraries are drowning in modifier classes.
.btn-primary,.btn-danger,.btn-success, on and on. Every one of them is just a different background color and maybe a border.With
if()and a custom property, you can collapse the whole thing:.button { padding: 10px 20px; border-radius: 6px; color: white; background-color: if( style(--variant: danger): red; style(--variant: success): green; else: blue; ); }<button class="button">Submit</button> <button class="button" style="--variant: danger;">Delete</button>No JavaScript reading props and toggling class names. No BEM modifier soup. You set a variable, the CSS reacts. The component owns its own logic.
Feature Detection Inline
The same pattern works for graceful degradation when you want a newer feature with a fallback:
.element { display: if(supports(display: grid): grid; else: flex;); color: if( supports(color: lch(75% 0 0)): lch(75% 0 0); else: rgb(185 185 185); ); }This used to require a dedicated
@supportsblock with the property repeated inside. Now it doesn’t.Observations
A few things are happening here that go deeper than syntactic sugar.
The first is bloat reduction. A component that used to need a base class plus four modifier classes plus a media query block can be one selector. Multiply that across a design system and the savings get real.
The second is encapsulation. A component’s stylesheet can carry its own logic without relying on a JavaScript framework to compute the right class name and shove it into the DOM. The CSS engine is doing the work, natively, and it’s a lot faster at this than your render loop is.
If the logic lives where the value lives, you don’t context-switch to figure out why a property has the value it does. You read the property, you read the condition, you understand it, you move on.
That’s the same reason inline error handling beats a separate try/catch block ten lines away. Co-location is a maintenance superpower.
Browser Support
Browser support is still narrow. Chrome shipped
if()in version 137, but Firefox and Safari haven’t followed yet, so this isn’t something to drop into a production stylesheet without a fallback. The pattern is to declare a plain value first and then override withif(), so non-supporting browsers ignore the line they don’t understand:.card { background-color: white; background-color: if(media(prefers-color-scheme: dark): #333; else: white;); }The syntax has also shifted through CSS Working Group drafts, so older articles you find about
if()may show a comma-ternary form that no longer works. Check MDN before copying anything.But the direction is right. CSS has been quietly absorbing more and more of the work we used to push to JavaScript, and
if()is one of the bigger steps in that direction. I’ll probably keep poking at it.Sources
- if() CSS function — MDN
- CSS conditionals with the new if() function — Chrome for Developers
- Lightly Poking at the CSS if() Function in Chrome 137 — CSS-Tricks
- CSS if() function — Can I use
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
/ Web development / Css / Frontend