Features that landed in 2024–2025. Some are in every browser, some are Chrome-only, one or two are still bleeding edge. All worth knowing about.
@starting-styleAdd an element to the DOM and CSS can now animate its entry — no JavaScript transition hacks needed. @starting-style defines the style an element has before its first computed style, giving the browser a "from" state to transition from.
li {
transition: opacity 0.4s ease, transform 0.4s ease;
@starting-style {
opacity: 0;
transform: translateY(-8px);
}
}
text-wrap: balance & prettybalance distributes text evenly across lines — no more awkward single-word orphans on the last line of a heading. pretty goes further: it optimizes the whole paragraph, not just the last line, avoiding orphaned words anywhere. Both are single CSS properties. Neither needs JavaScript.
The most powerful creative choices are often subtractive. What you leave out is as important as what you put in.
The most powerful creative choices are often subtractive. What you leave out is as important as what you put in.
The most powerful creative choices are often subtractive. What you leave out is as important as what you put in.
/* No more orphaned heading words */
h1, h2, h3 {
text-wrap: balance;
}
/* Optimize the whole paragraph */
p {
text-wrap: pretty;
}
:has() — the parent selectorFor twenty years, CSS couldn't select a parent based on its children. :has() changes that. A container can now respond to its own contents — style a form based on whether its inputs are filled, highlight a card when its button is hovered, show a label when a checkbox is checked. No JavaScript. Below: a submit button that's disabled until there's input, with the container border changing state too. Pure CSS.
/* Form has an empty input — dim the button */
form:has(input:placeholder-shown) .submit {
opacity: 0.35;
pointer-events: none;
}
/* Form has a filled input — activate everything */
form:has(input:not(:placeholder-shown)) {
border-color: var(--accent);
}
field-sizing: contentTextareas that grow with their content — finally a CSS-only solution. Previously you needed a JavaScript resize observer or a mirror element. Now it's one property. The top textarea is fixed height. The bottom one grows as you type. Resize the window and it reflows correctly.
textarea {
field-sizing: content;
min-height: 2.5rem; /* floor it */
/* That's it. No JS needed. */
}
animation-timeline: view() links an animation to an element's position in the viewport — no IntersectionObserver, no JavaScript scroll listeners. Scroll down to see the items below enter. The entrance is driven entirely by your scroll position. We already use this for the reading progress bar in essays; this is the same API applied to element entrances.
.item {
animation: slide-in linear both;
animation-timeline: view();
animation-range: entry 0% entry 30%;
}
@keyframes slide-in {
from { opacity: 0; transform: translateX(-16px); }
to { opacity: 1; transform: translateX(0); }
}
clamp() — fluid typeFixed type scales jump at breakpoints. Fluid type scales continuously. clamp(min, preferred, max) sets a floor, a ceiling, and a linear slope between them — one value, no media queries needed. The preferred is a viewport-relative calculation: a base rem size plus a small vw term that grows it as the screen widens. Drag the slider below to see the difference between a breakpoint jump and a smooth scale.
/* Fixed — binary jump at breakpoint */
h2 { font-size: 1.5rem; }
@media (max-width: 640px) {
h2 { font-size: 1.25rem; }
}
/* Fluid — scales with viewport, no media query */
h2 {
font-size: clamp(1.25rem, 1.17rem + 0.42vw, 1.5rem);
/* 320px → 1.25rem 1280px → 1.5rem */
}
/* This site's display tokens rewritten as clamp() */
--text-2xl: clamp(1.75rem, 1.67rem + 0.42vw, 2rem);
--text-3xl: clamp(2.25rem, 2.08rem + 0.83vw, 2.75rem);
--text-4xl: clamp(2.75rem, 2.5rem + 1.25vw, 3.5rem);
/* Body: intentionally larger on narrow screens */
--text-base: clamp(1.125rem, 1.29rem - 0.21vw, 1.25rem);
Two features worth watching. Not demoing them yet — browser support is too patchy. But the code is here.
Style an element differently when it's actually stuck, snapped, or overflowing. A sticky header that knows it's stuck — without JavaScript. This closes one of the last gaps in CSS's ability to respond to layout state.
Tether any element to any other element — tooltips, dropdowns, popovers — and have them stay anchored as the page scrolls and reflows. No JavaScript position calculations. The browser handles the geometry.
I wanted to actually use these features, not just read about them. The best way to understand what a CSS property does is to have it break in front of you and then fix it. Building this page was that process made visible.
@starting-style is the one I keep thinking about. The idea that CSS now has a concept of "before the element existed" — a state that only fires once, on first render — is a genuinely new primitive. It's not just a transition; it's an entry hook. :has() is the other one. Twenty years of CSS without a parent selector, and now it's here and it's clean.
sibling-count() and sibling-index() — CSS functions that let you style elements based on how many siblings they have. I wanted to include them but browser support is still fragmented. When those land properly, they deserve their own section: a row of items that automatically distributes color or size based purely on count, no JS, no custom properties set from JavaScript. That's the experiment I'm waiting on.
Monospace-first. The CSS is the content here, so the font that reads code should be the dominant voice. Lora only appears in the page title as a single moment of warmth against the technical grid. Everything else is JetBrains Mono — labels, descriptions, demos.