Interaction Specimens

The third working surface for the motion system. Motion Specimens answers what scrolling can drive; Animation Specimens answers pointer, entrance, text, shape, ambient, data and easing. This one answers the gap both of them leave: a thing becoming something else. Everything we had animates something arriving, or something being pointed at. Almost nothing animated a change of state, which is most of what a real page actually does.

47 specimens across ten families. Free means pure CSS, authoring only — 23 of them are. Behavior means it needs a piece of code in pattern_render.js, because templates ship no JavaScript; the runtime does. Nothing here is decided or queued: see spec/accelerate-motion-system.md.

The photograph and the icon face are embedded in the page rather than linked, so this post carries its own assets wherever it is published. The icons are seven glyphs subset out of Font Awesome Free (CC BY 4.0) — 112 KB down to 1 KB.

Your browser, right now

State change

01 — Height to auto, for real

interpolate-size: allow-keywords + calc-size()  —  free

Every accordion ever written has guessed at a max-height, because auto could not be interpolated. Guess low and long panels are cut off; guess high and the transition spends most of its duration animating empty space, which is why so many of them ease wrong. interpolate-size lets the keyword take part, so height: 0 to height: auto is simply a transition.

interpolate-size

Nothing. We price from the survey, and the price we give is the price you pay.
Three to five days for a typical terraced house, weather allowing. We will tell you which days before we start, and we do not start a job we cannot finish. If the felt underneath has gone, that adds a day.

max-height, guessed at 90px

Nothing. We price from the survey, and the price we give is the price you pay.
Three to five days for a typical terraced house, weather allowing. We will tell you which days before we start, and we do not start a job we cannot finish. If the felt underneath has gone, that adds a day.

Open the long panel on both sides. The guess clips it. This is the specimen that matters most on the page, because accordion and tabs already ship — it improves two things we have rather than adding a third. Chromium today; check the panel above for this browser.

02 — View Transition between two layouts

document.startViewTransition() + view-transition-name  —  behavior

The cards do not fade out and back in — each one travels from where it was to where it now is, because the browser matched them by name across the two states. Six lines of code for something that used to be a library. Where it is unsupported the layout still swaps, just instantly, which is the correct fallback.

Re-roofingStrip, felt, batten, re-tile.
Flat roofsEPDM and torch-on felt.
GutteringReplaced, realigned, cleared.
ChimneysRepointing and flashing.

One view-transition-name per card, set by the runtime because two elements sharing a name in one transition is an error. Nothing in the stylesheet describes the movement.

03 — Filter and sort that move

FLIP — measure, mutate, invert, play  —  behavior

A filtered grid normally teleports: you press a button and the remaining items are suddenly somewhere else, and the reader has to find their place again. FLIP measures every item first, lets the DOM change, measures again, and animates the difference away. It works in every browser and needs no View Transition support.

Ridge tilesRoof
DownpipesRainwater
RepointingMasonry
Felt and battenRoof
Gutter clearingRainwater
Chimney stackMasonry
Valley leadRoof
Fascia boardRainwater
RenderingMasonry
Slate repairRoof
Soffit ventsRainwater
LintelsMasonry

The same instrument does filter and sort, because neither is really about filtering or sorting — both are just “the DOM changed, animate the difference”.

04 — The card becomes the panel

FLIP from the card’s rect to the panel’s  —  behavior

A detail view that fades in on top of a grid asks the reader to work out what they are now looking at. A detail view that grows out of the card they clicked does not. It is the same measurement as the filter above, run once, between two different elements.

 

 

Deliberately built on FLIP rather than a View Transition, because it has to work in the browser this post is being read in. When View Transitions are universal this becomes two lines.

05 — Adding and removing

FLIP on the survivors, keyframes on the arrival  —  behavior

The row that leaves should leave, and the rows below it should close the gap rather than jump up into it. Two different animations for two different jobs: the leaving row is animated directly, and everything that has to move because of it is FLIPped.

Scaffold, three sides£640
Strip and dispose£480
Breathable membrane£310

