There Is No "CSS4": CSS Snapshots, Baseline, Interop and What Arrived 2023–2026
CSS has no version number: modules raise their own levels independently, and the W3C's CSS Snapshot 2025 states plainly that there is no CSS Level 4. Whether to use a feature is decided with Baseline (newly and widely available), Interop and @supports; the post builds the 2023–2026 chronology from web-features and compares the browser targets of Angular, Vite and Tailwind on Bookstore.
Version note: This post follows the W3C CSS Snapshot 2025 and the Baseline status of November 2026 (web-features 3.46); the examples were run in Chrome 158 and WebKit 27.2 (Playwright 1.65), Angular 22.2 and TypeScript 6.0, and Firefox support is taken from the web-features data.
Guess First
In bookstore-web, the Angular client of Bookstore, the online bookstore used as the shared example across this site's series, I added a nested rule to the global stylesheet, as we learned in the nesting post of this series:
/* the app header, written with native nesting: the build target decides whether it stays nested */
app-root > header {
padding: 1rem;
border-block-end: 1px solid #e5e7eb;
& a {
font-weight: 700;
}
}
Native nesting has been Baseline since December 2023 and widely available since June 2026; in every example of this series it went to the browser as written. After ng build, is this rule still nested in the CSS file in the dist folder, or did the build turn it into two flat rules?
In the last post of the series we step back and look at the big picture. "CSS3" is in the name of this series too, but CSS has no version number, and there will be no "CSS4". In this post we cover how CSS moves forward (levels, modules, Snapshots), how it is decided that a feature is "usable" (Baseline and Interop), the chronology of what arrived between 2023 and 2026, and how to align these decisions with your own project's build tools. The answer to the guessing question is in the last sections.
Levels, Modules and CSS Snapshots
CSS 1 and CSS 2 were single specifications; CSS 2.1 became the final form of level two. After that, the CSS Working Group split the language into modules instead of growing it in one document: Selectors, Color, Cascade, Grid, Flexbox, Values and Units and dozens more. "CSS3" is the name given to the first wave of this split. From then on every module raises its own level at its own pace. What we saw in this series is a good example: :has() comes from Selectors Level 4, @layer from Cascade Level 5, @scope from Cascade Level 6, oklch() from Color Level 4, color-mix() and relative colour from Color Level 5; Grid Level 2 brought subgrid and Conditional Rules Level 5 brought container queries. Anchor positioning, scroll-driven animations and nesting are new modules still at their first levels.
So there is no next level of CSS as a whole. The W3C's CSS Snapshot 2025 says it in one sentence: "There is no CSS Level 4." Modules can reach level 4 or 5, but the language itself no longer has a level. If you see "CSS4" in a job advert, it is usually an informal label meaning "the modern CSS of the last few years".
With no single version number, the question "what is CSS today?" is answered by CSS Snapshots. At intervals the CSS Working Group collects the specifications considered stable at that moment into a list; the latest is the CSS Snapshot 2025 from September 2025, a W3C Group Note. The list groups specifications by stability: those that form the official definition, those that are largely stable but not yet Recommendations, those whose design is complete but with little implementation experience, and those that are widely used but whose details are still not fully specified. An important detail: being in a Snapshot does not mean browser support. The document is mainly for browser implementers. As a developer, the answer to "can I use this today?" comes from somewhere else.
Baseline: Newly and Widely Available
We gave Baseline dates in every post of this series. Baseline is a label that says whether a web feature is supported across the whole core browser set. The core set is Chrome (desktop and Android), Edge, Firefox (desktop and Android) and Safari (macOS and iOS). When a feature is supported in the latest versions of all of these browsers it becomes "newly available"; 30 months after that date it becomes "widely available". The state before that is called "limited availability". Baseline was started by the Chrome team; today it is defined by the W3C's WebDX Community Group, which publishes the data as the web-features npm package.
I added web-features to this post's snapshot as a devDependency and wrote a small script that prints the status of the features this series used:
import { features } from 'web-features';
// the features this series used, from :has() to anchor positioning, plus the ones still on the horizon
const ids = [
'cascade-layers', 'has', 'nesting', 'container-queries', 'subgrid', 'media-query-range-syntax', 'color-mix', 'oklab',
'linear-easing', 'registered-custom-properties', 'starting-style', 'transition-behavior', 'light-dark', 'relative-color',
'popover', 'view-transitions', 'scrollbar-color', 'invoker-commands', 'anchor-positioning', 'shape-function', 'scope',
'contrast-color', 'container-name-queries', 'container-style-queries', 'field-sizing', 'sibling-count',
'progress-function', 'alpha', 'scroll-driven-animations',
'if', 'function', 'random-function', 'customizable-select', 'cross-document-view-transitions', 'display-animation',
'grid-lanes', 'interest-invokers',
];
const browsers = support => Object.entries(support)
.filter(([browser]) => ['chrome', 'firefox', 'safari'].includes(browser))
.map(([browser, version]) => `${browser} ${version}`).join(', ') || 'no browser data';
const rows = ids.map(id => ({ id, ...features[id] }));
const baseline = rows.filter(f => f.status.baseline).sort((a, b) => a.status.baseline_low_date.localeCompare(b.status.baseline_low_date));
const limited = rows.filter(f => !f.status.baseline);
for (const f of baseline) {
const level = f.status.baseline === 'high' ? `widely ${f.status.baseline_high_date}` : 'newly';
console.log(`${f.status.baseline_low_date} ${f.id.padEnd(30)} ${level}`);
}
console.log('--- not Baseline (November 2026)');
for (const f of limited) {
// how many of the feature's compat keys are Baseline on their own
const keys = Object.values(f.status.by_compat_key ?? {});
const ready = keys.filter(k => k.baseline).length;
console.log(`${f.id.padEnd(32)} ${browsers(f.status.support).padEnd(38)} keys Baseline ${ready}/${keys.length}`);
}
node scripts/16-baseline.mjs
2022-03-14 cascade-layers widely 2024-09-14
2023-02-14 container-queries widely 2025-08-14
2023-03-27 media-query-range-syntax widely 2025-09-27
2023-05-09 color-mix widely 2025-11-09
2023-05-09 oklab widely 2025-11-09
2023-09-15 subgrid widely 2026-03-15
2023-12-11 nesting widely 2026-06-11
2023-12-11 linear-easing widely 2026-06-11
2023-12-19 has widely 2026-06-19
2024-05-13 light-dark newly
2024-07-09 registered-custom-properties newly
2024-08-06 starting-style newly
2024-08-06 transition-behavior newly
2024-09-16 relative-color newly
2025-01-27 popover newly
2025-10-14 view-transitions newly
2025-12-12 scrollbar-color newly
2025-12-12 invoker-commands newly
2026-02-24 shape-function newly
2026-03-24 scope newly
2026-04-10 contrast-color newly
2026-05-07 container-name-queries newly
2026-05-19 container-style-queries newly
2026-06-16 field-sizing newly
2026-08-18 sibling-count newly
2026-09-01 progress-function newly
2026-09-14 alpha newly
2026-10-27 scroll-driven-animations newly
--- not Baseline (November 2026)
anchor-positioning chrome 157, safari 27 keys Baseline 323/325
if chrome 137 keys Baseline 0/1
function chrome 139 keys Baseline 0/12
random-function no browser data keys Baseline 0/7
customizable-select chrome 135, safari 27 keys Baseline 0/5
cross-document-view-transitions chrome 126, safari 18.2 keys Baseline 0/4
display-animation chrome 117, safari 18 keys Baseline 0/4
grid-lanes safari 27.2 keys Baseline 0/5
interest-invokers chrome 149 keys Baseline 0/22
The 2023–2026 Chronology and What Is Not Baseline Yet
The top half of the list is not the order of this series' topics, but it is its story. 2023 was a big year for modern CSS: container queries became Baseline in February, oklch() and color-mix() in May, subgrid in September, nesting and linear() in December, and :has() in the last days of the year. All of them are widely available today: every date in the "widely" column is in the past. 2024 brought light-dark(), @property, @starting-style and relative colour; they are waiting for the end of 2026 and 2027 to pass the 30-month mark. 2025 brought popover, same-document view transitions, scrollbar-color and invoker commands. 2026's list is the longest: the core of anchor positioning in January, shape() in February, @scope in March, contrast-color() in April, name-only and style container queries in May, field-sizing in June, sibling-index() and sibling-count() in August, progress() and alpha() in September, and scroll-driven animations at the end of October.
The bottom half is what is not Baseline in November 2026. if() and @function are only in Chrome; we tried both behind @supports in the colour and maths post of this series. For random() web-features 3.46 has no browser data yet; in that post we quoted Safari 26.2 and Chrome 156 from release notes. Customizable select (appearance: base-select) is in Chrome and Safari 27. Cross-document view transitions and transitioning display are in Chrome and Safari but not Firefox; in the last two posts of the series we used both as progressive enhancement. display: grid-lanes, the outcome of the masonry debate, is only in Safari 27.2. interestfor for hover-based popovers is only in Chrome.
The first line of the list needs its own explanation: anchor positioning shows as "limited" in web-features, yet 323 of its 325 browser compatibility entries are Baseline. As the previous post explained in detail, the two entries come from the renaming of the position-visibility values. That is why I added the "keys Baseline" column to the script: a feature's status is only as strong as its weakest entry, and a feature that is almost entirely ready can carry the same "limited" label as one that has not started at all. Do not say "we cannot use it" without looking at the detail behind the label.
Interop: How Features Become Baseline
A feature does not arrive in every engine with the same behaviour by itself. Interop is a process in which the companies contributing to browser engines work every year on focus areas they choose together; the participants in 2026 are Apple, Google, Igalia, Microsoft and Mozilla. Focus areas are chosen from developer proposals and from the incompatibilities most complained about in the State of CSS and State of HTML surveys. Each area is tied to a set of web-platform-tests, and the engines' pass rates on those tests are tracked on a public dashboard throughout the year.
Most of the 20 focus areas of Interop 2026, announced in February, are topics of this series: anchor positioning (continuing from 2025, with only the 2025 tests), container style queries, dialogs and popovers, scroll-driven animations, view transitions (including cross-document transitions), contrast-color(), shape(), attr() and zoom. Following this list is the easiest way to see next year's Baseline candidates in advance: scroll-driven animations and container style queries were in Interop this year and became Baseline this year.
@supports: What the Browser Understands and What It Does
Baseline tells you a feature's general status; @supports tells you what is true in the browser the page is running in right now. In this series we put most new features behind it. In the lab I collected the commonly used forms:
/* 1) property and value: is this declaration understood? */
@supports (position-area: block-end) {
#declaration { color: rgb(22, 163, 74); }
}
/* 2) selector(): is this selector understood? */
@supports selector(:has(> img)) {
#selector { color: rgb(22, 163, 74); }
}
/* 3) not, and, or: the fallback branch written explicitly */
@supports not (animation-timeline: scroll()) {
#negation { color: rgb(185, 28, 28); }
}
@supports (display: grid-lanes) or (grid-template-rows: masonry) {
#either { color: rgb(22, 163, 74); }
}
/* 4) at-rule(): is this at-rule understood? (not Baseline itself) */
@supports at-rule(@scope) {
#at-rule { color: rgb(22, 163, 74); }
}
node scripts/16-supports.mjs
--- chromium
blocks applied: declaration yes, selector yes, negation no, either no, at-rule yes
position-area: block-end true
animation-timeline: view() true
transition-behavior: allow-discrete true
overlay: auto true
position-visibility: anchors-visible true
display: grid-lanes false
width: if(media(width > 1px): 1px; else: 2px) true
selector(:open) true
at-rule(@starting-style) true
--- webkit
blocks applied: declaration yes, selector yes, negation no, either yes, at-rule yes
position-area: block-end true
animation-timeline: view() true
transition-behavior: allow-discrete true
overlay: auto false
position-visibility: anchors-visible true
display: grid-lanes true
width: if(media(width > 1px): 1px; else: 2px) true
selector(:open) true
at-rule(@starting-style) true
@supports tests a declaration, a selector with selector(), and an at-rule with at-rule(); they can be combined with not, and and or. According to web-features, at-rule() itself is not Baseline (Chrome 148, Firefox 157); Playwright's WebKit supports it, but there is no such data for released Safari. It is still safe, because a browser without support ignores that block.
The real lesson of the output is in the middle lines, though. @supports tests whether something can be parsed, not how it behaves. WebKit says true for transition-behavior: allow-discrete; yet in the animation post of this series we measured that the dialog's closing animation did not run in WebKit. The same lines contain a hint: overlay, which the closing needs, is false in WebKit. position-visibility: anchors-visible is true in both engines, but in the previous post we saw WebKit still sending clicks to the hidden tip. CSS is getting named-feature() for testing behaviour; according to web-features it is only in Chrome 150 for now. Until it arrives, where you have to rely on behaviour, measure in a real browser; that is what all the scripts in this series are for.
A Baseline Target for Your Project: Angular, Vite and Tailwind
Build tools also work with a browser target and transform the CSS you write to fit it. To see this I wrote a script that prints which browser versions a few Baseline queries correspond to, using the browserslist the Angular build uses; the script also shows the header rule in the last build's output:
cd angular
npx ng build
cd ..
node scripts/16-targets.mjs
Angular 22.2 default chrome 119, edge 119, firefox 119, ios_saf 17, safari 17
Vite 8 default chrome 111, edge 111, firefox 114, ios_saf 16.4, safari 16.4
widely available today chrome 124, edge 124, firefox 126, ios_saf 17.5, safari 17.5
newly available chrome 153, edge 153, firefox 159, ios_saf 27, safari 27
styles-4KJB3OTK.css: app-root>header{padding:1rem;border-block-end:1px solid #e5e7eb} app-root>header a{font-weight:700}
Angular 22.2's build package uses the query baseline widely available on 2026-05-07 as its default target (that date is fixed in the source of @angular/build). That means Chrome and Firefox 119 and Safari 17. The versions in which nesting became Baseline are Chrome 120 and Safari 17.2. The answer to the guessing question is in the last line of the output: the build unwrapped the rule into two flat rules, app-root>header and app-root>header a. Because the oldest browser in the target does not understand nesting, the build does it for you. In a separate test, the same build left light-dark() and oklch() untouched: features whose syntax can be transformed are transformed, while a new function's behaviour cannot be imitated and is left as it is. For browsers outside the target, @supports and fallback values are still your job.
Vite 8's default build.target is baseline-widely-available; according to the Vite documentation it uses a fixed date for each major version, 2026-01-01 for Vite 8, which means Chrome 111, Firefox 114 and Safari 16.4. The Tailwind CSS v4 documentation gives Chrome 111, Safari 16.4 and Firefox 128 as the minimum versions. All three tools agree: the default target is a widely available line a few years back.
You can also choose the target yourself. I put a .browserslistrc at the root of the Angular project:
baseline widely available on 2026-11-14
The same build, and the last line of the script:
▲ [WARNING] Could not parse 1 selector in styles-TP45VFSL.css; its rule was left out of the critical CSS but still applies once the full stylesheet loads:
& a (Empty sub-selector)
styles-TP45VFSL.css: app-root>header{padding:1rem;border-block-end:1px solid #e5e7eb;& a{font-weight:700}}
With the target raised to Chrome 124 and Safari 17.5, nesting stayed as written. But the Angular build gave a warning: the step that inlines the first screen's CSS into the HTML (critical CSS) could not parse the & a selector and left the rule out of the inlined CSS; the rule still applies once the full stylesheet loads. So raising the target does not mean every tool in the chain understands the new syntax. I deleted the file after the experiment.
For Bookstore there are two options. The first is to stay on Angular's default: the target matches Angular's support policy, the build does the syntax transformations itself and warnings like this do not appear; the cost is slightly longer CSS and writing @supports everywhere for post-2024 features. The second is to move the target to "widely available today" or to a date based on your analytics: fewer transformations, shorter CSS; the cost is knowingly leaving older browsers out and re-testing every step in the tool chain. For Bookstore I stayed on the default. The reason is the institutional customers: the computers of the schools and libraries on the Institutional Orders side are centrally managed, and their browsers often lag behind those of individual users. In your project this decision should rest on the browser distribution in your analytics, not on the dates in a blog post.
Production Notes
I decide whether to use a feature with three questions. What is its Baseline status: if widely, use it directly; if newly, think about what a browser without support shows; if limited, look at the detail. Does the page break in a browser without support, or does it just look plainer: most of the new features in this series were in the second group and were used safely with @supports. How does my build tool handle this feature: does it transform it, leave it as it is, or warn at some step in the chain?
The web-features data is updated every week, and dates are sometimes corrected retroactively; the status of anchor positioning changed twice this year. If your team has set a Baseline target, tie it to web-features or browserslist data pinned to a version, and review updates like any dependency update. The Baseline rules of lint tools (for example stylelint and ESLint plugins) can move this check into code review.
Common Pitfalls
First, thinking "CSS3 or CSS4?". CSS has no version; the question should be "is this feature Baseline, and is it in the browsers I target?".
Second, rejecting a feature without reading behind the "limited availability" label. The label shows the weakest entry; an almost completely ready feature such as anchor positioning can carry the same label.
Third, taking @supports saying true as proof that a feature works correctly. Being parsed and behaving as expected are different things.
Fourth, writing CSS without knowing your build tool's browser target. The same source file produces different CSS for different targets; when you change the target, check the output and the warnings.
A Small Experiment
★☆☆ Add three more features from the early posts of this series to the list in 16-baseline.mjs (for example logical-properties, aspect-ratio, gap; look up the correct ids in web-features) and check when each became widely available.
★★☆ Add a .browserslistrc with the value baseline widely available on 2025-01-01 to the Angular project. Look at whether the build gives a warning and at the output of 16-targets.mjs; read what Angular says about browsers outside its own support policy.
★★★ Turn 16-baseline.mjs into a tool that scans the files in Bookstore's web/css folder and maps the properties and at-rules used in them to features in web-features. It should report whether every newly available or limited use is inside an @supports block.
This post ends the series. From the cascade to anchor positioning, we have seen how CSS is written. Scaling the same CSS with Sass and architectural methods such as BEM are in the Sass/SCSS series, the utility-first approach in the Tailwind CSS series, and the colour, typography and spacing decisions themselves in the UI/UX Design Principles series; I suggest carrying the measuring habit we built in this series, verifying every claim in a real browser, into those series as well.