A mosaic grid transition effect for multi-page websites. Built with GSAP and data attributes. Works with Webstudio, static sites, and any HTML page.
Add GSAP to your page. No plugins required.
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
Webstudio: Paste this in the Custom Code → Head Code section of your project settings, or in a page-level HTML embed.
.block-transition-grid {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 9999;
overflow: hidden;
}
.block-transition-block {
position: absolute;
opacity: 0;
}
Webstudio: Add this CSS in a global
style embed or in
Project Settings → Custom Code → Head Code
wrapped in <style> tags.
Add the transition container element to every page. Configure the effect using data attributes:
<!-- Place this on every page (e.g. in a global embed or site-wide component) -->
<div
data-block-transition
data-block-size="60"
data-block-color="#0f0f0f"
data-stagger-amount="0.5"
data-block-duration="0.05"
data-enter-delay="0.3"
data-stagger-from="random"
></div>
Then add data-transition-link to any link that should
trigger the transition:
<!-- Any link with this attribute will trigger the block transition -->
<a href="/about" data-transition-link>About</a>
<a href="/work" data-transition-link>Our Work</a>
<a href="/contact" data-transition-link>Contact</a>
Webstudio: Select your link element, open the
Settings panel, scroll to
Custom Attributes, and add
data-transition-link with an empty value. For the
config div, use an HTML Embed component.
This script handles everything: grid creation, link interception, leave/enter animations, and cross-page coordination via sessionStorage.
document.addEventListener("DOMContentLoaded", () => {
// ── Read config from data attributes ──
const configEl = document.querySelector("[data-block-transition]");
if (!configEl) return;
const BLOCK_SIZE = parseInt(configEl.dataset.blockSize) || 60;
const BLOCK_COLOR = configEl.dataset.blockColor || "#0f0f0f";
const STAGGER_AMOUNT = parseFloat(configEl.dataset.staggerAmount) || 0.5;
const BLOCK_DURATION = parseFloat(configEl.dataset.blockDuration) || 0.05;
const ENTER_DELAY = parseFloat(configEl.dataset.enterDelay) || 0.3;
const STAGGER_FROM = configEl.dataset.staggerFrom || "random";
// ── Create the grid container ──
const grid = document.createElement("div");
grid.className = "block-transition-grid";
document.body.appendChild(grid);
let blocks = [];
function createGrid() {
grid.innerHTML = "";
blocks = [];
const cols = Math.ceil(window.innerWidth / BLOCK_SIZE);
const rows = Math.ceil(window.innerHeight / BLOCK_SIZE) + 1;
const offsetX = (window.innerWidth - cols * BLOCK_SIZE) / 2;
const offsetY = (window.innerHeight - rows * BLOCK_SIZE) / 2;
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const block = document.createElement("div");
block.className = "block-transition-block";
block.style.cssText =
"width:" + BLOCK_SIZE + "px;" +
"height:" + BLOCK_SIZE + "px;" +
"left:" + (col * BLOCK_SIZE + offsetX) + "px;" +
"top:" + (row * BLOCK_SIZE + offsetY) + "px;" +
"background-color:" + BLOCK_COLOR;
grid.appendChild(block);
blocks.push(block);
}
}
}
createGrid();
window.addEventListener("resize", createGrid);
// ── Enter animation (reveal new page) ──
if (sessionStorage.getItem("block-transition-active")) {
sessionStorage.removeItem("block-transition-active");
gsap.set(blocks, { opacity: 1 });
gsap.to(blocks, {
opacity: 0,
duration: BLOCK_DURATION,
delay: ENTER_DELAY,
ease: "power2.inOut",
stagger: { amount: STAGGER_AMOUNT, from: STAGGER_FROM },
});
}
// ── Leave animation (cover page, then navigate) ──
document.querySelectorAll("[data-transition-link]").forEach((link) => {
link.addEventListener("click", (e) => {
e.preventDefault();
const href = link.getAttribute("href");
if (!href || href === "#") return;
sessionStorage.setItem("block-transition-active", "true");
gsap.to(blocks, {
opacity: 1,
duration: BLOCK_DURATION,
ease: "power2.inOut",
stagger: { amount: STAGGER_AMOUNT, from: STAGGER_FROM },
onComplete: () => {
window.location.href = href;
},
});
});
});
});
Webstudio: Paste this inside a
<script> tag in
Project Settings → Custom Code → Body Code
(end of body). This ensures it runs on every page.
Config element
([data-block-transition]):
| Attribute | Default | Description |
|---|---|---|
data-block-transition |
required | Marks the config element. Place one per page. |
data-block-size |
60 |
Size of each grid block in pixels. |
data-block-color |
#0f0f0f |
Background color of the blocks. |
data-stagger-amount |
0.5 |
Total time (seconds) to spread the stagger across all blocks. |
data-block-duration |
0.05 |
Duration (seconds) of each individual block's fade. |
data-enter-delay |
0.3 |
Delay (seconds) before the enter/reveal animation starts. |
data-stagger-from |
random |
Stagger pattern. Options: random,
center, edges, end, or a
number index.
|
Link element ([data-transition-link]):
| Attribute | Default | Description |
|---|---|---|
data-transition-link |
required |
Add to any <a> tag to enable the block
transition on click.
|
| What | Where in Webstudio |
|---|---|
GSAP <script> tag |
Project Settings → Custom Code → Head Code |
CSS (.block-transition-grid, etc.) |
Project Settings → Custom Code → Head Code (in
<style> tags)
|
Config <div> with
data-block-transition
|
HTML Embed component on each page (or in a shared layout section) |
| JavaScript |
Project Settings → Custom Code → Body Code (in
<script> tags)
|
data-transition-link on links |
Select link → Settings panel → Custom Attributes |
<!-- Larger blocks, white color, slower animation -->
<div
data-block-transition
data-block-size="100"
data-block-color="#ffffff"
data-stagger-amount="0.8"
data-block-duration="0.08"
data-enter-delay="0.5"
data-stagger-from="center"
></div>
<!-- Small dense blocks, fast transition -->
<div
data-block-transition
data-block-size="30"
data-block-color="#0f0f0f"
data-stagger-amount="0.3"
data-block-duration="0.03"
data-enter-delay="0.2"
data-stagger-from="random"
></div>