The mistake to avoid is animating the leaving row and calling it done. What the eye actually notices is the gap closing.

06 — A swap with no observer and no keyframes

@starting-style + transition-behavior: allow-discrete  —  free

An element going from display: none to visible has always snapped, because there was no first frame to transition from. @starting-style supplies one and allow-discrete keeps the element around long enough to leave properly. No class, no observer, no JavaScript at all — only a hidden attribute changing.

Free surveySomeone on the roof, not a price over the phone.

If this holds up everywhere, entrance animation above the fold needs no runtime at all. Below the fold it is still wrong, because it runs on first render rather than on being seen.

Picture

07 — Ken Burns

transform on the picture, alternating  —  free

A still photograph that slowly pushes in and drifts stops being wallpaper. Two rules matter: the scale never drops below about 1.02, so no pan can expose an edge, and the two tiles run at different durations so a pair of them never marches in step.

Twenty four second push
Thirty second pull

The cheapest way to make a single supplied photograph carry a hero. Costs one keyframe and nothing at runtime.

08 — Before and after

clip-path: inset() driven by a range input  —  behavior

The handle is a real <input type="range"> lying over the frame at zero opacity. That is the whole accessibility story: it drags, it takes focus, it responds to arrow keys and it announces itself, none of which a div would have done without a great deal of work.

AfterBefore

Two copies of one photograph, graded differently. A real before-and-after would be two photographs; nothing else changes.

09 — Grade shift on hover

filter + a blend-mode tint  —  free

A grid of photographs from different jobs, in different weather, on different cameras, looks like a jumble. Push them all through one duotone and they look like a set. Hover returns the real picture, so the treatment never costs the reader the detail.

The tint is the palette, so a site that changes its accent changes its photography with it. This is the strongest argument on the page for treating color as design rather than as a buyer option.

10 — Focus pull

blur to sharp on arrival  —  free

The picture arrives out of focus and resolves, with the caption a third of a second behind it. The negative inset is doing quiet work: a blurred element shows a soft edge where it stops, so it is drawn slightly larger than its frame and the frame clips it.

Sanderstead, four days

The same shape as a real progressive image load, which is what makes it worth having: it is a decoration that a genuine low-quality placeholder could later replace with no change to the design.

11 — Type cut out of the photograph

background-clip: text over a drifting background-position  —  free

The letters are a window onto the picture, and the picture moves behind them. What travels is the background’s position across an oversized box — sliding the picture bodily would look static, because every part of it would arrive where the last part just was.

Twelve years
on the same
roofs

Wants a photograph with real contrast across it. On a flat sky it reads as gray text, which is a fitness rule the designer has to apply, not something the runtime can check.

12 — Directional hover

entry edge from the pointer, spent as a transform origin  —  behavior

The fill comes in from the side the pointer arrived on, and leaves by the side it departs to. It is a small thing that makes a card feel like a physical object rather than a rectangle with a hover state. The runtime reports the edge; the stylesheet does everything else.

Re-roofing
Flat roofs
Guttering

Comparing the pointer offset against the box’s own proportions is what makes it right on a wide card and a tall one alike. Comparing against pixels gets the corners wrong.

Line

13 — Two ways to bring an icon in

stroke-dashoffset over a measured length  /  a moving mask  —  free

On the left, each stroke is dashed with exactly its own length, offset out of sight, and let back in. The length comes from getTotalLength() — guessing it is the commonest way this fails, since a guess of 1000 against a real 1013 leaves the shape eleven pixels from closing, which reads as a mistake rather than a style. On the right, nothing is drawn at all: a gradient mask travels across and reveals whatever is underneath.

Stroke draw

Needs a stroked path with fill: none. Hand-drawn here.

Mask wipe

Fixed before we start

Filled SVG, then real Font Awesome glyphs from the icon chooser, then plain text. The face is embedded in the post, so the middle row does not depend on the host site having Font Awesome.

