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 →

@sheabinta

Follow along.

See More on Instagram
Instagram post
Instagram post
Instagram post
Instagram post
Instagram post
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;
  }
}

May 11, 2026

SECTIONS

Dual Push Cards

Two-up CTA card section with scroll-driven parallax on each card's background image. Cards scale down and drift vertically as you scroll past. Glassmorphic blur buttons at bottom-left. Stacks on mobile, side-by-side grid on desktop.

May 4, 2026

SECTIONS

Portfolio Grid

A responsive portfolio showcase section with a header tagline, blinking cursor counters, a 2-up/4-col project card grid with hover-zoom images and data-label metadata, plus a full-width CTA button. Scroll-triggered fade and move-up animations via GSAP.

May 1, 2026

SECTIONS

Logo Wall Cycle

A responsive logo grid that cycles through brand logos with smooth GSAP-powered swap animations. Shows 8 logos on desktop and 6 on tablet, shuffling hidden logos into view on a timed loop. Pauses when out of viewport or tab is hidden.