Pixelated Image Reveal
A card that transitions between two images using a randomized pixel grid — squares blink on in a staggered scatter, briefly expose the underlying image, then scatter off to reveal the new state. On touch devices the toggle fires on tap instead of hover.
gsaphoverpixelimagerevealgridtransition
Setup — External Scripts
CDN — GSAP (add before </body>)
html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>Code
HTML
html
<section class="cloneable">
<div data-hover="" data-pixelated-image-reveal="" class="pixelated-image-card">
<div class="before__100"></div>
<div class="pixelated-image-card__default">
<img src="https://cdn.prod.website-files.com/6712ad33825977f9d2f1ba2c/6714d43a777a77da89a9b5ec_osmo-pixelated-image-1.jpg" width="400" alt="" class="pixelated-image-card__img">
</div>
<div data-pixelated-image-reveal-active="" class="pixelated-image-card__active">
<img src="https://cdn.prod.website-files.com/6712ad33825977f9d2f1ba2c/6714d43a4d1abab1b3c81caf_osmo-pixelated-image-2.jpg" width="400" alt="" class="pixelated-image-card__img">
</div>
<div data-pixelated-image-reveal-grid="" class="pixelated-image-card__pixels">
<div class="pixelated-image-card__pixel"></div>
</div>
</div>
</section>CSS
css
.cloneable {
padding: var(--container-padding);
justify-content: center;
align-items: center;
min-height: 100svh;
display: flex;
position: relative;
}
.pixelated-image-card {
background-color: var(--color-neutral-800);
color: var(--color-primary);
border-radius: .5em;
width: 30vw;
max-width: 100%;
position: relative;
overflow: hidden;
}
.before__100 {
padding-top: 100%;
}
.pixelated-image-card__default,
.pixelated-image-card__img,
.pixelated-image-card__active,
.pixelated-image-card__pixels {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
.pixelated-image-card__active {
display: none;
}
.pixelated-image-card__pixel {
background-color: currentColor;
width: 100%;
height: 100%;
display: none;
position: absolute;
}JavaScript
javascript
function initPixelatedImageReveal() {
const animationStepDuration = 0.3;
const gridSize = 7;
const pixelSize = 100 / gridSize;
const cards = document.querySelectorAll('[data-pixelated-image-reveal]');
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || window.matchMedia("(pointer: coarse)").matches;
cards.forEach((card) => {
const pixelGrid = card.querySelector('[data-pixelated-image-reveal-grid]');
const activeCard = card.querySelector('[data-pixelated-image-reveal-active]');
const existingPixels = pixelGrid.querySelectorAll('.pixelated-image-card__pixel');
existingPixels.forEach(pixel => pixel.remove());
for (let row = 0; row < gridSize; row++) {
for (let col = 0; col < gridSize; col++) {
const pixel = document.createElement('div');
pixel.classList.add('pixelated-image-card__pixel');
pixel.style.width = `${pixelSize}%`;
pixel.style.height = `${pixelSize}%`;
pixel.style.left = `${col * pixelSize}%`;
pixel.style.top = `${row * pixelSize}%`;
pixelGrid.appendChild(pixel);
}
}
const pixels = pixelGrid.querySelectorAll('.pixelated-image-card__pixel');
const totalPixels = pixels.length;
const staggerDuration = animationStepDuration / totalPixels;
let isActive = false;
let delayedCall;
const animatePixels = (activate) => {
isActive = activate;
gsap.killTweensOf(pixels);
if (delayedCall) delayedCall.kill();
gsap.set(pixels, { display: 'none' });
gsap.to(pixels, {
display: 'block',
duration: 0,
stagger: { each: staggerDuration, from: 'random' }
});
delayedCall = gsap.delayedCall(animationStepDuration, () => {
activeCard.style.display = activate ? 'block' : 'none';
activeCard.style.pointerEvents = activate ? 'none' : '';
});
gsap.to(pixels, {
display: 'none',
duration: 0,
delay: animationStepDuration,
stagger: { each: staggerDuration, from: 'random' }
});
};
if (isTouchDevice) {
card.addEventListener('click', () => animatePixels(!isActive));
} else {
card.addEventListener('mouseenter', () => {
if (!isActive) animatePixels(true);
});
card.addEventListener('mouseleave', () => {
if (isActive) animatePixels(false);
});
}
});
}
// Initialize Pixelated Image Reveal
document.addEventListener('DOMContentLoaded', () => {
initPixelatedImageReveal();
});Attributes
| Name | Type | Default | Description |
|---|---|---|---|
| [data-pixelated-image-reveal] | attribute | — | The card container. The script attaches mouseenter/mouseleave (or click on touch) events here and reads the grid and active child elements from within it. |
| [data-pixelated-image-reveal-active] | attribute | — | The alternate state element shown after the pixel transition completes. Hidden by default (display: none) and toggled by the script at the midpoint of the animation. |
| [data-pixelated-image-reveal-grid] | attribute | — | The pixel overlay container. Any existing .pixelated-image-card__pixel children are cleared on init and replaced with a fresh gridSize × gridSize set of absolutely-positioned pixel divs. |
| animationStepDuration | number | 0.3 | Total duration in seconds for the pixel-on or pixel-off phase. Adjust in the script to make the reveal faster or slower. |
| gridSize | number | 7 | Number of rows and columns in the pixel grid (gridSize × gridSize total pixels). Increase for smaller, more numerous pixels; decrease for larger, blockier ones. |
Notes
- •Requires GSAP loaded via CDN before the script runs.
- •On touch devices (detected via ontouchstart, maxTouchPoints, and pointer: coarse), the animation toggles on each tap instead of hover enter/leave.
- •The pixel grid is rebuilt from scratch on every initPixelatedImageReveal() call — existing .pixelated-image-card__pixel elements inside the grid are removed first.
- •gsap.killTweensOf(pixels) and delayedCall.kill() ensure rapid re-triggers (fast hover in/out) never leave the card in an inconsistent state.
- •The active image is shown/hidden at exactly the animationStepDuration midpoint using gsap.delayedCall, so the swap is always hidden behind the pixel overlay.
- •Multiple [data-pixelated-image-reveal] cards on the same page are fully supported — each gets its own pixel grid, isActive flag, and event listeners.
Guide
Duration
Adjust animationStepDuration to control how fast the pixel scatter plays. Lower values create a snappier flash; higher values give a slower, more deliberate reveal.
const animationStepDuration = 0.3; // secondsPixel size
Change gridSize to increase or decrease the number of pixels in the grid. The pixel size is derived automatically: pixelSize = 100 / gridSize (as a percentage of the card).
const gridSize = 7; // 7×7 = 49 pixels