The two are not alternatives — they can reveal different things. A stroke draw needs a stroked path, so it only ever works on geometry the designer drew: the shape set, the connectors, the sparkline, the tick marks. It cannot touch a single icon a buyer can choose. All three sets in the chooser — Font Awesome, Bootstrap Icons, Dashicons — ship as webfonts, and a glyph has no path in the document to measure; the outline is inside the font file. Even as SVG they are filled outlines rather than strokes, and stroking one traces its silhouette, holes included. The mask wipe has no such requirement, because it reveals rather than draws — which is why the middle row above is the buyer’s real icons and the bottom row is a sentence. Strokes inside one icon are still staggered by --i, the same declaration a collection of any size would use.

14 — A shape set that can morph

transition on d, over one normal form  —  free

A path transitions directly, with no library and no interpolation code — on one condition: both shapes must be built from the same list of commands. That condition is what usually kills the technique, because a circle drawn as four arcs and a hexagon drawn as six lines have nothing in common. The answer is to stop authoring shapes and start generating them into a single form: M, twenty-four cubic segments, Z, every time. Any shape in the set can then become any other, in any order, with no work per pair.

 

Twenty-four is not arbitrary. The on-path points sit every 15°, and every corner of every shape lands exactly on one — the square’s at 45°, the triangle’s at 120°, the hexagon’s at 60°, the plus’s outer corners at 75°. A corner falling between two sample points would be quietly rounded off, which is the failure the spacing exists to prevent. Sharp shapes take control points a third and two thirds along each chord, so the edges are mathematically straight; smooth ones take Catmull-Rom handles, which reproduce a circle to within one percent.

15 — Connectors that draw between steps

a path built from measured positions  —  behavior

The steps rise in turn and the line reaches from each one to the next. The path cannot be authored, because where the cards are depends on how many columns fit — so it is measured, built, and rebuilt whenever the element’s own width changes. Narrow the window and the connectors turn the corner and run vertically.

Step oneYou tell us what happened
Step twoWe survey and put a price on it
Step threeWe do the work on the day we said

Watched by a ResizeObserver on the element itself, not a window resize listener. A block can change width with the window untouched — a sidebar opening, a container query reflowing a column.

16 — Traveling along a curve

offset-path + offset-rotate: auto  —  free

The marker follows the dotted route and turns to face the direction it is going, from one property. Doing this with keyframed translations means authoring every point on the curve; here the curve is the animation.

The route and the offset-path are written in the same fixed coordinates and the pair is scaled together. Two coordinate systems is how a marker ends up traveling beside its own path instead of along it.

Data

17 — Bars that grow

height transition, staggered by index  —  free

The bars grow rather than scale. A scaled bar stretches its gradient and its corner radius with it, which gives the trick away instantly. The track has an explicit height so the percentage has something definite to resolve against — the usual reason a chart like this renders flat.

38Mon
54Tue
47Wed
72Thu
91Fri
63Sat

One declaration carries the stagger for six bars or sixty. This is --accpt-i in everything but name, and it is why that property keeps coming up.

18 — A line that draws itself

stroke-dashoffset, then the fill, then the dot  —  free

Three things on one timeline: the line draws, the area beneath it fades up behind it, and the final point arrives on a spring. Each is a plain transition on its own delay — there is no sequencer, because delays are a sequencer.

The spring on the dot is a linear() curve. Overshoot used to need a physics library and is now a list of stops in a timing function.

19 — Donut segments in turn

@property percentages inside a conic-gradient  —  free

A percentage in a gradient cannot be animated until it is registered — the browser has no idea it is a number rather than an arbitrary token. Three registered properties, three transitions on three delays, and the segments sweep in sequence from a single background declaration.

Re-roofing — 41%Repairs — 26%Guttering — 22%Other — 11%

The hole is a pseudo-element with the surface color, not a mask. Worth saying because the obvious alternative — a pseudo at z-index: -1 — disappears behind its own parent’s background.

20 — A needle that overshoots and settles

linear() easing past 1 and back  —  free

A real gauge needle does not glide to its value and stop dead; it goes slightly past and comes back. The curve here passes 1.14 at 40% of its duration and settles, which is a spring written as a timing function. The arc fills on an ordinary ease, so the two read as one movement.

74

