Photography — Instagram Feed
A gallery-dark Instagram-style photo feed with a sticky profile header, follow toggle, stat counts, story highlights, and a responsive 3-column square grid whose tiles reveal like and comment counts on hover, plus keyboard-accessible like buttons and a lightweight toast.
MCP
Code
:root {
--ink: #0b0b0c;
--bg: #101012;
--surface: #17171b;
--surface-2: #1e1e23;
--paper: #f6f4ef;
--sand: #c9b79c;
--sand-d: #a8916f;
--muted: #9a9aa2;
--line: rgba(255, 255, 255, 0.10);
--line-2: rgba(255, 255, 255, 0.18);
--white: #fff;
--r-sm: 6px;
--r-md: 12px;
--r-lg: 18px;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
background:
radial-gradient(1200px 600px at 80% -10%, rgba(201, 183, 156, 0.10), transparent 60%),
var(--bg);
color: var(--white);
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.app {
max-width: 960px;
margin: 0 auto;
padding: 40px 20px 80px;
}
/* ---------- Profile header ---------- */
.profile {
display: grid;
grid-template-columns: 168px 1fr;
gap: 36px;
align-items: center;
padding-bottom: 28px;
border-bottom: 1px solid var(--line);
}
.profile__avatar {
width: 148px;
height: 148px;
border-radius: 50%;
display: grid;
place-items: center;
justify-self: center;
background:
conic-gradient(from 200deg, var(--sand), var(--sand-d), #6d5a3e, var(--sand));
padding: 4px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
}
.profile__avatar span {
width: 100%;
height: 100%;
border-radius: 50%;
display: grid;
place-items: center;
background: var(--surface-2);
color: var(--sand);
font-family: "Fraunces", serif;
font-size: 44px;
font-weight: 600;
letter-spacing: 0.02em;
}
.profile__top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.profile__id { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.profile__handle {
margin: 0;
font-family: "Fraunces", serif;
font-weight: 600;
font-size: 28px;
letter-spacing: -0.01em;
}
.badge {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.14em;
color: var(--sand);
border: 1px solid var(--line-2);
padding: 4px 10px;
border-radius: 999px;
}
.profile__actions { display: flex; gap: 10px; }
.btn {
font: inherit;
font-weight: 600;
font-size: 14px;
border-radius: var(--r-sm);
padding: 9px 20px;
cursor: pointer;
border: 1px solid transparent;
transition: transform 0.12s ease, background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
.btn:active { transform: translateY(1px) scale(0.98); }
.btn:focus-visible { outline: 2px solid var(--sand); outline-offset: 2px; }
.btn--follow { background: var(--paper); color: var(--ink); }
.btn--follow:hover { background: var(--white); }
.btn--follow.is-following {
background: transparent;
color: var(--white);
border-color: var(--line-2);
}
.btn--ghost {
background: var(--surface-2);
color: var(--white);
border-color: var(--line);
}
.btn--ghost:hover { background: #26262c; }
.stats {
display: flex;
gap: 40px;
list-style: none;
padding: 20px 0 0;
margin: 0;
}
.stat { display: flex; gap: 7px; align-items: baseline; color: var(--muted); font-size: 15px; }
.stat strong { color: var(--white); font-weight: 700; font-size: 16px; }
.profile__name { margin: 16px 0 4px; font-weight: 600; }
.profile__bio { margin: 0; color: var(--muted); font-size: 14px; }
/* ---------- Highlights ---------- */
.highlights {
display: flex;
gap: 26px;
padding: 26px 2px;
overflow-x: auto;
scrollbar-width: none;
border-bottom: 1px solid var(--line);
}
.highlights::-webkit-scrollbar { display: none; }
.highlight {
background: none;
border: 0;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
gap: 9px;
flex: 0 0 auto;
color: var(--muted);
font: inherit;
}
.highlight:focus-visible { outline: 2px solid var(--sand); outline-offset: 4px; border-radius: var(--r-md); }
.highlight__ring {
width: 68px;
height: 68px;
border-radius: 50%;
padding: 3px;
display: grid;
place-items: center;
border: 1px solid var(--line-2);
transition: transform 0.16s ease, border-color 0.2s ease;
}
.highlight:hover .highlight__ring { transform: translateY(-3px); border-color: var(--sand); }
.highlight__ring--new { border-style: dashed; }
.highlight__thumb {
width: 100%;
height: 100%;
border-radius: 50%;
background-size: cover;
background-position: center;
}
.highlight__thumb--new {
display: grid;
place-items: center;
color: var(--sand);
font-size: 24px;
background: var(--surface-2);
}
.highlight__label { font-size: 12px; letter-spacing: 0.02em; }
.h1 { background: linear-gradient(135deg, #3a4a5a, #7d94a6); }
.h2 { background: linear-gradient(135deg, #6d5a3e, var(--sand)); }
.h3 { background: linear-gradient(135deg, #2a2a30, #55555f); }
.h4 { background: linear-gradient(135deg, #5a3a3a, #a67d7d); }
.h5 { background: linear-gradient(135deg, #1c2b3a, #46607a); }
/* ---------- Tabs ---------- */
.tabs {
display: flex;
justify-content: center;
gap: 48px;
margin-top: 4px;
}
.tab {
background: none;
border: 0;
border-top: 2px solid transparent;
margin-top: -1px;
padding: 16px 4px;
color: var(--muted);
font: inherit;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.18em;
cursor: pointer;
transition: color 0.2s ease, border-color 0.2s ease;
}
.tab:hover { color: var(--white); }
.tab.is-active { color: var(--white); border-top-color: var(--paper); }
.tab:focus-visible { outline: 2px solid var(--sand); outline-offset: 2px; }
/* ---------- Grid ---------- */
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
margin-top: 20px;
}
.tile {
position: relative;
aspect-ratio: 1 / 1;
border: 0;
padding: 0;
cursor: pointer;
overflow: hidden;
border-radius: 2px;
background: var(--surface);
font: inherit;
color: var(--white);
display: block;
}
.tile__img {
position: absolute;
inset: 0;
background-size: cover;
background-position: center;
transition: transform 0.5s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.tile:hover .tile__img { transform: scale(1.06); }
.tile__pin {
position: absolute;
top: 8px;
right: 8px;
z-index: 3;
font-size: 9px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--white);
background: rgba(0, 0, 0, 0.45);
border: 1px solid var(--line-2);
padding: 3px 7px;
border-radius: 999px;
backdrop-filter: blur(6px);
}
.tile__overlay {
position: absolute;
inset: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
gap: 22px;
background: rgba(10, 10, 12, 0.55);
opacity: 0;
transition: opacity 0.22s ease;
}
.tile:hover .tile__overlay,
.tile:focus-within .tile__overlay { opacity: 1; }
.tile__meta {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: 14px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.tile__meta svg { width: 18px; height: 18px; }
.tile__like {
position: absolute;
bottom: 8px;
left: 8px;
z-index: 4;
width: 34px;
height: 34px;
display: grid;
place-items: center;
border-radius: 50%;
border: 1px solid var(--line-2);
background: rgba(0, 0, 0, 0.5);
color: var(--white);
cursor: pointer;
opacity: 0;
transform: translateY(6px);
transition: opacity 0.2s ease, transform 0.2s ease, background 0.2s ease;
backdrop-filter: blur(6px);
}
.tile:hover .tile__like,
.tile:focus-within .tile__like { opacity: 1; transform: translateY(0); }
.tile__like svg { width: 18px; height: 18px; }
.tile__like:hover { background: rgba(0, 0, 0, 0.72); }
.tile__like:focus-visible { outline: 2px solid var(--sand); outline-offset: 2px; opacity: 1; }
.tile__like.is-liked { color: #ff5a7a; border-color: #ff5a7a; }
.tile__like.is-liked svg { fill: #ff5a7a; }
.tile__like.pop { animation: pop 0.32s ease; }
@keyframes pop {
0% { transform: scale(1); }
45% { transform: scale(1.32); }
100% { transform: scale(1); }
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 24px);
background: var(--paper);
color: var(--ink);
padding: 11px 18px;
border-radius: 999px;
font-size: 13px;
font-weight: 600;
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.45);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
z-index: 50;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- Responsive ---------- */
@media (max-width: 720px) {
.profile { grid-template-columns: 96px 1fr; gap: 20px; align-items: start; }
.profile__avatar { width: 92px; height: 92px; }
.profile__avatar span { font-size: 28px; }
.stats { gap: 26px; }
}
@media (max-width: 520px) {
.app { padding: 24px 12px 64px; }
.profile { gap: 16px; }
.profile__top { flex-direction: column; align-items: flex-start; }
.profile__actions { width: 100%; }
.btn { flex: 1; text-align: center; }
.profile__handle { font-size: 22px; }
.stats {
justify-content: space-between;
gap: 12px;
order: 3;
width: 100%;
padding-top: 16px;
margin-top: 16px;
border-top: 1px solid var(--line);
}
.stat { flex-direction: column; gap: 0; align-items: center; text-align: center; font-size: 12px; }
.grid { grid-template-columns: repeat(2, 1fr); gap: 4px; }
.tabs { gap: 32px; }
/* touch: keep like buttons reachable */
.tile__like { opacity: 1; transform: none; }
}(function () {
"use strict";
var HEART =
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.6l-1-1a5.5 5.5 0 0 0-7.8 7.8l1 1L12 21l7.8-7.6 1-1a5.5 5.5 0 0 0 0-7.8Z"/></svg>';
var COMMENT =
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 11.5a8.4 8.4 0 0 1-8.5 8.5 8.6 8.6 0 0 1-3.9-.9L3 21l1.9-5.6A8.5 8.5 0 1 1 21 11.5Z"/></svg>';
// Fictional feed data. Gradients keep it dependency-free and always render.
var PHOTOS = [
{ g: "linear-gradient(135deg,#1b2a3a,#4a6b86)", cap: "Fjord fog", likes: 2140, comments: 63, pin: "Reel" },
{ g: "linear-gradient(135deg,#6d5a3e,#c9b79c)", cap: "Golden dunes", likes: 3890, comments: 121 },
{ g: "linear-gradient(135deg,#2a2a30,#55555f)", cap: "Studio grey", likes: 980, comments: 24 },
{ g: "linear-gradient(160deg,#3a1f2b,#a6647d)", cap: "Neon alley", likes: 5420, comments: 208, pin: "Top" },
{ g: "linear-gradient(135deg,#132033,#3f5a78)", cap: "Blue hour", likes: 1760, comments: 41 },
{ g: "linear-gradient(135deg,#40331f,#b89b6a)", cap: "Harvest light", likes: 2650, comments: 77 },
{ g: "linear-gradient(150deg,#1c1c22,#3d3d47)", cap: "Long exposure", likes: 1290, comments: 33 },
{ g: "linear-gradient(135deg,#22343f,#6d94a6)", cap: "Tidal lines", likes: 4310, comments: 156, pin: "Reel" },
{ g: "linear-gradient(135deg,#4a2f22,#c98f6a)", cap: "Rust & salt", likes: 2075, comments: 58 },
{ g: "linear-gradient(160deg,#20303a,#527088)", cap: "Cold shore", likes: 3140, comments: 92 },
{ g: "linear-gradient(135deg,#2c2438,#6b5a86)", cap: "Purple dusk", likes: 1880, comments: 47 },
{ g: "linear-gradient(135deg,#3a3020,#c9b79c)", cap: "Field grain", likes: 2990, comments: 88 }
];
var grid = document.getElementById("grid");
var toastEl = document.getElementById("toast");
var followBtn = document.getElementById("followBtn");
var followerCountEl = document.getElementById("followerCount");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2000);
}
function fmt(n) {
if (n >= 1000) return (n / 1000).toFixed(n % 1000 >= 100 ? 1 : 0) + "k";
return String(n);
}
// ----- Build grid -----
PHOTOS.forEach(function (p, i) {
var tile = document.createElement("article");
tile.className = "tile";
var img = document.createElement("span");
img.className = "tile__img";
img.style.background = p.g;
tile.appendChild(img);
if (p.pin) {
var pin = document.createElement("span");
pin.className = "tile__pin";
pin.textContent = p.pin;
tile.appendChild(pin);
}
var overlay = document.createElement("div");
overlay.className = "tile__overlay";
var likeMeta = document.createElement("span");
likeMeta.className = "tile__meta";
likeMeta.innerHTML = HEART + '<span class="js-likecount">' + fmt(p.likes) + "</span>";
var comMeta = document.createElement("span");
comMeta.className = "tile__meta";
comMeta.innerHTML = COMMENT + "<span>" + fmt(p.comments) + "</span>";
overlay.appendChild(likeMeta);
overlay.appendChild(comMeta);
tile.appendChild(overlay);
var likeBtn = document.createElement("button");
likeBtn.className = "tile__like";
likeBtn.type = "button";
likeBtn.innerHTML = HEART;
likeBtn.setAttribute("aria-pressed", "false");
likeBtn.setAttribute("aria-label", "Like photo: " + p.cap);
tile.appendChild(likeBtn);
var likes = p.likes;
var liked = false;
var countSpan = likeMeta.querySelector(".js-likecount");
likeBtn.addEventListener("click", function (e) {
e.stopPropagation();
liked = !liked;
likes += liked ? 1 : -1;
likeBtn.classList.toggle("is-liked", liked);
likeBtn.setAttribute("aria-pressed", liked ? "true" : "false");
countSpan.textContent = fmt(likes);
likeBtn.classList.remove("pop");
void likeBtn.offsetWidth; // restart animation
likeBtn.classList.add("pop");
toast(liked ? "Liked “" + p.cap + "”" : "Removed like");
});
tile.addEventListener("click", function () {
toast("Opening “" + p.cap + "” · " + fmt(p.comments) + " comments");
});
grid.appendChild(tile);
});
// ----- Follow toggle -----
var followers = 18400;
var following = false;
followBtn.addEventListener("click", function () {
following = !following;
followers += following ? 1 : -1;
followBtn.textContent = following ? "Following" : "Follow";
followBtn.classList.toggle("is-following", following);
followBtn.setAttribute("aria-pressed", following ? "true" : "false");
followerCountEl.textContent = fmt(followers);
toast(following ? "You're following @mira.kestrel" : "Unfollowed @mira.kestrel");
});
// ----- Tabs -----
var tabs = document.querySelectorAll(".tab");
tabs.forEach(function (tab) {
tab.addEventListener("click", function () {
tabs.forEach(function (t) {
t.classList.remove("is-active");
t.setAttribute("aria-selected", "false");
});
tab.classList.add("is-active");
tab.setAttribute("aria-selected", "true");
toast(tab.dataset.tab === "tagged" ? "No tagged photos yet" : "Showing 12 posts");
});
});
// ----- Highlights -----
document.querySelectorAll(".highlight").forEach(function (h) {
h.addEventListener("click", function () {
var label = h.querySelector(".highlight__label");
toast(label ? "Story: " + label.textContent : "Add a highlight");
});
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Photography — Instagram Feed</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Fraunces:wght@400;600&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="app">
<!-- Profile header -->
<header class="profile">
<div class="profile__avatar" aria-hidden="true">
<span>MK</span>
</div>
<div class="profile__body">
<div class="profile__top">
<div class="profile__id">
<h1 class="profile__handle">@mira.kestrel</h1>
<span class="badge">Photographer</span>
</div>
<div class="profile__actions">
<button class="btn btn--follow" id="followBtn" aria-pressed="false">Follow</button>
<button class="btn btn--ghost" id="messageBtn">Message</button>
</div>
</div>
<ul class="stats" role="list">
<li class="stat"><strong id="postCount">248</strong><span>posts</span></li>
<li class="stat"><strong id="followerCount">18.4k</strong><span>followers</span></li>
<li class="stat"><strong>512</strong><span>following</span></li>
</ul>
<p class="profile__name">Mira Kestrel</p>
<p class="profile__bio">
Light chaser · fog & long exposures · Reykjavík ⇄ Lisbon<br />
Prints & workshops → link in bio
</p>
</div>
</header>
<!-- Story highlights -->
<nav class="highlights" aria-label="Story highlights">
<button class="highlight"><span class="highlight__ring"><span class="highlight__thumb h1"></span></span><span class="highlight__label">Iceland</span></button>
<button class="highlight"><span class="highlight__ring"><span class="highlight__thumb h2"></span></span><span class="highlight__label">Coast</span></button>
<button class="highlight"><span class="highlight__ring"><span class="highlight__thumb h3"></span></span><span class="highlight__label">Studio</span></button>
<button class="highlight"><span class="highlight__ring"><span class="highlight__thumb h4"></span></span><span class="highlight__label">Film</span></button>
<button class="highlight"><span class="highlight__ring"><span class="highlight__thumb h5"></span></span><span class="highlight__label">Nights</span></button>
<button class="highlight"><span class="highlight__ring highlight__ring--new"><span class="highlight__thumb highlight__thumb--new">+</span></span><span class="highlight__label">New</span></button>
</nav>
<!-- Tabs -->
<div class="tabs" role="tablist" aria-label="Feed views">
<button class="tab is-active" role="tab" aria-selected="true" data-tab="grid">Grid</button>
<button class="tab" role="tab" aria-selected="false" data-tab="tagged">Tagged</button>
</div>
<!-- Feed grid -->
<section class="grid" id="grid" aria-label="Photo feed"></section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>An image-first social feed built for photographers who want their work to lead. A sticky profile header pairs an oversized avatar with a fictional handle, bio, and live stat counts, alongside a Follow button that flips to Following and nudges the follower total. Below it, a row of circular story highlights scrolls horizontally, and a segmented tab bar switches between the grid and a tagged view.
The heart of the component is a responsive square grid. Every tile is a full-bleed gradient or Unsplash-style photo that dims on hover to reveal centered like and comment counts in thin uppercase labels. Each tile carries a keyboard-focusable heart toggle so visitors can like a shot without leaving the grid — the count animates, the icon fills, and a small toast confirms the action.
Everything is vanilla: no frameworks, no build step. The layout collapses gracefully to a two-column grid and a stacked header at ~520px, focus rings stay visible for keyboard users, and contrast holds against the deep gallery-dark surfaces so the photography always stays the loudest thing on the page.