SectionsIntermediateApril 13, 2026
Blog Grid Reveal
A typographic blog grid with inline images embedded in large text links. Each row slides up into view on scroll via GSAP ScrollTrigger. Rows alternate alignment and images react on hover with rotation and opacity.
View Full Demo →Preview
Source
demo.jsx
import BlogGridReveal from "./index.jsx";
import { blogGridReveal } from "../demo-data.js";
import styles from "./demo.module.css";
export default function BlogGridRevealDemo() {
return (
<div className={styles.page}>
<div className={styles.spacer} />
<BlogGridReveal {...blogGridReveal} />
<div className={styles.spacer} />
</div>
);
}
index.jsx
"use client";
import { useEffect, useRef } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import styles from "./styles.module.css";
gsap.registerPlugin(ScrollTrigger);
function BlogItem({ item, reversed }) {
const { title, title1, title2, image, size, color, href = "#", imagePosition = "before" } = item;
const content = title1 && title2 ? (
<span>
{title1}{" "}
<img src={image} alt="" className={styles.inlineImage} />
{" "}{title2}
</span>
) : imagePosition === "after" ? (
<>
{title}{" "}
<img src={image} alt={title} className={styles.inlineImage} />
</>
) : (
<>
<img src={image} alt={title} className={styles.inlineImage} />
{" "}{title}
</>
);
return (
<div className={`${styles.row} ${reversed ? styles.reverse : ""}`}>
<div className={styles.revealWrapper}>
<div className={styles.revealInner}>
<a href={href} className={styles.link} style={{ fontSize: size, color }}>
{content}
</a>
</div>
</div>
</div>
);
}
export default function BlogGridReveal({
label = "Essential",
items = [],
readMoreLabel = "Read All",
readMoreHref = "#",
}) {
const sectionRef = useRef(null);
useEffect(() => {
const ctx = gsap.context(() => {
const els = gsap.utils.toArray(`.${styles.revealInner}`);
gsap.set(els, { yPercent: 100 });
ScrollTrigger.create({
trigger: sectionRef.current,
start: "top 60%",
once: true,
onEnter: () => {
gsap.to(els, {
yPercent: 0,
duration: 1.25,
ease: "power4.out",
stagger: 0.04,
});
},
});
}, sectionRef);
return () => ctx.revert();
}, []);
return (
<section ref={sectionRef} className={styles.wrapper}>
<div className={styles.column}>
<div className={styles.revealWrapper}>
<div className={`${styles.revealInner} ${styles.label}`}>
{label}
</div>
</div>
{items.map((item, index) => (
<BlogItem key={index} item={item} reversed={index % 2 !== 0} />
))}
<div className={styles.revealWrapper}>
<div className={styles.revealInner}>
<a href={readMoreHref} className={styles.readMore}>
<span>{readMoreLabel}</span>
</a>
</div>
</div>
</div>
</section>
);
}
demo.module.css
.page {
background: #f1ebe7;
}
.spacer {
height: 50vh;
}
styles.module.css
.wrapper {
width: 100%;
min-height: 70vh;
background-color: #f1ebe7;
display: flex;
align-items: center;
justify-content: center;
padding: 4rem 0;
box-sizing: border-box;
}
.column {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
/* ---- Reveal clip ---- */
.revealWrapper {
overflow: hidden;
display: block;
}
.revealInner {
will-change: transform;
}
/* ---- Label ---- */
.label {
font-size: 14px;
color: #000;
text-transform: uppercase;
text-align: center;
padding: 2rem 0;
width: 100%;
}
/* ---- Rows ---- */
.row {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 1rem;
margin: 0.25rem 0;
}
.reverse {
flex-direction: row-reverse;
}
/* ---- Link ---- */
.link {
color: #000;
text-transform: uppercase;
text-decoration: none;
letter-spacing: -0.06em;
transition: color 0.4s ease;
display: inline-flex;
align-items: center;
}
.link:hover {
color: #000;
}
/* ---- Inline image ---- */
.inlineImage {
height: 1.25em;
width: auto;
vertical-align: middle;
margin: 0 0.4em;
opacity: 0.5;
transition: transform 0.3s ease, opacity 0.3s ease;
display: inline-block;
object-fit: contain;
}
.link:hover .inlineImage {
opacity: 1;
transform: rotate(7deg) scale(1.05);
}
/* ---- Read more ---- */
.readMore {
font-size: 14px;
color: #000;
text-transform: uppercase;
text-align: center;
text-decoration: none;
margin-top: 1.25rem;
cursor: pointer;
display: inline-block;
}
.readMore span {
display: inline-block;
position: relative;
}
.readMore span::after {
content: "";
display: block;
margin: 0.25rem auto 0;
width: 100%;
height: 1px;
background: #000;
transition: width 0.3s ease;
}
.readMore:hover span::after {
width: 60%;
}
/* ---- Mobile ---- */
@media (max-width: 768px) {
.wrapper {
min-height: auto;
padding: 3rem 1rem;
}
.link {
letter-spacing: -0.04em;
}
}
Dependencies
gsap
Bass
Westend Studio
Stats
Delivrd