# CSS Scroll Reveal

> Native CSS scroll-driven reveals for crisp fade, slide, and scale entrances with accessible static fallbacks and no animation JavaScript.

Canonical: https://gsapvault.com/effects/css-scroll-reveal
Live demo: https://gsapvault.com/demos/css-scroll-reveal/index.html

| Property | Value |
|----------|-------|
| Type | effect |
| Tier | free |
| Price | Free |
| Difficulty | beginner |
| Plugins | Core GSAP only |
| Techniques | scroll-reveal, scroll-driven-animation, progressive-enhancement |
| Uses Lenis | No |

## Overview

A focused native CSS reveal system that binds opacity and transform entrances to an element's progress through the viewport. Five reusable classes cover fade, upward slide, side slides, and scale without using JavaScript for animation. The poster demo combines those primitives into one compact editorial cascade while unsupported and reduced-motion browsers receive the complete static composition.

## Features

- Five reusable reveal classes for fade, slide, and scale entrances
- Native view() timelines with no JavaScript animation dependency
- Per-element animation ranges through the --reveal-range custom property
- Compositor-friendly opacity and transform response channels
- CSS-only accent-rule growth for layered visual rhythm
- Complete static fallback when scroll timelines are unsupported
- Reduced-motion mode that removes the runway and shows the final state
- Responsive sequence with a shorter mobile scroll distance

## Use Cases

- Editorial landing pages with bold typographic entrances
- Culture and event sites that need lightweight scroll reveals
- Static sites where animation JavaScript is intentionally avoided
- Marketing sections that need progressive enhancement
- Mobile-first pages with a designed reduced-motion fallback

## Vibe-Code Ready Setup

This effect includes `START-HERE-AI.md`, a product-specific copy-paste setup prompt for Cursor, Claude Code, ChatGPT, GitHub Copilot, Windsurf, and other coding assistants. It tells the assistant to inspect the existing stack, integrate the supplied files, preserve the design, scope selectors, retain accessibility and responsive behaviour, add framework-appropriate GSAP cleanup, and report what it tested.

