ScrollFree

Scroll Fade Gallery

Five 16:9 blocks at 80% width inside a 280 px frame: each one starts at scale(.8) translateY(20px) and opacity 0 and reaches full size over its whole entry — animation-timeline: view(), animation-range: entry 0% entry 100%, zero JavaScript. Measured: a block shorter than the frame lands on scale(1) exactly when its bottom edge touches the bottom of the frame.

CSSview()Gallery

Updated

Scroll to see the effect
Hover or click the scene to interact

This effect is free — the Scroll category contains 70 effects total, including 12 free. Effect.Labs has 811 vanilla effects. Explore the category →

Usage examples

① Screenshots of an indie game — store page of a games platform

WhenA game's page on an online store: title, price, “Add to cart” button, then the 16:9 screenshots stacked vertically in the frame, each growing as it arrives.
WhyA game screenshot can only be judged whole: the entry 0% → entry 100% range makes final size and full visibility coincide, you never see a shrunken image sitting still. Scale In (fx-0536) ends at entry 50% and suits text cards, not visuals.
Settings344 px column, scroller at height: 100% (360 px), screenshots at the code's 80% (250 × 140 px), margin: 22px auto, 8 px corners, visuals drawn in CSS with a HUD, overscroll-behavior: contain. The first two screenshots are whole at rest, the third grows from the first pixel: measured 0 → 0.30 at 61 px → 0.80 at 131 px. The automatic scrolling is a demo device.
Cap Ouest Yachting · Brokerage

Albatros 38 — 2017

129 000 €Viewing by appointment
11.50 m · 3 cabins · 29 hp diesel · Lorient (56)

② Photos of a second-hand sailboat — a yacht broker's listing

WhenListing: boat name, year, price, then the 16:9 views (deck, cockpit, saloon, engine) one below the other, each with its caption.
WhyThe photos come straight from the broker's phone, untouched: the .8 → 1 scale of each view on entry stages them, each one reaching its full size with its caption, one below the other. Snap Gallery with Progress (fx-0574) would force one photo per screen and hide the captions.
Settings90 px header outside the frame, 270 px scroller (flex: 1), photos at the code's 80% (400 × 225 px): shorter than the frame, they land on scale(1) when their bottom edge touches the bottom of the frame; caption as a gradient strip inside the block (it follows its opacity), margin: 18px auto, light background. Measured on photo 2: 0 at rest, 0.22 at 68 px, 0.57 at 147 px, 1 at 252 px.

Move-in inspection report

12 rue des Lilas, Caen · 2 rooms, 46 m² · signed on 3 September 2026, 14 photos.

The tenant reads the report in the app: one photo per room, the reservations are marked in orange and circled in red.
9:41
Inspection · Move-in12 rue des Lilas · 3 Sept. 2026
PhotosMetersKeys

③ Inspection report photos — a real estate agency's mobile app