If named easing curves ever ship in a vocabulary they must be longhands. An animation: shorthand silently resets the timing function, and the spring quietly becomes an ease.

21 — Rating sweep

a clipped overlay widening to a percentage  —  free

Two identical rows of stars, one gray and one gold, the gold one clipped to a width. Partial stars come free, the value is a single number, and the whole thing is one element wide — no per-star markup, no half-star glyph, no icon font.

4.35 out of 5

Rating size belongs in the element panel, not in a template option. The animation is the designer’s; the size is the buyer’s, and duplicating it as an option would make the panel look broken.

22 — Figures that format as they count

a number reported per frame, formatted on the way out  —  behavior

A counter that runs to 1284900 and only becomes £1,284,900 at the end has spent its whole animation being unreadable. Formatting each frame costs nothing and the figure is legible throughout — currency, percentage or signed integer, decided by the node, not by the runtime.

£0
Work completed
0%
On the promised day
+0
Roofs this quarter
0
Tiles laid

Tabular figures, or the whole line jitters as digits of different widths pass through. That is a font setting, and it is the difference between this looking finished and looking cheap.

Scroll

23 — A section that scrolls sideways

a sticky pane, a tall runway, --p on the track  —  behavior

The page scrolls down and the content moves across. The tall container is only there to give the page something to scroll through; the pane sticks to the viewport and the track inside it is pushed by the same 0-to-1 fraction that drives the parallax specimen on the previous post. Nothing new is needed to build it.

SurveySomeone climbs up and looks.
PriceWritten, itemised, fixed.
ScaffoldUp the day before we start.
StripDown to the rafters, skipped same day.
Re-coverMembrane, batten, tile.
ClearScaffold down, drive swept.

The travel is measured from the track and the pane, watched by a ResizeObserver. Fixing the distance in the stylesheet is how these break the moment a buyer adds a seventh panel.

24 — A strip that advances, until you touch it

scroll-snap + a timer that yields  —  behavior

Auto-advance is usually built as a carousel that owns the content and fights the reader for it. This is a scrollable row first: with the script removed, or motion off, it is still a perfectly good strip you can drag. The timer stops the moment a human does anything and only comes back after seven seconds of stillness.

Sanderstead
Purley
Coulsdon
Kenley
Warlingham
Caterham

The reader owning the control is the whole design. An auto-advancing thing that resumes over the top of someone reading is the most complained-about pattern on the web.

25 — A header that condenses

one fraction spent on six properties at once  —  behavior

Padding, type size, letter-spacing, the strapline’s height and opacity, the button, and the shadow — all driven by the same number, all in the stylesheet. The runtime does not know a header is being condensed. It reports a fraction and stops.

Sanderstead and the CR postcodes since 2013Get a price

Keep scrolling. The bar above condenses over the first 150 pixels of this block, then holds. The block itself has no height of its own: the runway is however long this text happens to be.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.

Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit.

At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa.

Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores.

Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus.

Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.

Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit.

This one taught us something when its authored 170vh was removed. A fraction of the runway is the wrong measure for a condensing header: with the runway coming from content, the same 18% is 50 pixels under a short section and 500 under a long one. So it takes data-pdist instead — finish after this many pixels, whatever the element’s length. Fractions suit anything that should track the whole passage; distances suit anything that should settle and then hold.

26 — Reading progress

the same fraction, as a width  —  behavior

The plainest possible use of the fraction, and the most useful. It is included to make a point: this is not a new effect, it is the same instrument as the condensing header and the sideways section, pointed at a different property.

Progress through this block — 0%

Four specimens now, all sharing one reader. If a fifth appears it will not need a fifth piece of code. This block has no height of its own either — the bar is measuring real text, which is the only thing it would ever be measuring on a real page.

Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit.

At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa.

Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores.

Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus.

Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.

Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit.

At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa.

Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores.

Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus.

Where reversing on scroll-up matters and Chromium-only is acceptable, this is also the one effect that CSS scroll-driven animation can do entirely on its own.

27 — Color that ramps as you pass

color-mix() with a fraction as the percentage  —  free

