SectionsSimpleApril 21, 2026
Instagram Feed
A 5-column Instagram post grid with handle, serif heading, and CTA link. Supports both images and videos per card. Hardcoded posts with commented-out Instagram Graph API and Behold integration paths.
View Full Demo →Preview
@sheabinta
Follow along.
Source
demo.jsx
import InstagramFeed from "./index.jsx";
import { instagramFeed } from "../demo-data.js";
export default function InstagramFeedDemo() {
return <InstagramFeed {...instagramFeed} />;
}
index.jsx
"use client";
import styles from "./styles.module.css";
/*
── Instagram API Integration ──
To pull posts dynamically instead of hardcoding:
// Option A: Instagram Graph API (Business/Creator account)
// Requires: Facebook App, Instagram Business account, long-lived token
//
// Server-side fetch (Next.js server component or getStaticProps):
// const INSTAGRAM_TOKEN = process.env.INSTAGRAM_TOKEN;
// const res = await fetch(
// `https://graph.instagram.com/me/media?fields=id,media_type,media_url,permalink,thumbnail_url&access_token=${INSTAGRAM_TOKEN}&limit=6`
// );
// const data = await res.json();
//
// Map response to posts:
// const posts = data.data.map(post => ({
// image: post.media_type === "VIDEO" ? post.thumbnail_url : post.media_url,
// video: post.media_type === "VIDEO" ? post.media_url : undefined,
// href: post.permalink,
// }));
//
// Note: tokens expire every 60 days. Refresh via:
// GET https://graph.instagram.com/refresh_access_token?grant_type=ig_refresh_token&access_token={token}
// Option B: Behold (third-party, handles token refresh)
// Sign up at behold.so, connect your Instagram, get a feed ID.
// Fetch from: https://feeds.behold.so/{FEED_ID}
// Returns JSON array of posts with media URLs and permalinks.
*/
function PostCard({ post }) {
const hasVideo = !!post.video;
return (
<a
href={post.href || "#"}
className={styles.card}
target="_blank"
rel="noopener noreferrer"
>
<div className={styles.cardInner}>
{hasVideo ? (
<video
className={styles.media}
src={post.video}
autoPlay
muted
loop
playsInline
poster={post.image}
/>
) : (
<img
className={styles.media}
src={post.image}
alt={post.alt || "Instagram post"}
/>
)}
<div className={styles.igIcon}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z" />
</svg>
</div>
</div>
</a>
);
}
export default function InstagramFeed({
handle = "@studio",
heading = "Follow along.",
ctaLabel = "See More on Instagram",
ctaHref = "#",
posts = [],
}) {
return (
<section className={styles.section}>
<div className={styles.header}>
<div className={styles.headerLeft}>
<p className={styles.handle}>{handle}</p>
<h2 className={styles.heading}>{heading}</h2>
</div>
{ctaLabel && (
<a
href={ctaHref}
className={styles.cta}
target="_blank"
rel="noopener noreferrer"
>
{ctaLabel} →
</a>
)}
</div>
<div className={styles.grid}>
{posts.map((post, i) => (
<PostCard key={i} post={post} />
))}
</div>
</section>
);
}
styles.module.css
.section {
width: 100%;
padding: 5rem 2rem 4rem;
}
/* ---- Header ---- */
.header {
display: flex;
flex-direction: column;
gap: 1.5rem;
margin-bottom: 2rem;
}
.headerLeft {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.handle {
font-size: 0.65rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.12em;
color: #1a1a1a;
}
.heading {
font-family: Georgia, "Times New Roman", serif;
font-size: clamp(1.75rem, 3vw, 2.75rem);
font-weight: 400;
line-height: 1.15;
color: #1a1a1a;
}
.cta {
font-size: 0.65rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #1a1a1a;
text-decoration: none;
border-bottom: 1px solid #1a1a1a;
padding-bottom: 0.2rem;
align-self: flex-start;
white-space: nowrap;
transition: color 0.3s ease, border-color 0.3s ease;
}
.cta:hover {
color: #555;
border-color: #555;
}
/* ---- Grid ---- */
.grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
}
/* ---- Card ---- */
.card {
display: block;
text-decoration: none;
}
.cardInner {
position: relative;
width: 100%;
aspect-ratio: 1 / 1;
overflow: hidden;
border-radius: 0.25rem;
}
.media {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform 0.5s ease;
}
.card:hover .media {
transform: scale(1.03);
}
/* ---- Instagram icon ---- */
.igIcon {
position: absolute;
bottom: 0.75rem;
right: 0.75rem;
width: 1.75rem;
height: 1.75rem;
background: rgba(255, 255, 255, 0.85);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #1a1a1a;
opacity: 0.7;
transition: opacity 0.3s ease;
}
.card:hover .igIcon {
opacity: 1;
}
/* ---- Desktop ---- */
@media (min-width: 768px) {
.header {
flex-direction: row;
justify-content: space-between;
align-items: flex-end;
}
.cta {
align-self: flex-end;
}
.grid {
grid-template-columns: repeat(5, 1fr);
}
}
/* ---- Mobile ---- */
@media (max-width: 767px) {
.section {
padding: 3rem 1rem 3rem;
}
}




