Back to Gallery

CSS Scroll Reveal

FREE

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

beginner
3 more details
scroll-revealscroll-driven-animationprogressive-enhancement

About This Effect

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.

What's Included

8 items
  • 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

Perfect For

5 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

How It Works

1 section

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.

Difficulty Beginner
Includes HTML + JS + CSS source, documentation, AI setup prompt, lifetime updates
<!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>
/**
 * 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');
	}
});
!function(n){"loading"===document.readyState?document.addEventListener("DOMContentLoaded",n):n()}(function(){"use strict";window.CSS&&CSS.supports("animation-timeline","view()")||document.documentElement.classList.add("no-scroll-timeline")});
* {
	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;
}

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>:

<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:

.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>:

<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:

.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:

.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 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.

Your Cart

Your cart is empty

Browse Effects