The band interpolates between two palette roles as it crosses the viewport, and it does it in both directions — there is no state to get out of step, only a fraction recomputed from where the element currently is. Because the colors are roles rather than values, a site with a different palette gets a different ramp for free, and neither end of it was ever chosen by hand.

The palette is doing the work0%

This one had a real bug, and it is a useful one. There are two ways to measure a fraction and only one of them suits a given element. Runway is for a tall container holding something sticky. Band is how far an element has crossed the viewport, from just below it to just above. This was being measured as a runway on the strength of being slightly taller than the viewport — so the fraction ran 0 to 1 across about a hundred pixels and sat clamped for the whole of the rest of the scroll, which reads as two flat colors with a brief mix between them. The mode is declared now rather than guessed at.

28 — Edges that admit there is more

a mask with registered lengths  —  behavior

A row that scrolls sideways with a hard edge looks like a row that ends. Fading it says there is more, and removing the fade at each end says there is not. The fade lengths are registered properties, so they transition instead of snapping when you reach the end.

Re-roofing
Flat roofs
Guttering
Chimneys
Fascias
Soffits
Leadwork
Repointing

A scroll listener today. scroll-state() container queries would make it pure CSS, and are worth watching for exactly this.

29 — A cue that retires

an animation that stops once it has been obeyed  —  behavior

A scroll hint that keeps bouncing after the reader has scrolled is nagging them, so it needs a condition under which it stops. The condition here is most of a screen’s worth of scrolling from the moment the cue comes into view — it stays solid while you are still in its section, fades over the last third, and is gone by the time you have scrolled past it.

Scroll

Retired: 0%

Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores.

Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus.

Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.

The first build of this measured from page load rather than from arrival, so for anything below the fold the counter was already spent by the time the cue came into view — it retired before it had been seen at all, which is why it looked like nothing was happening. A cue’s clock starts when the reader arrives, not when the document does. The general rule it stands for: every ambient animation should have a condition under which it stops. Most have none, and that is why so many pages feel restless.

Pointer

30 — A button that reaches for the pointer

distance to center, spent as a translation  —  behavior

Inside a radius, the button leans toward the cursor by a fraction of the distance — more as you get closer, none at the edge. It makes a single call to action feel like the only interactive thing in a quiet area, which is usually exactly what it is.

Wrong for a row of buttons: several things all leaning at once reads as a wobble. This is a one-per-screen effect and the vocabulary should say so.

31 — Ripple from where you pressed

a circle sized and placed at the click point  —  behavior

Feedback that starts under the finger rather than at the center of the control. On a small button it barely matters; on a large card or a wide bar it is the difference between the press being acknowledged and the whole thing just flashing.

The circle is removed after 700ms. Left in place they accumulate, and a page that has been clicked forty times carries forty dead elements.

32 — Tilt with a glare that tracks

two rotations and a moving radial highlight  —  behavior

The tilt alone reads as a gimmick. Adding a highlight that moves as though a light source were fixed above the page is what makes it read as a surface — the card is catching the light rather than wobbling.

Sanderstead, full re-roof

The glare is a sibling with its own opacity, not a pseudo at a negative z-index — that would paint behind the card’s own background and be invisible.

33 — Layers that separate under the cursor

one pointer position, three different depths  —  behavior

Three copies of one photograph at different blurs and different travel distances, with the copy moving the other way. The runtime reports a position between minus one and one; each layer multiplies it by its own depth. Adding a fourth layer needs no new code.

Twelve years on the same roofsMove the pointer across

White text on a black shadow, because it sits over a photograph and no palette role can be guaranteed to be readable there. That rule is absolute in the module spec and this is why.

34 — Ghosted, shimmering, then solid as you approach

distance to the pointer, plus a masked glint  —  behavior

At rest each icon sits at about a sixth of full strength with a glint passing through it every few seconds — enough to say it is there and can be used, not enough to compete with the words. As the pointer comes near, the nearest one fades up and grows very slightly, and its shimmer stops: once the reader has arrived it no longer needs advertising. Move away and it settles back.

