41 named effects · every one live on this page
You cannot ask for what you cannot name. This is one page, not a folder — 41 interface effects running side by side, each labelled with the term practitioners actually use, so the next brief can just say “pin and scrub” or “progressive blur”.
24 named effects
How far you have scrolled is how far the animation has played. The hairline across the top of this page is one.
also called scroll-linked animation · page progress
The animation's progress is the scroll container's progress. Zero JavaScript, and it runs off the main thread — so it cannot jank even while the main thread is busy.
animation-timeline: scroll(root);Every row times its own animation by its own trip across the screen, so no two are at the same point at the same moment.
also called view timeline · element-in-viewport progress
Each element measures its own passage through the viewport, so several different values exist at once. animation-range is the part most people miss — it decides which slice of that passage the animation occupies.
view-timeline-name: --plate;
animation-timeline: --plate;
animation-range: entry 0% cover 45%;The picture holds still while the page keeps moving, and your scrolling becomes the playhead. Scroll back up and it plays backwards.
also called sticky section · pinned stage · ScrollTrigger pin + scrub
Pin holds the stage still while the page keeps scrolling. Scrub is the separate idea that progress follows scroll position rather than elapsed time, which is why it reverses exactly. Most “cinematic” sites are these two words.
position: sticky; top: 0;
/* GSAP */ ScrollTrigger({ pin: true, scrub: 1 })The layers move at different rates, and the difference between the rates is the part you read as distance.
also called differential scroll · multiplane
The oldest trick here and the easiest to overdo. It reads as depth only when the rates differ by a little; large offsets read as broken layout. Built on a view progress timeline it costs no JavaScript.
@keyframes drift { from { transform: translateY(-10%) } to { transform: translateY(10%) } }
animation: drift linear both; animation-timeline: view();
/* each layer gets its own distance; the difference is the depth */You scroll down; the content travels sideways. A list you ranked becomes a row you compare across.
also called pinned horizontal scroll · sideways gallery
Changing the axis changes what the content means: a stack implies rank, a track implies comparison. Travel must be measured from real content — a track that barely overflows reads as a glitch, not a journey.
/* --travel is the track's overflow, measured from its content */
transform: translateX(calc(-1 * var(--travel)));Scrolling comes to rest on a whole item instead of halfway between two. It feels like turning pages.
also called snap points · scroll snapping
Two properties, and it inherits the platform's real scroll physics — momentum, rubber-banding, trackpad feel. Every JavaScript reimplementation is worse.
scroll-snap-type: y mandatory;
scroll-snap-align: start;Cards catch at the top one after another and pile over each other as you keep going.
also called card stack · sticky stack
One property on a list. Each item sticks at the same offset so later items slide over earlier ones and the stack builds itself. No library, no measurement, no scroll listener.
li { position: sticky; top: 76px; }It reads how fast you are scrolling rather than where you have got to. Throw the page and the picture leans, then settles.
also called skew on scroll · velocity distortion · momentum skew
Velocity is a genuinely separate signal from position. The asymmetry is the craft: it rises about six times faster than it falls, which is what makes it read as momentum rather than twitch.
v += (target - v) * (target > v ? 0.34 : 0.055);
transform: skewY(calc(var(--vel) * -4deg));velocity 0.00
The moment the bar catches at the top of its panel, it knows, and changes itself.
also called stuck detection · scroll-state container query · sticky-aware header
Until this shipped, every “shrink the header once it sticks” needed a scroll listener and a hand-picked pixel threshold that broke under zoom. Now the element is simply told what it is.
container-type: scroll-state;
@container scroll-state(stuck: top) { … }Whichever slide is parked at the centre knows it is the one you are looking at, and lights up.
also called snapped detection · active-slide styling · scroll-state(snapped)
The companion query. A carousel always needed to know which slide is current, and every hand-rolled version computed it from offsets and got the edges wrong. This is the browser answering the question itself.
scroll-snap-align: center;
@container scroll-state(snapped: block) { … }The dots under the strip are made by the strip itself, and follow wherever you are in it.
also called ::scroll-marker · CSS-only carousel dots · scroll-marker-group
The dots are generated by the list and track it for free — including keyboard traversal and focus order, which hand-built dot navigation almost never gets right.
scroll-marker-group: after;
li::scroll-marker { content: '' }
li::scroll-marker:target-current { … }The back and forward arrows come out of the row of items itself, and go grey when there is nothing left that way.
also called ::scroll-button() · generated prev/next · CSS-only controls
The same idea as the markers, for the arrows — and the :disabled state at each end arrives free, which is the part hand-rolled carousels forget. No JavaScript, and each one is a real button to a screen reader.
ul::scroll-button(left) { content: '←' }
ul::scroll-button(right) { content: '→' }
ul::scroll-button(*):disabled { opacity: .25 }A figure far down the panel drives the bar at the top of it. One element's passage moves something it sits nowhere near.
also called timeline-scope · remote timeline · cross-tree driving
A scroll timeline is normally visible only to the element's own descendants. timeline-scope lifts it onto a shared ancestor, so a bar at the top can be driven by a figure far below it — something that previously required JavaScript by definition.
timeline-scope: --plate;
/* on a child */ view-timeline-name: --plate;
/* on a sibling */ animation-timeline: --plate;The line draws itself as you arrive, as though someone were tracing it in front of you.
also called stroke-dashoffset scrub · line draw · SVG path reveal
The robustness trick is pathLength="1": it normalises any path to a length of one, so the same two lines of CSS draw any shape without measuring it in JavaScript first.
path { pathLength: 1; stroke-dasharray: 1; stroke-dashoffset: 1 }
animation-timeline: view();
animation-range: entry 5% cover 50%;Draws as you arrive
The sentence lights up a word at a time, and how much of it has arrived is how far you have come down the page.
also called read-along highlight · per-word reveal · text scrub
Each word owns a different slice of one timeline, so the sentence resolves left to right as you descend. The overlap is the entire craft: too little and it strobes word by word, too much and the sentence arrives all at once.
/* per word, i = its index */
animation-range: cover calc(var(--i) * 1.5%)
cover calc(var(--i) * 1.5% + 26%);Going down puts the bar away so you can read. Going back up brings it straight back.
also called auto-hide header · direction-aware nav · scroll-up reveal
Position says where you are; direction says what you want. Descending reads as “reading, leave me alone”, ascending as “looking for something”. A few pixels of dead zone are mandatory, or a trackpad makes it flicker.
dir = y > last ? "down" : "up";
[data-dir="down"] .bar { transform: translateY(-100%) }The next panel covers the one before it rather than pushing it out of the way.
also called panel slide-over · curtain reveal · section overlap
Two rules and nothing else. The outgoing panel stays pinned while the incoming one rides over it, so the change reads as depth rather than travel. Note what is absent: no animation, no timeline, no measurement.
.a { position: sticky; top: 0 }
.b { position: relative; z-index: 2 }Only the band across the middle of the screen is sharp. Everything above and below it falls out of focus.
also called focus falloff · viewport-centre focus · scroll blur
A camera metaphor applied to a list: attention is a narrow band, and everything outside it recedes. Keep the blur under about 6px — past that it stops reading as focus and starts reading as a rendering fault.
@keyframes focusband {
0%, 100% { filter: blur(4.5px); opacity: .34 }
46%, 54% { filter: blur(0); opacity: 1 }
}The list has no end. Keep going and it carries on, with no seam to catch on.
also called endless scroll · recycled list · seamless wrap
The correction must happen in the same frame as the move or the seam shows as a jump. Done right the list has no beginning and no end — and the scrollbar stops telling you where you are, which is the price.
el.append(el.firstElementChild);
el.scrollTop -= rowHeight; /* same frame, or it jumps */Grab it and throw it. It keeps going after you let go, then slows to a stop as though it had weight.
also called drag-to-scroll · inertial scrolling · momentum decay
Everything lives in the decay constant. 0.90 stops abruptly and feels cheap; 0.98 slides forever and feels broken. Around 0.94 is where it starts feeling like an object with mass.
v *= 0.94; /* per frame */
if (Math.abs(v) < 0.1) stop();The figures wait until they are on screen, then run up to their value — once, not every time you pass.
also called odometer · number roll · stat counter
The tabular figures matter more than the easing — proportional numerals change width as they count and the whole row twitches. It fires once: a counter that replays on every pass is a tell that nobody checked it twice.
font-variant-numeric: tabular-nums;
new IntersectionObserver(fn, { threshold: 0.6 })Whatever has reached eye level is the thing the index marks as where you are.
also called section tracking · active-anchor highlight · reading position
That rootMargin collapses the viewport to a thin band across its middle, so “current” means what is at eye level rather than what merely happens to be on screen. The index pinned to the top of this page is one.
new IntersectionObserver(fn, {
rootMargin: '-45% 0px -45% 0px'
})Push both panels past their last line. One hands your gesture on to the page behind it; the other keeps it.
also called scroll chaining · overscroll-behavior · gesture trapping
One property, and it is the whole difference between a panel that feels solid and one that feels like a leak. The chaining default is right for a document and wrong for almost every overlay.
overscroll-behavior: contain;default — chains
contain — absorbs
Flick both tracks hard. The top one skips three or four items in one go; the bottom one stops at the very next one.
also called scroll-snap-stop · no-skip snapping · mandatory stop
Use it when every item must actually be seen, and leave it off for browsing. It is the difference between a catalogue and a queue.
scroll-snap-stop: always;normal — a hard flick skips
always — one at a time
7 named effects
A small blurred stand-in appears at once and the real picture fades in over it — in a box that was already the right size, so nothing jumps.
also called low-quality image placeholder · progressive loading
Two jobs at once: something to look at immediately, and a reserved box so nothing shifts when the real file lands. Layout shift is a ranked Core Web Vital, so the aspect-ratio is not decoration.
figure { aspect-ratio: 3 / 2 } /* the box is reserved before the file lands */
.placeholder { filter: blur(18px); transform: scale(1.1) }
.full { opacity: 0; transition: opacity .9s } /* → 1 once loaded */A still photograph is given a slow drift and a slow push in — just enough to feel like time passing.
also called slow pan and zoom · documentary pan
Named for the documentarian who made a career of it. The rule is that the move must be slower than the viewer's attention — if they notice the motion starting, it is too fast.
@keyframes kb { to { transform: scale(1.14) translate(-2%, 1.5%) } }
animation: kb 18s ease-in-out infinite alternate;Any photograph, whatever it arrived wearing, comes out in one colour. Drag back to see what it came in as.
also called two-tone mapping · bichromate
A brand-colour treatment that survives any source image. Grayscale the base, then lay a single hue over it with mix-blend-mode.
background: var(--accent);
mix-blend-mode: color; /* over a grayscale base */The picture softens towards the bottom, so the caption sits on something readable without a black bar laid across it.
also called gradient blur · layered blur · tapered blur
Stacked layers, each blurring more than the last, each masked to a different band. Far more elegant than a scrim because the image stays legible while the text still reads.
backdrop-filter: blur(Npx);
mask-image: linear-gradient(to top, #000, transparent);Text sits on light
The picture is cut down to a handful of tones, and the ones that are missing are made up out of a regular grain. Drag to take more away.
also called Bayer dithering · halftone · posterize
Real pixel work — it needs crossorigin="anonymous" and a CORS-clean host, otherwise the canvas is tainted and getImageData throws. Wikimedia sends the header; most museum CDNs do not.
const BAYER8 = [...]; // 8×8 threshold matrix
v = v + (BAYER8[y%8][x%8]/64 - 0.5) * spread;The picture is uncovered by an edge travelling across it, not by fading up underneath.
also called mask reveal · wipe · shape reveal
A wipe has a direction and an edge; a fade has neither. That edge is what makes the reveal feel authored rather than merely delayed.
clip-path: inset(0 100% 0 0) → inset(0 0 0 0);
transition: clip-path .9s cubic-bezier(.16,.84,.24,1);Two versions of the same frame, one laid over the other. Drag the divider across to compare them.
also called before/after · juxtapose
One image clipped over another, the clip driven by pointer position. Keep both layers exactly registered or the comparison lies.
clip-path: inset(0 calc(100% - var(--x)) 0 0);5 named effects
The lines rise up out of the page one after another, each a beat behind the last.
also called staggered line reveal · line mask · SplitText
The mask is what matters: a wrapper with overflow:hidden and a translated inner. That makes text rise out of the page instead of fading in on top of it.
.line { overflow: hidden }
.line span { transform: translateY(110%) → none }Weight is a dial here, not a set of steps. Drag it and the letterforms thicken without ever swapping to another file.
also called axis interpolation · variable axes
A variable font exposes continuous axes rather than fixed cuts, so weight can be animated instead of swapped. One file, the whole family, and no flash between weights.
font-variation-settings: 'wght' 340;
transition: font-variation-settings .6s;Weight is a continuous axis
The picture shows only through the letters. The type is the window, not the caption.
also called knockout text · background-clip · image-in-type
The type becomes an aperture onto the image. It needs real weight to work — thin letterforms leave too little opening for the picture to read.
background-image: url(…);
background-clip: text;
color: transparent;Set down the page instead of across it, the way Chinese, Japanese and Korean have always been set. Each column is written downward, and they arrive right to left. Latin and figures turn sideways to follow — all but the year, held upright in a single square.
also called 竖排 · tate-gumi · vertical typesetting
Native in CSS and correct for CJK — the browser rotates Latin runs and places the punctuation itself. text-combine-upright is the piece almost nobody reaches for: it sets a two- to four-character number upright inside one em box, which is how a printed vertical page has always handled a date. This is the experimental lane: the writing here is material to look at, never the interface.
writing-mode: vertical-rl;
text-orientation: mixed;
.year { text-combine-upright: all } /* 縦中横 */滚动即时间轴
名は体を表す
이름이 곧 형태다
神坂雪佳 百々世草 1909
A band of text running sideways under its own power, whether or not you touch the page. Hover to stop it.
also called ticker · infinite scroller · running band
The one effect here that ignores scroll entirely — it gives a page a pulse when nothing else moves. Duplicate the content exactly once and travel -50%; that is what makes the loop seamless.
.row { width: max-content; animation: slide 26s linear infinite }
@keyframes slide { to { transform: translateX(-50%) } }
/* the content is duplicated once, so -50% lands exactly on the seam */5 named effects
Every colour on the card is held to one dial. Scroll, and they all turn together, in step, never landing on a combination nobody chose.
also called scroll-driven theme · animated custom properties · palette rotation
The whole palette is one number. @property is what makes it animatable at all — an unregistered custom property has no interpolable type. Every token keeps its lightness and chroma and shares the angle, so nothing can drift out of step.
@property --rot { syntax: '<angle>'; inherits: true }
--accent: oklch(0.637 0.145 calc(40.2deg + var(--rot)));Twelve plates, one angle.
OpenThe same colour change, asked of a few elements or of a great many. Press the switch and watch the counter beside it.
also called custom property invalidation · style recalc scope
Where the angle is declared decides what it costs. On the root, every element on the page re-resolves its colours each frame; on the stage, only the stage does. Scope is the whole difference between a palette that animates for free and one that eats the frame budget.
.stage { --rot: 0deg; animation: turn linear both; animation-timeline: scroll(root) }
/* declared on .stage, not :root — only this subtree re-resolves each frame */-- · 0 consumers
Two ways to get from terra to blue. The left field goes round the outside and is a full colour the whole way; the right one goes straight across and turns to mud halfway.
also called hue path · chroma preservation · colour interpolation
How much colour a screen can hold changes with hue: the space bulges towards violet and pinches near cyan, so it is not a cylinder. That makes the route between two colours a gamut question rather than a matter of taste — and the default route, a straight line through the middle of the space, is the one that passes closest to grey. The strips under each field are the whole journey at once.
/* round the outside */
background: oklch(0.637 0.148 calc(40.2deg + var(--t) * -135.8deg));
/* straight across — what you get by default */
background: color-mix(in srgb, <end> calc(var(--t) * 100%), <start>);along the rimfull colour the whole way
straight acrossgrey in the middle
The words turn themselves into the opposite of whatever passes behind them, so they stay readable over a photograph, over white and over black — and then vanish over one particular grey.
also called mix-blend-mode · invert on crossing · self-inverting overlay
One property, no JavaScript, and the overlay never has to know what is behind it. What it cannot survive is mid-grey, where inverting a value returns very nearly the value you started from. Plan for that band rather than discovering it on the one slide that has it.
mix-blend-mode: difference;Legible on anything
At one point on the way down, the whole panel changes its colours at once rather than easing between them.
also called section theming · discrete palette swap · data-theme
The common form, and the honest one when two palettes are unrelated rather than a rotation apart. Everything rests on the transition: without one it strobes, and much past a second it lags the scroll and reads as a bug.
new IntersectionObserver(fn, { rootMargin: '-45% 0px -45% 0px' })
.flip { transition: background-color .7s var(--settle) }
/* these three palettes are unrelated by design, so they are written out
rather than derived from the house tokens */