WhenMove-in inspection report read by the tenant in the app: room by room, one 16:9 photo and a note (scratch, mark), inside a scrolling frame.
WhyAn inspection photo is evidence: the tenant looks for the red circle in it, and the parquet boards or tile joints must stay straight. The .8 → 1 growth around the center keeps each photo in place while it sharpens — measured at the start of the entry: the photo's center is 16 px too low, the red circle 8 and 9 px from its final spot on photo 3, 13 and 19 px on photo 4 where it sits in a corner. Fade Up (fx-0535) would lift the photo, mark included, by 60 px; Rotate In (fx-0542) would tilt it 15° before straightening it, a leaning tiled wall on a document both parties sign.
Settings236 px screen, 258 px scroller below the app bar and the tabs, photos at width: 100% (212 × 119 px, the only departure from the code's 80%: a mobile app shows its photos full width), margin: 0 0 7px, hidden scrollbar, a red pin on each reservation, the note as text below the block. Measured on photo 2: 0.09 at rest, 0.54 at 54 px, 1 at 115 px.

How it works

The sold HTML is nine elements: the frame .demo-preview (centered flex, height: 280px, overflow: hidden, position: relative, #0a0a0f background, white text, sans-serif), the scroller .fade-gallery-container (100% × 100%, overflow-y: auto, overflow-x: hidden, padding: 20px 12px), two 60 px .scroll-spacers around five empty .fade-gallery-items — width: 80%, margin: 24px auto, aspect-ratio: 16/9, border-radius: 10px, five 135° gradients from pink to violet set by :nth-child(2) to (6) (child 1 of the scroller is the spacer) — and the hint .slr-scroll-hint “Scroll to see the effect”, absolute 8 px above the bottom of the frame, 11.2 px, white at 40%, bouncing 4 px every 2 s (hintBounce). The JavaScript field is empty. Measured in Chromium 151, blank 1,280 px page with a classic scrollbar: frame 1,264 × 280, blocks 992 × 558 px, 582 px pitch (the 24 px margins collapse), 2,814 px scroll range; at 390 px: blocks 280 × 158, range 812.

The motion comes from three declarations on the block. animation: fadeScaleIn linear both names the keyframes (from: opacity 0, scale(0.8) translateY(20px); to: opacity 1, scale(1) translateY(0)), with no duration, a linear progression, and a fill that holds the start state before the range and the end state after it. animation-timeline: view() replaces the clock with the block's position inside the nearest scroll container — here .fade-gallery-container, not the page: an anonymous view timeline, nothing to name or declare on the scroller. animation-range: entry 0% entry 100% bounds the travel: 0% when the block's top edge crosses the bottom of the frame, 100% when the block has finished entering. Since the progression is linear, opacity equals the entry fraction, scale is 0.8 + 0.2 × fraction, and the translateY(20px), written after scale(0.8) and therefore multiplied by it, is 16 px on screen at the start. Measured at 390 px on block 2 (top at 286 px from the start of the scroller, 280 px frame): 0.346 and scale(0.869) after 60 px of wheel, 0.727 / 0.945 at 120, 1 / 1 at 180 — the range is 158 px, the block's height; at mid-range, 0.502, scale(0.9) and a 9 px offset. The block is sharp and whole at the same instant.

“Finished entering” means two things depending on the block's size. Shorter than the frame, it has finished entering when its bottom edge touches the bottom of the frame: the range is its height. Taller than the frame, it can never fit whole: the range ends when it fills the frame, top edge against top edge, and is then the frame's height. That is the sold demo on a wide screen — a 16:9 block exceeds 280 px as soon as it is wider than 498 px, i.e. a frame of about 650 px (measured: 279 px tall at 660 px with a classic scrollbar, 297 at 700). At 1,280 px, block 3 goes from 0 at scrollTop 988 to 0.996 at 1,267: 280 px of travel, then it stays at 1 while its remaining 278 px scroll by. Everything is reversible, the timeline is a position, not a clock: scrolling back up 100 px at a time brings block 3 from 0.757 to 0.4, 0.043, then 0; a block that leaves through the top stays at 1 (both).

Accessibility

  • prefers-reduced-motion not in the code: measured under emulation, nothing changes — the blocks start at 0 and grow on entry, the hint bounces (hintBounce measured running). The growth is driven by the reader (WCAG 2.3.3 targets unsolicited motion) and hides nothing more than usual, but a block growing under the finger is still motion: add @media (prefers-reduced-motion: reduce) { .fade-gallery-item { animation: none; opacity: 1; transform: none } .slr-scroll-hint { animation: none } } — a flat gallery, as in Firefox.
  • Keyboard: measured, Tab focuses the scroller without a tabindex in Chromium 151 as in Firefox 153 (activeElement = .fade-gallery-container, outline: auto 1px ring); Arrow Down moves 40 px (Chromium) or 51 (Firefox), PageDown 285 / 303, End goes to the end (2,814). Safari does not focus a scrolling box without tabindex="0": add it, with an aria-label (“Five visuals, vertical scrolling”) and a :focus-visible style.
  • Screen readers and images: the five blocks are empty divs with gradient backgrounds, nothing to announce. Opacity 0 is visual only: a block that has not entered yet stays in the accessibility tree and in reading order, no content is hidden from a screen reader. If you put <img>s in them, give them an alt; a caption placed inside the block follows its opacity (unreadable until it has entered) — place it below the block if it must be read first.
  • Hint and scroll chaining: “Scroll to see the effect” is hard-coded text, at 11.2 px and white at 40% — rendered rgb(108, 108, 111) on #0a0a0f, measured 3.77:1, below the 4.5:1 expected at that size; put aria-hidden="true" on it, it is decorative, or remove the span. With a finger, the gesture must start inside the frame; at the end of the range, the wheel drags the page along (measured in Chromium: 200 px of page after the last block) — overscroll-behavior: contain on the scroller prevents it.

Browser compatibility

CSS only: animation-timeline: view() and animation-range (scroll-driven animations), aspect-ratio, overflow-y, linear-gradient, @keyframes. Zero JavaScript, zero dependencies, no site CSS variable. Chrome and Edge 115+, Safari and iOS 26+; Firefox: no version — measured in Firefox 153, CSS.supports('animation-timeline: view()') returns false and the gallery renders flat.

Chrome 115+✓ Full
Firefox (all versions)✓ Flat gallery: five opaque blocks at full size, no fade, no growth (measured Firefox 153)
Safari 26+✓ Full
Edge 115+✓ Full
Mobile iOS 26+✓ Full (finger scrolling inside the frame)
Android Chrome 115+✓ Full

Without scroll-driven animations (Firefox, Chrome before 115, Safari before 26), animation-timeline and animation-range are dropped; the shorthand animation: fadeScaleIn linear both has no duration, hence 0 s, and the both fill applies the end state: five blocks at opacity 1 and scale(1), the frame scrolls normally (measured Firefox 153: 2,814 px range, identical, every block visible). Nothing is hidden, which is why the code has no @supports not (animation-timeline: view()) block — the end state is already the readable one. Without aspect-ratio (Chrome before 88, Safari before 15, Firefox before 89), an empty div has no height: the gallery vanishes unless the blocks get a height or some content.

The code

Copy the three blocks into your page. No dependencies.

HTML
<div class="fade-gallery-container">
    <div class="scroll-spacer"></div>
    <div class="fade-gallery-item"></div>
    <div class="fade-gallery-item"></div>
    <div class="fade-gallery-item"></div>
    <div class="fade-gallery-item"></div>
    <div class="fade-gallery-item"></div>
    <div class="scroll-spacer"></div>
  </div>
  <span class="slr-scroll-hint">Scroll to see the effect</span>
CSS
.slr-scroll-hint {
  position: absolute;
  bottom: 8px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 0.7rem;
  color: rgba(255, 255, 255, 0.4);
  pointer-events: none;
  z-index: 10;
  animation: hintBounce 2s ease-in-out infinite;
}

.scroll-spacer {
  height: 60px;
}

.fade-gallery-container {
  width: 100%;
  height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 20px 12px;
}

.fade-gallery-item {
  width: 80%;
  margin: 24px auto;
  aspect-ratio: 16/9;
  border-radius: 10px;
  opacity: 0;
  transform: scale(0.8) translateY(20px);
  animation: fadeScaleIn linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}

/* Les fonds comptent les enfants du défileur : le premier enfant est l'espaceur,
   les cinq blocs sont donc les enfants 2 à 6. */
.fade-gallery-item:nth-child(2) {
  background: linear-gradient(135deg, #ec4899, #f43f5e);
}

.fade-gallery-item:nth-child(3) {
  background: linear-gradient(135deg, #06b6d4, #3b82f6);
}

.fade-gallery-item:nth-child(4) {
  background: linear-gradient(135deg, #10b981, #059669);
}

.fade-gallery-item:nth-child(5) {
  background: linear-gradient(135deg, #f59e0b, #ea580c);
}

.fade-gallery-item:nth-child(6) {
  background: linear-gradient(135deg, #6366f1, #8b5cf6);
}

@keyframes hintBounce {

  0%,
  100% {
    transform: translateX(-50%) translateY(0);
  }

  50% {
    transform: translateX(-50%) translateY(-4px);
  }
}

@keyframes fadeScaleIn {
  from {
    opacity: 0;
    transform: scale(0.8) translateY(20px);
  }

  to {
    opacity: 1;
    transform: scale(1) translateY(0);
  }
}
JavaScript (fx-0566)

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
animation-range (CSS — .fade-gallery-item) entry 0% entry 100% The block reaches scale(1) exactly when it has finished entering. entry 0% entry 50%: done at mid-entry, the lower half still below the frame (Scale In, fx-0536). entry 0% cover 50%: keeps growing until the block's center reaches the frame's, i.e. after it is whole (View Timeline Scale, fx-0546). entry 20% entry 80% tightens the travel around the middle of the entry.
fadeScaleIn keyframes (CSS — @keyframes) opacity 0 → 1, scale(0.8) → scale(1), translateY(20px) → 0 The starting scale sets the amplitude: scale(0.9) for a discreet growth, scale(0.6) for a pop. The translateY is multiplied by the scale (16 px rendered at the start): write it before the scale for an exact 20 px, or remove it for a pure zoom. Keep the from identical to the block's resting style, otherwise it jumps before entering the range.
Block width and ratio (CSS — width: 80%, aspect-ratio: 16/9) 80% of the scroller, 16:9 (992 × 558 px at 1,280, 280 × 158 at 390) The height follows, and with it the length of the range: shorter than the frame, the block lands on scale(1) whole; taller, it finishes when it fills the frame. In a 280 px frame, a 16:9 block at 80% stays shorter up to ~650 px of width; 4/3 or 1/1 lower that threshold, width: 60% raises it.
Spacing (CSS — margin: 24px auto, .scroll-spacer { height: 60px }) 24 px between blocks (collapsed margins), 60 px before and after The space before the first block decides its state at rest (measured 0.629 at 1,280 px): a first spacer at least as tall as the frame makes it start from 0. The margins set how many blocks are in transition at once: at 390 px, a 182 px pitch, never more than one.
Block backgrounds (CSS — :nth-child(2) to (6), HTML) 5 gradients at 135°, pink to violet The selector counts the scroller's children, spacer included: the blocks are children 2 to 6. A sixth block (child 7) has no background, hence nothing to see on the dark frame: add its rule, or replace the five with a single color or with <img>s (see the last question).
Progression (CSS — linear in the animation shorthand) linear With a scroll timeline, the timing function warps the position → state mapping, not time: ease-out grows fast at the start of the entry and slowly at the end, ease-in the opposite. linear is the only setting where opacity equals the entry fraction exactly (measured 0.502 at mid-range).
“Scroll to see the effect” hint (CSS — .slr-scroll-hint, hintBounce) 8 px from the bottom of the frame, 11.2 px, white at 40%, 4 px bounce every 2 s Anchored to the .demo-preview frame (position: relative, measured offsetParent): without that frame, it drops to the bottom of the page. Hard-coded text. To remove it, delete the span; the rule and the @keyframes hintBounce go with it.

FAQ

Why is the first block already half visible before any scrolling on a large screen?

Because its range started before the resting position. The entry range begins when the block's top edge is at the bottom of the frame; the first block's top edge sits 104 px from the start (20 px of padding, 60 px of spacer, 24 px of margin), i.e. 176 px above that line in a 280 px frame. At 1,280 px the block is 558 px tall, so its range is 280 px and 176 px are already used: measured 0.629 and scale(0.926) before any gesture. At 390 px the block is 158 px tall: 104 + 158 = 262 < 280, it is whole and its range is over, measured 1. To have it always start from 0, give the first .scroll-spacer at least the frame's height, or accept a first block that is sharp from the start — in a real page, that is the fate of images above the fold.

How does it differ from Scale In (fx-0536), View Timeline Scale (fx-0546) and Scale on Scroll (fx-0558)?

The technique is the same in all four: animation-timeline: view(), keyframes leading to scale(1) and opacity 1, a both fill, no JavaScript, a 280 px scroller. The three neighbours animate 260 px text cards (heading, paragraph, translucent white background) from scale(.7), with no rise. Scale In and Scale on Scroll use animation-range: entry entry 50%: the card reaches its size at mid-entry, when its lower half is still below the frame — fine for text whose heading sits at the top, not for a visual judged as a whole. View Timeline Scale uses entry cover 50%: the card keeps growing until its center reaches the frame's, i.e. after it is whole. Scroll Fade Gallery bounds the range to entry 0% entry 100% on 16:9 blocks at 80% width, with scale(.8) and a 20 px rise: final size and full visibility coincide, and it is the only free one of the four. All four render flat in Firefox.

How do I use it in a real page, with photos, without the 280 px frame?

Remove .demo-preview and the scroller's overflow: view() takes the nearest scroll container, failing that the viewport, and nothing else changes — keep the .fade-gallery-item rule as is. Put an <img> in each block with display: block; width: 100%; height: 100%; object-fit: cover and overflow: hidden on the block to keep the corners; the aspect-ratio: 16/9 reserves the height before loading, so the timeline does not jump when the image arrives. In a 1,200 px column on a 900 px viewport, an image at 80% is 960 × 540: shorter than the viewport, it finishes entering when its bottom edge touches the bottom of the screen, 540 px of wheel; on a 390 px phone, 292 × 164 px, 164 px of thumb. Images already on screen at load are sharp from the start.