← experiments
01 — @starting-style
Chrome 117+ · Firefox 129+ · Safari 17.5+

Add 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.

Demo — add and remove items
  • Item one
  • Item two
li {
  transition: opacity 0.4s ease, transform 0.4s ease;

  @starting-style {
    opacity: 0;
    transform: translateY(-8px);
  }
}
02 — text-wrap: balance & pretty
balance: all major · pretty: Chrome 117+

balance 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.

Same text, three wrapping modes
none (default)

The most powerful creative choices are often subtractive. What you leave out is as important as what you put in.

balance

The most powerful creative choices are often subtractive. What you leave out is as important as what you put in.

pretty

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;
}
03 — :has() — the parent selector
Chrome 105+ · Firefox 121+ · Safari 15.4+

For 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.

Demo — form that responds to its own state
The button state and border are controlled by CSS :has() — no JS event listeners.
/* 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);
}
04 — field-sizing: content
Chrome 123+ · not yet in Firefox/Safari

Textareas 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.

Demo — type in both
Fixed height (old way)
field-sizing: content (new way)
textarea {
  field-sizing: content;
  min-height: 2.5rem; /* floor it */
  /* That's it. No JS needed. */
}
05 — Scroll-driven view animations
Chrome 115+ · Firefox 132+ · Safari 18+

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.

Demo — scroll down to trigger
animation-timeline: view() Ties animation progress to scroll position in viewport
animation-range: entry 0% entry 30% Plays the animation while the element is entering the viewport
@media (prefers-reduced-motion) Always disable scroll animations for users who prefer it
No JavaScript required Entirely declarative. The browser handles the math.
.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); }
}
06 — clamp() — fluid type
All major browsers

Fixed 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.

Demo — drag to simulate viewport width
Viewport: 800px
Fixed + breakpoint
The most powerful design choices are often subtractive.
font-size:
clamp() — fluid
The most powerful design choices are often subtractive.
font-size:
/* 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);
07 — On the horizon
Canary / limited

Two features worth watching. Not demoing them yet — browser support is too patchy. But the code is here.

scroll-state queries

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.

.sticky-header {
  container-type: scroll-state;
}

@container scroll-state(stuck: top) {
  .sticky-header {
    background: var(--bg-solid);
    border-bottom: 1px solid var(--border);
  }
}
anchor positioning

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.

.trigger {
  anchor-name: --my-anchor;
}

.tooltip {
  position: absolute;
  position-anchor: --my-anchor;
  top: anchor(bottom);
  left: anchor(center);
}
Cee's notes

Why this page exists

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.

The ones that surprised me

@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.

What's missing

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.

The aesthetic choice

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.