Scroll Crossword Fill Reveal — Free HTML CSS JS Snippet
Scroll Crossword Fill Reveal · Scroll · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="hint">Scroll ↓ to fill in the puzzle</div>
<section class="puzzle-section">
<div class="grid" id="grid">
<div class="cell" data-n="1"><span class="num">1</span><span class="letter">S</span></div>
<div class="cell" data-n="2"><span class="letter">C</span></div>
<div class="cell" data-n="3"><span class="letter">R</span></div>
<div class="cell" data-n="4"><span class="letter">O</span></div>
<div class="cell" data-n="5"><span class="letter">L</span></div>
<div class="cell" data-n="6"><span class="letter">L</span></div>
<div class="cell blank"></div>
<div class="cell" data-n="7"><span class="num">2</span><span class="letter">G</span></div>
<div class="cell" data-n="8"><span class="letter">S</span></div>
<div class="cell" data-n="9"><span class="letter">A</span></div>
<div class="cell" data-n="10"><span class="letter">P</span></div>
<div class="cell blank"></div>
<div class="cell" data-n="11"><span class="num">3</span><span class="letter">T</span></div>
<div class="cell" data-n="12"><span class="letter">R</span></div>
<div class="cell" data-n="13"><span class="letter">I</span></div>
<div class="cell" data-n="14"><span class="letter">G</span></div>
<div class="cell" data-n="15"><span class="letter">G</span></div>
<div class="cell" data-n="16"><span class="letter">E</span></div>
<div class="cell" data-n="17"><span class="letter">R</span></div>
</div>
<div class="clues">
<p><strong>1 across</strong> — moves a page vertically</p>
<p><strong>2 across</strong> — the space between two points</p>
<p><strong>3 across</strong> — what starts an animation</p>
</div>
</section>* { box-sizing: border-box; }
body { margin: 0; font-family: 'Courier New', monospace; background: #f4f1e8; color: #1c2b4a; }
.hint { position: sticky; top: 12px; text-align: center; font-size: 13px; letter-spacing: 0.05em; color: #6b7a99; z-index: 4; padding: 10px; }
.puzzle-section { min-height: 130vh; display: flex; flex-direction: column; align-items: center; padding: 8vh 20px 20vh; gap: 32px; }
.grid { display: grid; grid-template-columns: repeat(6, 44px); grid-auto-rows: 44px; gap: 3px; background: #1c2b4a; padding: 3px; border-radius: 4px; box-shadow: 0 8px 24px rgba(28,43,74,0.25); }
.cell { position: relative; background: #fff; display: flex; align-items: center; justify-content: center; opacity: 0; transform: scale(0.5); }
.cell.blank { background: #1c2b4a; opacity: 1; }
.num { position: absolute; top: 2px; left: 4px; font-size: 9px; color: #6b7a99; }
.letter { font-size: 20px; font-weight: 700; color: #1c2b4a; }
.cell.filled { background: #dbe8ff; }
.clues { max-width: 340px; font-size: 14px; line-height: 1.9; color: #3a4a6a; }
.clues strong { color: #1c2b4a; }gsap.registerPlugin(ScrollTrigger);
var cells = gsap.utils.toArray('.cell:not(.blank)');
var tl = gsap.timeline({
scrollTrigger: {
trigger: '.puzzle-section',
start: 'top 75%',
end: 'top 5%',
scrub: 0.4,
},
});
cells.forEach(function (cell, i) {
var pos = i * 0.06;
tl.fromTo(cell,
{ opacity: 0, scale: 0.5 },
{ opacity: 1, scale: 1, duration: 0.3, ease: 'back.out(2)' },
pos
).to(cell, { backgroundColor: '#dbe8ff', duration: 0.15 }, pos)
.to(cell, { backgroundColor: '#ffffff', duration: 0.25 }, pos + 0.2);
});Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Scroll Crossword Fill Reveal — Staggered Grid Reveal with a Scrubbed Timeline

A puzzle-style scroll reveal where a CSS grid of crossword cells fills in one at a time as the section scrolls into view, each cell popping into place and briefly flashing a highlight color like a pen just wrote it in. Pair with Scroll Text Clip Reveal for a complementary letter-driven effect, or Scroll Reveal Grid patterns for a simpler staggered reveal.
A CSS grid, not a canvas
The puzzle is a plain display: grid of .cell divs with grid-template-columns: repeat(6, 44px). Filled black squares between words are separate .cell.blank divs that stay opaque from the start — no JavaScript needed to distinguish playable cells from blocked ones, it's a CSS class.
One scrubbed timeline, one tween per cell
gsap.utils.toArray('.cell:not(.blank)') collects every playable cell in document order. A single timeline loops over them, giving cell i a start position of i * 0.06 on the shared timeline. Each cell gets a fromTo popping it from scale(0.5)/opacity:0 to full size with a back.out(2) ease for a slight overshoot, like a letter snapping into its box.
The "just filled in" flash
Right after each cell's pop-in tween, two more tweens at the same and nearby timeline positions animate backgroundColor from white to a pale blue and back to white — a quick highlight flash that reads as "this cell was just written," independent of the scale/opacity pop.
Why start/end target only the grid's entrance
The ScrollTrigger's start: 'top 75%' / end: 'top 5%' window is deliberately short — just the distance it takes the grid to scroll from mostly-below-viewport to near the top. Because it's all one scrubbed timeline, scrolling back down below the start point resets every cell back to invisible/unscaled, and scrolling past the end point leaves the completed grid fully filled.
Reversibility for free
Because every letter's appearance is driven by scroll-mapped timeline position rather than a one-shot trigger, scrolling up "erases" the puzzle in the same staggered order it filled in, which is what makes this feel fundamentally different from a fire-once IntersectionObserver reveal.
Step by step
How to Use
- 1Scroll the grid into viewScroll down until the crossword grid enters the upper part of the viewport — cells pop in one by one with a brief blue highlight flash.
- 2Scroll back upThe grid empties out in the same staggered order, confirming the effect is fully reversible.
- 3Change the puzzle contentEdit the letters inside each .letter span and the data-n clue numbers in the HTML panel to spell a different word or phrase.
- 4Adjust the grid shapeChange grid-template-columns (currently repeat(6, 44px)) in the CSS panel to make a wider, narrower, or differently-sized grid.
- 5Tune the stagger speedChange the 0.06 multiplier in pos = i * 0.06 in the JS panel to make cells fill in faster or slower relative to each other.
- 6Export in your formatClick "HTML" for a standalone file, "JSX" for React, or "Tailwind" for a React + Tailwind version.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Blank squares use an extra .blank CSS class that sets opacity: 1 and a dark background from the start. The JS collects animated cells with gsap.utils.toArray(".cell:not(.blank)"), so blanks are simply skipped by the query selector.
They are two separate tweens placed at the same (or a slightly offset) timeline position: one animates scale/opacity for the "appearing" motion, another animates backgroundColor for the "just written in" highlight. Layering simple tweens like this is easier to tune than one complex keyframe animation.
Every cell's appearance is a tween inside one scrubbed timeline mapped to a specific scroll range. Scrubbing the timeline backward runs those tweens in reverse, so cells shrink back to invisible in the same order they appeared.
Yes — add more .cell divs to the grid and increase grid-template-columns' repeat count in the CSS. The JS automatically staggers however many non-blank cells exist, no changes needed there.
The reveal is meant to complete quickly as the grid scrolls into the upper viewport, similar to a staggered card-grid reveal — pinning isn't needed because the grid doesn't need to hold the viewport's attention beyond its own entrance.