[How AI-assisted setup works](https://gsapvault.com/vibe-coding)

## How It Works

Each reusable class assigns keyframes to a native view() animation timeline, so the browser maps an element's viewport entry directly to opacity or transform progress. The --reveal-range custom property can move the start and finish points without changing the keyframes. In the demo, the same classes use the root scroll timeline to form a short three-panel cascade, while @supports and prefers-reduced-motion rules remove animation and expose the settled poster immediately.

## Documentation

Native CSS scroll-driven entrance classes for opacity, translate, and scale. Animation requires no JavaScript; the optional script only adds an unsupported-browser fallback hook.

## Quick Start

**1. Copy the reveal classes and keyframes from `assets/style.css` into your stylesheet.**

**2. Add a reveal class to an element in your `<body>`:**

```html
<article class="reveal-slide-up">
  This fades and rises as it enters the viewport.
</article>
```

**3. Optionally tune that element's range in your CSS:**

```css
.feature-card {
  --reveal-range: entry 10% cover 35%;
}
```

No animation script or GSAP CDN tag is required.

## Options

| Class / property | Values | Default | Description |
|---|---|---|---|
| `.reveal-fade` | Class | — | Reveals with opacity only |
| `.reveal-slide-up` | Class | — | Fades and translates upward |
| `.reveal-scale` | Class | — | Fades and scales from `0.72` |
| `.reveal-slide-left` | Class | — | Fades while entering from the left |
| `.reveal-slide-right` | Class | — | Fades while entering from the right |
| `--reveal-range` | Any valid `animation-range` | `entry 0% cover 40%` | Controls where the reveal starts and settles |

The class names are preserved as the reusable API. The editorial demo binds its three panels to `scroll(root block)` only to make one compact, synchronized sequence; ordinary elements continue to use `view()`.

## Examples

### A three-item cascade

**Add to your HTML `<body>`:**

```html
<div class="story-grid">
  <article class="reveal-slide-up">First story</article>
  <article class="reveal-scale">Second story</article>
  <article class="reveal-slide-left">Third story</article>
</div>
```

**Add to your stylesheet:**

```css
.story-grid > :nth-child(1) { --reveal-range: entry 0% cover 35%; }
.story-grid > :nth-child(2) { --reveal-range: entry 8% cover 43%; }
.story-grid > :nth-child(3) { --reveal-range: entry 16% cover 51%; }
```

### Accent-rule growth

The poster's secondary response is also CSS-driven:

```css
.accent-rule {
  transform-origin: left;
  animation: rule-grow auto ease-out both;
  animation-timeline: view();
  animation-range: entry 10% cover 35%;
}

@keyframes rule-grow {
  from { opacity: 0; transform: scaleX(0); }
  to { opacity: 1; transform: scaleX(1); }
}
```

## Browser Support and Fallback

Native scroll-driven animations are supported in current Chromium-based browsers and Safari releases. Check [Can I Use](https://caniuse.com/css-scroll-driven-animations) for current versions.

The stylesheet includes an `@supports not (animation-timeline: view())` branch that removes animation and shows the final composition. `assets/script.js` is optional: it performs the same support check and adds `.no-scroll-timeline` for older browsers whose CSS feature detection is inconsistent. It never drives animation.

Without JavaScript, supported browsers animate normally and unsupported browsers receive the complete static content through CSS.

## Accessibility

A `prefers-reduced-motion: reduce` media query disables every reveal, removes the extra runway, and presents the settled content immediately. Keep meaningful content in normal HTML rather than pseudo-elements so it remains available to assistive technology and all fallback modes.

## Dependencies

None. The effect uses native CSS only. The demo loads Google Fonts for its poster styling, but the reveal API does not depend on them.

## Source Code

This effect is free. The complete source is included below.

### index.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>CSS Scroll Reveal Demo | GSAP Vault</title>
	<link rel="stylesheet" href="assets/style.css">
	<link rel="preconnect" href="https://fonts.googleapis.com">
	<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
	<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;600;700&family=Space+Grotesk:wght@500;600;700&family=Syne:wght@600;700;800&display=swap" rel="stylesheet">
</head>
<body>
	<main class="poster-runway">
		<section class="poster-stage" aria-label="Native CSS scroll reveal poster sequence">
			<header class="scroll-cue">
				<span class="scroll-cue__mark" aria-hidden="true"></span>
				<strong>Scroll / reveal</strong>
				<span>CSS only</span>
			</header>

			<div class="stage-index" aria-hidden="true">NO. 01</div>
			<div class="threshold" aria-hidden="true"><span>entry threshold</span></div>

			<div class="poster-stack" data-thumbnail-target>
				<article class="poster-panel poster-panel--lime reveal-slide-up">
					<div class="poster-panel__meta"><span>Independent voices</span><span>24—26</span></div>
					<p class="poster-panel__kicker">Night print / live type</p>
					<h1>MAKE<br>NOISE</h1>
					<div class="poster-rule" aria-hidden="true"></div>
					<p class="poster-panel__foot">Three nights. One city.<br>No quiet conclusions.</p>
				</article>

				<article class="poster-panel poster-panel--orange reveal-scale">
					<div class="poster-panel__meta"><span>Field notes</span><span>02:00</span></div>
					<p class="poster-panel__kicker">Culture after closing</p>
					<h2>STAY<br>OUT</h2>
					<div class="poster-rule" aria-hidden="true"></div>
					<p class="poster-panel__foot">Talks / sound / moving image<br>Doors refuse to close.</p>
				</article>

				<article class="poster-panel poster-panel--cyan reveal-slide-left">
					<div class="poster-panel__meta"><span>Open frequency</span><span>FM 88.6</span></div>
					<p class="poster-panel__kicker">Transmit the margins</p>
					<h2>NEW<br>WAVE</h2>
					<div class="poster-rule" aria-hidden="true"></div>
					<p class="poster-panel__foot">Broadcast live from nowhere<br>Signal over silence.</p>
				</article>
			</div>

			<p class="stage-note">Opacity / translate / scale</p>
		</section>
	</main>

	<script src="assets/script.js"></script>
</body>
</html>
```

### assets/style.css

```css
* {
	box-sizing: border-box;
	margin: 0;
	padding: 0;
}

:root {
	--black: #050505;
	--paper: #f4f1e8;
	--lime: #c8ff00;
	--orange: #ff5a1f;
	--cyan: #20d9ff;
	--muted: #969696;
	--line: rgba(244, 241, 232, 0.2);
	color-scheme: dark;
}

html {
	background: var(--black);
	scrollbar-color: var(--lime) #111;
	scrollbar-width: thin;
}

html::-webkit-scrollbar { width: 8px; }
html::-webkit-scrollbar-track { background: #111; }
html::-webkit-scrollbar-thumb { background: var(--lime); }

body {
	min-width: 320px;
	background:
		linear-gradient(var(--line) 1px, transparent 1px),
		linear-gradient(90deg, var(--line) 1px, transparent 1px),
		var(--black);
	background-size: 12.5vw 12.5vw;
	color: var(--paper);
	font-family: 'Space Grotesk', system-ui, sans-serif;
	line-height: 1.2;
	overflow-x: clip;
	-webkit-font-smoothing: antialiased;
}

::selection {
	background: var(--lime);
	color: var(--black);
}

.poster-runway {
	min-height: 164svh;
}

.poster-stage {
	position: sticky;
	top: 0;
	min-height: 100svh;
	overflow: hidden;
	isolation: isolate;
}

.poster-stage::before {
	content: '';
	position: absolute;
	inset: 0;
	background: radial-gradient(circle at 50% 46%, rgba(255, 255, 255, 0.055), transparent 48%);
	pointer-events: none;
}

.scroll-cue {
	position: absolute;
	top: clamp(1rem, 3vw, 2rem);
	left: clamp(1rem, 3vw, 2rem);
	display: flex;
	align-items: center;
	gap: 0.65rem;
	font-family: 'JetBrains Mono', monospace;
	font-size: 0.65rem;
	letter-spacing: 0.12em;
	text-transform: uppercase;
	z-index: 10;
}

.scroll-cue strong { color: var(--paper); font-weight: 600; }
.scroll-cue > span:last-child { color: var(--muted); }

.scroll-cue__mark {
	width: 0.5rem;
	height: 0.5rem;
	background: var(--lime);
	box-shadow: 0 0 0 4px rgba(200, 255, 0, 0.12);
	animation: cue-pulse 1.5s ease-in-out infinite;
}

.stage-index {
	position: absolute;
	top: clamp(1rem, 3vw, 2rem);
	right: clamp(1rem, 3vw, 2rem);
	font-family: 'Syne', sans-serif;
	font-size: clamp(1.1rem, 2vw, 1.65rem);
	font-weight: 800;
	letter-spacing: -0.04em;
}

.threshold {
	position: absolute;
	left: 0;
	right: 0;
	top: 73%;
	border-top: 1px dashed rgba(244, 241, 232, 0.32);
	z-index: 0;
}

.threshold span {
	position: absolute;
	right: clamp(1rem, 3vw, 2rem);
	top: -1.5rem;
	color: var(--muted);
	font-family: 'JetBrains Mono', monospace;
	font-size: 0.58rem;
	letter-spacing: 0.12em;
	text-transform: uppercase;
}

.poster-stack {
	position: absolute;
	left: 50%;
	top: 17%;
	width: min(74vw, 820px);
	height: min(66svh, 440px);
	transform: translateX(-50%);
	z-index: 2;
}

.poster-panel {
	position: absolute;
	width: 62%;
	height: 82%;
	padding: clamp(1rem, 2vw, 1.45rem);
	border: 1px solid rgba(5, 5, 5, 0.8);
	box-shadow: 12px 14px 0 rgba(0, 0, 0, 0.58);
	color: var(--black);
	display: flex;
	flex-direction: column;
	justify-content: space-between;
	transform-origin: 50% 100%;
}

.poster-panel--lime {
	left: 0;
	top: 0;
	background: var(--lime);
	z-index: 1;
	--demo-range: 3% 53%;
}

.poster-panel--orange {
	left: 18%;
	top: 9%;
	background: var(--orange);
	z-index: 2;
	--demo-range: 10% 60%;
}

.poster-panel--cyan {
	left: 36%;
	top: 18%;
	background: var(--cyan);
	z-index: 3;
	--demo-range: 17% 67%;
}

.poster-panel__meta,
.poster-panel__foot,
.poster-panel__kicker {
	font-family: 'JetBrains Mono', monospace;
	font-size: clamp(0.52rem, 0.85vw, 0.68rem);
	font-weight: 600;
	letter-spacing: 0.08em;
	text-transform: uppercase;
}

.poster-panel__meta {
	display: flex;
	justify-content: space-between;
	gap: 1rem;
	border-bottom: 1px solid currentColor;
	padding-bottom: 0.55rem;
}

.poster-panel__kicker { margin-top: auto; }

.poster-panel h1,
.poster-panel h2 {
	font-family: 'Syne', sans-serif;
	font-size: clamp(2.4rem, 6.2vw, 5.35rem);
	font-weight: 800;
	letter-spacing: -0.085em;
	line-height: 0.72;
	margin: 0.6rem 0 0.75rem;
}

.poster-rule {
	width: 100%;
	height: clamp(0.35rem, 0.8vw, 0.65rem);
	background: var(--black);
	transform-origin: left center;
	animation: rule-grow auto cubic-bezier(0.16, 1, 0.3, 1) both;
	animation-timeline: scroll(root block);
	animation-range: var(--demo-range);
}

.poster-panel__foot {
	align-self: flex-end;
	margin-top: 0.75rem;
	text-align: right;
}

.stage-note {
	position: absolute;
	left: clamp(1rem, 3vw, 2rem);
	bottom: clamp(1rem, 3vw, 2rem);
	color: var(--muted);
	font-family: 'JetBrains Mono', monospace;
	font-size: 0.6rem;
	letter-spacing: 0.13em;
	text-transform: uppercase;
}

@keyframes cue-pulse {
	50% { opacity: 0.35; transform: translateY(3px); }
}

/* Reusable API: set --reveal-range on an element to tune its viewport window. */
.reveal-fade,
.reveal-slide-up,
.reveal-scale,
.reveal-slide-left,
.reveal-slide-right {
	animation-duration: auto;
	animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
	animation-fill-mode: both;
	animation-timeline: view();
	animation-range: var(--reveal-range, entry 0% cover 40%);
	will-change: opacity, transform;
}

.reveal-fade { animation-name: fade-in; }
.reveal-slide-up { animation-name: slide-up; }
.reveal-scale { animation-name: scale-in; }
.reveal-slide-left { animation-name: slide-left; }
.reveal-slide-right { animation-name: slide-right; }

/* The showcase reuses the same classes against one short root-scroll sequence. */
.poster-panel.reveal-fade,
.poster-panel.reveal-slide-up,
.poster-panel.reveal-scale,
.poster-panel.reveal-slide-left,
.poster-panel.reveal-slide-right {
	animation-timeline: scroll(root block);
	animation-range: var(--demo-range);
}

@keyframes fade-in {
	from { opacity: 0; }
	to { opacity: 1; }
}

@keyframes slide-up {
	from { opacity: 0; transform: translateY(42svh); }
	to { opacity: 1; transform: translateY(0); }
}

@keyframes scale-in {
	from { opacity: 0; transform: translateY(26svh) scale(0.72); }
	to { opacity: 1; transform: translateY(0) scale(1); }
}

@keyframes slide-left {
	from { opacity: 0; transform: translate(-38vw, 18svh); }
	to { opacity: 1; transform: translate(0, 0); }
}

@keyframes slide-right {
	from { opacity: 0; transform: translate(38vw, 18svh); }
	to { opacity: 1; transform: translate(0, 0); }
}

@keyframes rule-grow {
	from { opacity: 0; transform: scaleX(0); }
	to { opacity: 1; transform: scaleX(1); }
}

@media (max-width: 700px) {
	body { background-size: 25vw 25vw; }
	.poster-runway { min-height: 138svh; }
	.poster-stack {
		top: 18%;
		width: calc(100vw - 2rem);
		height: min(69svh, 560px);
	}
	.poster-panel {
		width: 84%;
		height: 74%;
		padding: 0.9rem;
		box-shadow: 7px 8px 0 rgba(0, 0, 0, 0.58);
	}
	.poster-panel--orange { left: 8%; top: 10%; }
	.poster-panel--cyan { left: 16%; top: 20%; }
	.poster-panel h1,
	.poster-panel h2 { font-size: clamp(2.65rem, 15vw, 4.2rem); }
	.threshold { top: 78%; }
	.stage-note { display: none; }
	.poster-panel--lime { --demo-range: 2% 48%; }
	.poster-panel--orange { --demo-range: 8% 56%; }
	.poster-panel--cyan { --demo-range: 14% 64%; }
}

@media (prefers-reduced-motion: reduce) {
	*, *::before, *::after {
		animation-duration: 0.01ms !important;
		animation-delay: 0ms !important;
		scroll-behavior: auto !important;
	}
	.poster-runway { min-height: 100svh; }
	.poster-panel,
	.poster-rule,
	.reveal-fade,
	.reveal-slide-up,
	.reveal-scale,
	.reveal-slide-left,
	.reveal-slide-right {
		animation: none !important;
		opacity: 1 !important;
		transform: none !important;
	}
	.scroll-cue__mark { animation: none; }
}

@supports not (animation-timeline: view()) {
	.poster-runway { min-height: 100svh; }
	.poster-panel,
	.poster-rule,
	.reveal-fade,
	.reveal-slide-up,
	.reveal-scale,
	.reveal-slide-left,
	.reveal-slide-right {
		animation: none;
		opacity: 1;
		transform: none;
	}
}

.no-scroll-timeline .poster-runway { min-height: 100svh; }
.no-scroll-timeline .poster-panel,
.no-scroll-timeline .poster-rule,
.no-scroll-timeline .reveal-fade,
.no-scroll-timeline .reveal-slide-up,
.no-scroll-timeline .reveal-scale,
.no-scroll-timeline .reveal-slide-left,
.no-scroll-timeline .reveal-slide-right {
	animation: none;
	opacity: 1;
	transform: none;
	will-change: auto;
}
```

### assets/script.js

```js
/**
 * CSS Scroll Reveal
 *
 * The reveals are entirely CSS-driven. This optional helper only exposes a
 * support hook so unsupported browsers can render the completed composition.
 */
(function onReady(init) {
	if (document.readyState === 'loading') {
		document.addEventListener('DOMContentLoaded', init);
	} else {
		init();
	}
})(function detectScrollTimelineSupport() {
	'use strict';

	if (!window.CSS || !CSS.supports('animation-timeline', 'view()')) {
		document.documentElement.classList.add('no-scroll-timeline');
	}
});
```

---

From [GSAP Vault](https://gsapvault.com): production-ready GSAP animation effects. Full catalog for agents: https://gsapvault.com/llms-full.txt
