Just CSS

The progress bar at the top of this page tracks your scrolling. The quiz at the bottom grades itself. The adder does arithmetic. There is no JavaScript anywhere on this page.

0 lines of JavaScript here too

The parent selector exists now :has()

For two decades CSS could only style down the tree. :has() styles up and sideways: this card watches its own form fields and reacts. Type a bad email and the whole card knows.

Pick a mood:
.signup:has(input:user-invalid) { border-color: red; }
.mood:has(#m-hyped:checked) .mood-box { background: conic-gradient(โ€ฆ); }

Animations driven by scrolling scroll-driven animations

The gradient bar pinned to the top of this page is animation-timeline: scroll(root) โ€” its keyframes play as you scroll, not as time passes. The cards below use view(): each one animates as it enters the viewport. This used to be an IntersectionObserver plus a scroll listener plus jank.

I animated in
when I entered
the viewport.
Scroll me out
and back โ€”
I'll do it again.
.card { animation: rise 0.7s ease-out both;
        animation-timeline: view();
        animation-range: entry 0% entry 80%; }

Animating the un-animatable @property

Gradients never animated โ€” the browser saw them as strings. Register the angle as a typed custom property and suddenly it interpolates. This is one element and one background.

@property --spin { syntax: '<angle>'; inherits: false; initial-value: 0deg; }
.plasma { background: conic-gradient(from var(--spin), โ€ฆ);
          animation: rotateGradient 6s linear infinite; }

Components that respond to their container container queries

Media queries ask "how wide is the screen?" Container queries ask "how much room do I have?" โ€” which is what components always wanted to know. Drag the resize handle (bottom-right corner of the box) and watch the card restyle itself. Even the heading size is in container units (cqi).

๐ŸฆŠ

Fiona Fox

Wide layout: avatar left, text right, this bio visible. Narrow: stacked, centered, bio hidden. The component decided โ€” not the page.

Color got good color-mix() & oklch

Nine swatches, one rule each: color-mix() blending our orange into our blue through the perceptual oklch color space โ€” no muddy gray midpoint, no design-tool export, no Sass.

background: color-mix(in oklch, #E44D26 50%, #2965f1);

Typography niceties

Small properties, big polish.

Gradient text is two lines of CSS.

This headline wraps with an awkward orphan word

text-wrap: initial

This headline wraps with an awkward orphan word

text-wrap: balance

Drop caps were a print-design flex that took absurd float hacks on the web. Now it's initial-letter: 3 on a ::first-letter โ€” one declaration, correctly aligned to your baseline grid, in every modern browser.

Shapes, charts, and frosted glass

A morphing clip-path (hover the pentagon), a donut chart that is one conic-gradient and one mask, and a frosted pane of backdrop-filter. Zero SVG, zero images, zero canvas.

backdrop-filter: blur(9px)

Carousels without a carousel library

Swipe or scroll the row โ€” it settles on each card by itself. scroll-snap is the physics engine.

Swipe โ†’
I snap
into place
natively
.snap { scroll-snap-type: x mandatory; }
.snap > div { scroll-snap-align: center; }

CSS is accidentally a programming language

HTML stores state (checkboxes, radios). CSS reads state (:checked, :has()) and computes over it (counters). That's memory, conditionals, and arithmetic โ€” which is why people have built calculators and games in pure CSS. Exhibit A: a quiz that grades itself.

Which HTML element makes an exclusive accordion?
What does :has() let CSS do?
How many npm packages does this page use?

Score:

Exhibit B: a 4-bit adder. Each lit bit adds its place value to a CSS counter โ€” the stylesheet is literally doing the math. Click the bits.

A =
B =

A =  ยท  B =  ยท  A + B =

.bit:has(#a8:checked) { counter-increment: aval 8 total 8; }
.readout .t::after    { content: counter(total); }

The gallery

Bigger productions, still zero JavaScript: