
The Future of CSS: Upcoming Features to Watch
CSS is moving faster than it ever has. A few years ago, a new layout feature took the better part of a decade to go from proposal to something you could ship. Today, features like container queries, :has(), nesting, and cascade layers have gone from "someday" to "use it in production" in a remarkably short time, and the pipeline behind them is full.
This article looks at the features that are arriving now or are close behind: things you can experiment with today, that some browsers already ship, and that will change how you write CSS over the next couple of years. For each one, I'll show what the code looks like, where support stands as of late 2026 in general terms, and how to use it without breaking the experience for people on browsers that don't have it yet. Support changes quickly, so treat the status notes here as a starting point and confirm on caniuse or MDN before you rely on anything.
How a CSS Feature Gets From Idea to Browser
It helps to understand the pipeline, because it tells you how much to trust a feature's current shape.
- Proposal and discussion : Ideas start as issues in the CSS Working Group's GitHub repository. Syntax changes a lot at this stage.
- Editor's draft : A spec text exists and browser engineers start prototyping, often behind a flag.
- First implementation : One engine ships it, sometimes as an origin trial first. The syntax is usually stable by now, but details can still shift.
- Interop : Other engines implement it. Many features are prioritized through the yearly Interop project, where browser vendors agree on a shared list of areas to fix and align.
- Baseline : Once all the core browsers support it, it becomes "Baseline newly available," and about 30 months later "widely available."
Features in stages 1 and 2 are fun to read about but risky to depend on. Features in stage 3 and 4 are good candidates for progressive enhancement, which is how most of the examples below are written.
Inline Conditionals With if()
For years, "CSS has no if statements" was a running joke. Media queries and @supports were the closest thing, and they only worked at the rule level. The if() function brings conditionals inside a single declaration.
.button {
--variant: primary;
background: if(
style(--variant: danger): #f472b6; style(--variant: ghost): transparent;
else: #38bdf8
);
padding: if(media(width < 480px): 0.5rem 0.75rem; else: 0.75rem 1.25rem);
}
.button.is-danger {
--variant: danger;
}
if() accepts three kinds of conditions: style() to test a custom property's value, media() to test a media feature, and supports() to test feature support. Conditions are checked in order and the first match wins, with else as the catch-all.
The big win is colocation. Instead of splitting a button's padding across a base rule and a media query further down the file, both values live together. It also makes custom-property-driven variants far more practical than the old trick of combining calc() with 0 and 1 toggles.
Support : if() has shipped in Chromium-based browsers. Other engines haven't shipped it at the time of writing. Because an unsupported function invalidates the whole declaration, write a plain fallback first:
.button {
background: #38bdf8; /* fallback */
background: if(style(--variant: danger): #f472b6; else: #38bdf8);
}
Custom Functions With @function
Closely related, the CSS Functions and Mixins specification lets you define your own reusable functions. A function takes arguments, which are custom properties, and returns a value through a result descriptor:
@function --fluid(--min, --max) {
result: clamp(var(--min), calc(var(--min) + 2vw), var(--max));
}
@function --tint(--color, --amount: 12%) {
result: color-mix(in oklch, var(--color) var(--amount), transparent);
}
h1 {
font-size: --fluid(2rem, 3.5rem);
}
.badge {
background: --tint(#818cf8);
color: #818cf8;
}
Notice that the function name starts with two dashes, like a custom property, and that arguments can have defaults. You can also declare argument and return types to get type checking, though that part of the syntax is still settling.
Custom functions remove one of the last common reasons to keep a preprocessor around. Sass functions run at build time with fixed values; CSS functions run in the browser and can respond to custom properties that change with theme, container size, or state.
Support : Custom functions have started shipping in Chromium. The mixins half of the spec, reusable blocks of declarations, is still under discussion and isn't ready to use. Until functions are broadly supported, keep using fallbacks or a build step for anything critical.
Masonry Layout With grid-lanes
Masonry, the Pinterest-style layout where items of varying heights pack tightly into columns, has been one of the most requested CSS features for a decade. The debate was never about whether to build it, but how: as part of CSS Grid, or as its own display type. After years of discussion, the working group settled on a dedicated display: grid-lanes value that reuses Grid's track syntax.
<ul class="gallery">
<li><img src="/images/photo-1.jpg" alt="Harbor at dawn" /></li>
<li><img src="/images/photo-2.jpg" alt="City skyline" /></li>
<li><img src="/images/photo-3.jpg" alt="Forest trail" /></li>
<li><img src="/images/photo-4.jpg" alt="Desert road" /></li>
</ul>
.gallery {
list-style: none;
padding: 0;
gap: 1rem;
/* Fallback: a regular grid with aligned rows */
display: grid;
grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
}
@supports (display: grid-lanes) {
.gallery {
display: grid-lanes;
}
}
.gallery img {
display: block;
width: 100%;
height: auto;
border-radius: 0.5rem;
}
With grid-lanes, the browser places each item in whichever column currently has the most room, so there are no gaps under short items. Because it reuses grid-template-columns, everything you know about repeat(), minmax(), and auto-fill still applies.
Support : This is experimental. It has appeared in preview builds, including Safari Technology Preview, and Chromium has been working on an implementation. Syntax details may still change. The fallback above gives you a perfectly usable grid in the meantime, and the @supports block upgrades it automatically when support lands.
A Fully Styleable <select>
Styling the native <select> element has been impossible beyond the closed button, so teams rebuilt dropdowns from <div> elements and inevitably broke keyboard or screen reader support. Customizable select changes that. Opt in with appearance: base-select and you can style the button, the dropdown picker, and the options themselves, and even put rich HTML inside options.
<label for="plan">Plan</label>
<select id="plan" class="plan-select">
<button>
<selectedcontent></selectedcontent>
</button>
<option value="starter">Starter</option>
<option value="pro">Pro</option>
<option value="team">Team</option>
</select>
.plan-select,
.plan-select::picker(select) {
appearance: base-select;
}
.plan-select {
padding: 0.5rem 0.75rem;
border: 1px solid #334155;
border-radius: 0.5rem;
background: #1f2937;
color: #e2e8f0;
}
.plan-select::picker(select) {
border: 1px solid #334155;
border-radius: 0.75rem;
background: #111827;
padding: 0.25rem;
}
.plan-select option {
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
}
.plan-select option:checked {
background: #38bdf8;
color: #0f172a;
}
.plan-select::picker-icon {
color: #94a3b8;
}
The <selectedcontent> element mirrors the currently chosen option inside the button. Everything else, including keyboard navigation, form submission, and accessibility semantics, is still the browser's native select.
Support : Available in Chromium-based browsers, with other engines working on it. This is a textbook progressive enhancement: browsers that don't understand base-select ignore it and show a regular native select, which still works fine. Just make sure the fallback looks acceptable by also styling the closed select with ordinary properties.
Anchor Positioning
Anchor positioning lets you tether one element to another purely in CSS, which is exactly what tooltips, dropdown menus, and popovers need. Combined with the Popover API, it removes the need for positioning libraries in many cases.
<button class="menu-trigger" popovertarget="account-menu">Account</button>
<div id="account-menu" class="menu" popover>
<a href="/profile">Profile</a>
<a href="/settings">Settings</a>
<a href="/logout">Log out</a>
</div>
.menu-trigger {
anchor-name: --account;
}
.menu {
position-anchor: --account;
position-area: bottom span-right;
position-try-fallbacks: flip-block, flip-inline;
margin: 0.5rem 0 0;
inset: auto;
}
position-area places the menu relative to the anchor on a 3-by-3 grid, and position-try-fallbacks tells the browser to flip the menu above or to the other side if it would overflow the viewport, the job that previously required JavaScript measuring on every scroll.
Support : Anchor positioning shipped first in Chromium and has since arrived in Safari, with Firefox working toward it. It's a strong candidate for Baseline soon. Wrap it in @supports (anchor-name: --a) and keep a simple fallback position if you need older browsers.
Scroll-Driven Animations
Scroll-driven animations link a CSS animation's progress to scroll position instead of time. Reading progress bars, parallax effects, and reveal-on-scroll animations no longer need a scroll listener.
.reading-progress {
position: fixed;
inset: 0 0 auto;
height: 4px;
background: #38bdf8;
transform-origin: left;
transform: scaleX(0);
}
@supports (animation-timeline: scroll()) {
.reading-progress {
animation: grow-progress linear both;
animation-timeline: scroll(root);
}
}
@keyframes grow-progress {
to {
transform: scaleX(1);
}
}
@media (prefers-reduced-motion: reduce) {
.reading-progress {
animation: none;
}
}
For element-based effects, view() timelines track an element as it crosses the viewport, and animation-range controls which part of that journey drives the animation. Because the animations run on the compositor where possible, they're typically smoother than JavaScript equivalents.
Support : Supported in Chromium and in recent Safari, with Firefox support still in progress. Always guard with @supports and respect prefers-reduced-motion.
Scoped Styles With @scope
@scope limits styles to a subtree of the document, with an optional lower boundary so styles don't leak into nested content:
@scope (.card) to (.card__content) {
img {
border-radius: 0.75rem;
}
a {
color: #818cf8;
}
}
Here, images and links inside a card are styled, but anything inside .card__content, perhaps user-generated rich text, is left alone. @scope also introduces scoping proximity into the cascade: when two scoped rules conflict with equal specificity, the one whose scope root is closer to the element wins, which makes nested themes behave the way you'd intuitively expect.
Support : Available in Chromium and Safari, and Firefox has been implementing it. Because an unsupported @scope block is ignored entirely, don't put critical styles only inside one yet.
Smaller Features With Big Impact
Not every upcoming feature is a headline. These smaller additions solve everyday annoyances.
Animating to auto Height
Transitioning an accordion to height: auto has never worked. The interpolate-size property opts a page into animating between lengths and intrinsic keywords:
:root {
interpolate-size: allow-keywords;
}
.accordion-panel {
height: 0;
overflow: hidden;
transition: height 300ms ease;
}
.accordion.is-open .accordion-panel {
height: auto;
}
It's supported in Chromium. Elsewhere, the panel simply opens instantly, which is a perfectly acceptable fallback. For the lower-level version, calc-size() lets you do math on intrinsic sizes.
Tree-Counting Functions
sibling-index() and sibling-count() return an element's position among its siblings and the total count. Staggered animations become one line:
.list-item {
animation: fade-in 400ms ease both;
animation-delay: calc(sibling-index() * 60ms);
}
@keyframes fade-in {
from {
opacity: 0;
translate: 0 8px;
}
}
Currently in Chromium only. Without support, the animation-delay declaration is dropped and all items animate together.
Typed attr()
attr() has worked in content for decades, but only as a string. The upgraded version reads attributes as typed values in any property:
<div class="progress" data-value="72%"></div>
.progress {
--value: attr(data-value type(<percentage>), 0%);
background: linear-gradient(to right, #34d399 var(--value), #1f2937 0);
height: 8px;
border-radius: 4px;
}
This is Chromium-only for now, so pass the value through a custom property in inline styles as a fallback if it matters.
Scroll-State Container Queries
Scroll-state queries let you style an element based on whether it's stuck, snapped, or scrollable, all without an IntersectionObserver:
.site-header {
position: sticky;
top: 0;
container-type: scroll-state;
}
@container scroll-state(stuck: top) {
.site-header__inner {
box-shadow: 0 4px 16px rgb(0 0 0 / 0.3);
background: #111827;
}
}
Note that the styles apply to a child of the sticky element, because a container query can't style the container itself. Chromium-only for now.
Trimming Text Boxes
text-box trims the extra space above and below text that comes from font metrics, so text can finally align optically with icons and container edges:
.button-label {
text-box: trim-both cap alphabetic;
}
Supported in Chromium and Safari. Without support, you just get the usual half-leading, so it's safe to add.
New Corner Shapes
corner-shape extends border-radius with shapes like squircle, bevel, scoop, and notch:
.avatar {
border-radius: 30%;
corner-shape: squircle;
}
It's recently landed in Chromium. Other browsers fall back to ordinary rounded corners, which is a graceful degradation.
How to Experiment Safely
Trying new CSS doesn't have to be risky. A few habits keep experiments from turning into production bugs:
- Use preview browsers : Chrome Canary, Firefox Nightly, and Safari Technology Preview get new features first. Many experimental features are toggled in
chrome://flagsor Safari's Feature Flags settings. - Always write the fallback first : Put the widely supported declaration before the new one, or wrap the enhancement in
@supports. - Test with the feature off : Open the page in a browser that lacks support and make sure it's still usable. That's the experience a real share of your users will get.
- Follow the source : The CSS Working Group's GitHub issues, the Chrome Developers blog, WebKit's feature announcements, and Mozilla's Hacks blog announce changes early. The Interop dashboard shows which features the vendors are aligning on this year.
- Watch Baseline status : MDN and caniuse show Baseline badges. When a feature turns Baseline newly available, it's a good moment to review whether your fallback can become the primary code path.
Conclusion
The future of CSS is less about new visual tricks and more about moving logic into the language: conditionals with if(), reusable logic with custom functions, positioning with anchors, animation timelines tied to scroll, and native components like the customizable select that used to require thousands of lines of JavaScript.
Not all of these are ready for unguarded use, and some will still change before they settle. But almost every one can be adopted today as a progressive enhancement, with a plain fallback for browsers that aren't there yet. Pick one that solves a real problem in your current project, wrap it in @supports, and ship it. By the time it becomes Baseline, you'll already know it well.