Move the pointer along the row. Distance is measured to every icon, not only the one underneath.

The glint is a second copy of the same icon at full strength, masked to a narrow traveling band — so it takes the icon’s own shape instead of being a bright rectangle laid over the top. That is the same instrument as the mask wipe in 13, looped and narrowed, which means it works on a font glyph and needs no path. The falloff is squared, so the nearest icon clearly wins and its neighbors only just stir; a linear falloff reads as one wide soft hover rather than as proximity. The honest difficulty is that a ghosted icon is faint by design. If it is decoration that is fine, and if it carries meaning it is not — so with no pointer to get closer with, the whole premise is gone and the icons simply show: @media (hover: none), and the same with motion off.

Text

35 — Word by word

each word clipped, each rising on its own delay  —  behavior

Letters arriving one at a time is a party trick; words arriving one at a time is readable while it happens. Each word sits in its own overflow-hidden box and rises out of it, so the line looks like it is being set rather than assembled.

Someone climbs up and looks before anyone gives you a price

The delay is calc(var(--i) * 55ms). Same declaration for eleven words or forty, which is the entire argument for emitting an index.

36 — Line by line

lines found by measuring where the text wrapped  —  behavior

A line is a result of layout, not a property of the text, so the only honest way to find one is to lay the words out and see where the tops change. Resize the window: the lines are found again and the effect still runs on real lines rather than on where they were when the page loaded.

We have been on the roofs around Sanderstead for twelve years, which is long enough to have been back to some of them twice. The second visit is usually somebody else’s work.

Re-split on the element’s own resize, and the run state is carried across the rebuild — otherwise resizing an already-revealed paragraph hides it again.

37 — Highlighter across the phrase

background-size on a no-repeat gradient  —  free

The phrase is marked as it is read, in sequence, as though someone were going through with a pen. It is a <mark> element, so it carries the meaning as well as the look, and the sweep is a background growing from zero width.

A survey is free and takes half an hour. The price we give you afterwards is fixed in writing before anyone climbs a ladder, and the work is guaranteed for ten years.

The gradient is two palette roles at 45% each, so it tints rather than obliterates and the text underneath stays at full contrast.

38 — Weight as a continuous thing

font-variation-settings on a variable face  —  free

Not a jump between a light cut and a bold cut — every weight in between actually exists and is drawn. The headline arrives at 200 and settles at 840; the second line runs a wave along the letters. This needs a genuine variable font, which is the whole cost of it.

Fixed before we start

Torrance Roofing

Worth knowing about but awkward to ship: it means the design depends on a variable face being available, and a fallback stack that steps between two cuts looks broken rather than plain.

Feedback

39 — Idle, working, done

three states, the tick drawing itself  —  behavior

The commonest unanimated moment on any site: you press the button and nothing happens for a second and a half. Three states with a width that does not jump between them, and a tick that draws rather than appears — the drawing is what makes it read as a result rather than as a new icon.

The minimum width is fixed so the three labels do not resize the button under the cursor. That is not decoration — a button that shrinks while being pressed can move out from under a finger.

40 — Skeleton handing over

both states in one grid cell, crossfading  —  free

The shimmer is the easy half and we already had it. The half that is usually wrong is the handover: the placeholder disappears, the content appears, and the page jolts because the two were never the same size. Stacking both in one grid cell means nothing reflows at the moment of the swap.

Sanderstead, four days

Stripped to the rafters, new breathable membrane, treated batten and reclaimed clay plain tiles to match the neighbors.

The scaffold went up on the Monday and came down on the Friday.

The placeholder should be the shape of the content, not a generic gray block. Getting the proportions roughly right is what stops the swap being noticeable at all.

41 — A field that says no, then yes

a shake that can retrigger, a tick that draws  —  behavior

Press the button with something wrong in the field, then again, then fix it. The second press has to shake as visibly as the first, which is where this normally breaks: removing and re-adding a class in the same frame is coalesced into no change at all unless layout is forced in between.

getBoundingClientRect() forces the layout. The usual trick, void el.offsetWidth, is undefined on SVG elements and silently does nothing there.

