Part 1 covered container queries, :has(), nesting, cascade layers, color-mix(), logical properties, and @property. This installment picks up where we left off, covering the features that make JavaScript-free interactivity and animation more achievable than ever.
#@starting-style — Entry Animations Without JavaScript
Before @starting-style, animating an element into the DOM required JavaScript to toggle a class after insertion. Now you can define the style an element starts from, purely in CSS.
1dialog {
2 opacity: 1;
3 transform: translateY(0);
4 transition: opacity 0.3s, transform 0.3s, display 0.3s allow-discrete;
5
6 @starting-style {
7 opacity: 0;
8 transform: translateY(-16px);
9 }
10}
11
12dialog:not([open]) {
13 opacity: 0;
14 transform: translateY(-16px);
15 display: none;
16}
@starting-style defines the "before" snapshot — the style the browser reads on the element's very first render frame. The transition then plays forward from that snapshot to the element's normal styles.
The allow-discrete keyword on the display transition is required when transitioning properties that are normally non-animatable, like display: none.
Browser support: Chrome 117+, Firefox 129+, Safari 17.5+.
#Anchor Positioning
Anchor positioning lets any element act as a reference point for positioning another element — without JavaScript measuring getBoundingClientRect().
1.tooltip-trigger {
2 anchor-name: --trigger;
3}
4
5.tooltip {
6 position: absolute;
7 position-anchor: --trigger;
8
9 /* Position the tooltip above the trigger */
10 bottom: anchor(top);
11 left: anchor(center);
12 translate: -50% 0;
13}
The real power is position-try-fallbacks, which automatically repositions the element when it would overflow the viewport:
1.tooltip {
2 position: absolute;
3 position-anchor: --trigger;
4 bottom: anchor(top);
5 left: anchor(center);
6 translate: -50% 0;
7
8 position-try-fallbacks: --below, --left, --right;
9}
10
11@position-try --below {
12 bottom: auto;
13 top: anchor(bottom);
14}
15
16@position-try --left {
17 left: auto;
18 right: anchor(left);
19 translate: 0 -50%;
20}
This replaces entire libraries dedicated to tooltip and popover positioning logic.
Browser support: Chrome 125+, Edge 125+, Firefox 147+, Safari 26+.
#light-dark() — Inline Theming
light-dark() is a function that returns one of two values depending on the user's color scheme preference, without writing a @media (prefers-color-scheme) block.
1:root {
2 color-scheme: light dark;
3}
4
5body {
6 background: light-dark(#ffffff, #1a1a1a);
7 color: light-dark(#111111, #eeeeee);
8}
9
10.card {
11 background: light-dark(#f5f5f5, #2a2a2a);
12 border: 1px solid light-dark(#e0e0e0, #3a3a3a);
13}
The :root { color-scheme: light dark; } declaration is required — it tells the browser the page supports both modes and enables light-dark() to work.
You can also control color scheme per subtree:
1.inverted {
2 color-scheme: dark;
3 background: light-dark(#1a1a1a, #ffffff);
4}
Browser support: All major browsers since 2024.
#Scroll-Driven Animations
Scroll-driven animations let you tie CSS animation progress to scroll position — no scroll event listeners, no IntersectionObserver, no requestAnimationFrame.
There are two types of scroll timelines:
#scroll() — tied to a scrollable container
1@keyframes progress {
2 from { width: 0%; }
3 to { width: 100%; }
4}
5
6.reading-progress {
7 animation: progress linear;
8 animation-timeline: scroll(root block);
9}
The root keyword targets the page scroller; block means the vertical axis.
Firefox is lacking support for this as of this moment.
#view() — tied to an element's position in the viewport
1@keyframes fade-in {
2 from {
3 opacity: 0;
4 translate: 0 40px;
5 }
6 to {
7 opacity: 1;
8 translate: 0 0;
9 }
10}
11
12.reveal {
13 animation: fade-in linear both;
14 animation-timeline: view();
15 animation-range: entry 0% entry 40%;
16}
animation-range controls which part of the element's journey through the viewport triggers the animation. entry 0% entry 40% means: play the full animation while the element travels from just entering the viewport to being 40% visible.
This entirely replaces the common pattern of IntersectionObserver + toggling a .visible class.
Browser support: Chrome 115+, Edge 115+, Firefox 114+ (behind a flag), Safari 26+.
#text-wrap: balance and text-wrap: pretty
Two new values for text-wrap that improve typographic quality automatically.
#balance
Distributes text evenly across lines, eliminating awkward single-word orphans in headings.
1h1, h2, h3 {
2 text-wrap: balance;
3}
Before: a heading might have four words on the first line and one on the second.
After: the browser redistributes to two and three, or three and two.
Best used on short text (headings, captions) — browsers cap it at around six lines for performance.
#pretty
Similar goal but for body text. It avoids leaving a single word alone on the last line of a paragraph, without rebalancing the entire block.
1p {
2 text-wrap: pretty;
3}
Browser support: balance — all major browsers since 2023. pretty — Chrome 117+, Edge 117+, Safari 26+.
#field-sizing: content
Inputs and textareas that automatically resize to fit their content, with no JavaScript.
1textarea {
2 field-sizing: content;
3 min-height: 3lh;
4 max-height: 20lh;
5}
6
7input[type="text"] {
8 field-sizing: content;
9 min-width: 10ch;
10}
lh is the line-height unit — 3lh means "three line heights tall." Combined with min-height and max-height, this gives you a textarea that grows as the user types and stops at a sensible limit.
Browser support: Chrome 123+, Edge 123+, Safari 26.2+. Firefox support is pending.
#@layer and Third-Party Styles
A practical pattern from Part 1 worth expanding: wrapping third-party CSS in a layer so your own styles always win without !important.
1@import url("https://cdn.example.com/some-library.css") layer(vendor);
2
3@layer vendor, base, components, utilities;
4
5/* Your styles in a later layer always override vendor styles,
6 regardless of specificity */
7@layer components {
8 .button {
9 background: var(--brand);
10 }
11}
Any @import can accept a layer() keyword directly. This is the cleanest way to integrate design systems, resets, or UI libraries into a layered architecture.
#What's Coming in Part 3
- View Transitions API (CSS side) — coordinated page and component transitions
if()in CSS — inline conditional values without custom properties- Relative color syntax — derive colors from other colors with
oklch(from var(--base) l c h) @scope— scoped styles without naming conventions or Shadow DOM
All browser support notes refer to evergreen browsers as of early 2025. For production use, always verify on caniuse.com.