42 — Confirmation that does not move anything

an absolutely positioned pill on a spring  —  behavior

Saying “copied” by changing the button’s label makes the button change width and the row reflow. Saying it beside the button, out of flow, does not disturb anything — and it can leave on its own without a second reflow when it goes.

✓ Copied

The spring is a linear() curve again. Three specimens on this page use one now, which is the argument for naming a few and putting them in the vocabulary.

Ambient

43 — Grain

an feTurbulence tile, jumped on a step timer  —  free

A flat gradient is the most obviously computer-made thing a page can contain. Three hundred bytes of generated noise over the top, jumping to a new offset six times a second, gives it a surface. The tile is an SVG filter — there is no image to ship.

Twelve years on the same roofs

steps(1, end) rather than a smooth movement. Grain that slides is a texture sliding; grain that jumps is film.

44 — A glow that breathes

scale and two shadows on a long, soft curve  —  free

Five and a bit seconds per cycle, which is slow enough that nobody watches it and fast enough that the page does not feel dead. The second shadow expands and fades to nothing, so the pulse leaves rather than simply getting bigger and smaller.

Everything ambient on this page has a duration between five and thirty seconds. Under about three, an idle animation stops being atmosphere and starts being a notification.

45 — A field that never repeats

five blobs on coprime durations  —  free

Thirteen, seventeen, nineteen, twenty-three and twenty-nine seconds. Because no two share a factor, the arrangement does not come back around for well over an hour, and the eye never finds the loop. Give them all the same duration and the loop is obvious within one cycle.

Coprime, so it never lines up

The same reasoning applies to the aurora on the previous post and to any set of ambient loops. It is a rule worth writing down, because the instinct is to give them all the same duration.

Discipline

46 — Both settings at once

one central rule, honoured once  —  free

The right-hand panel ignores the switch at the top of the page and is permanently in the reduced-motion state. Everything is still there — the hover still responds, the content still arrives — it simply arrives rather than traveling. That is the standard being met, not a lesser version of the design.

Motion as designed

Free survey
Fixed price
Ten year guarantee

prefers-reduced-motion: reduce

Free survey
Fixed price
Ten year guarantee

Honoured once, centrally, rather than in ninety-one stylesheets each remembering to. That is the only version of this that survives contact with a catalog.

47 — The same journey, cheap and expensive

transform and opacity against left, width, filter and shadow  —  free

Both lanes travel the same distance over the same duration with the same easing. The top one changes only transform and opacity, which the compositor can do without the main thread. The bottom one changes geometry and filters, so every frame costs a layout and a repaint. Press the button to run sixty of each and watch the late-frame count.

transform + opacity
left / width / filter / shadow
frames per secondframes over 24ms

This is the specimen that decides what belongs in a vocabulary. An effect that can only be expressed in geometry is an effect that will be switched off on a phone.

What this set changes

State change was the real hole. Six specimens here animate a thing turning into another thing, and one of them — 01 — is not a new feature at all: it is the missing half of the accordion and tabs behaviors that already ship. That is the strongest candidate on any of the three posts, because it improves what we have rather than adding to it.

FLIP is one instrument, not four features. Filter, sort, add, remove and card-becomes-panel are all the same eleven lines: measure, let the DOM change, measure again, animate the difference away. It works in every browser, which is what makes it the floor under View Transitions rather than a fallback for them.

One scroll reader now serves five specimens. The sideways section, the condensing header, the reading bar and the color ramp all spend the same 0-to-1 fraction that the parallax on the first post proved. A sixth would need no new code. data-pspan is the only addition: it compresses the fraction into part of the runway, without which every scroll effect would have to be as long as its container.

Three specimens now use a linear() spring and three use @property. Both have earned a place in any vocabulary that ships — and named curves must be longhands, because an animation: shorthand silently resets the timing function.

Two of these are not effects. 46 shows both motion settings at once, because reduced motion is a version of the design rather than a lesser one. 47 puts a frame counter under the difference between animating transform and animating geometry — which is what decides whether an effect belongs in a vocabulary at all.