UI Components Medium
Social Media Feed Card
A pixel-perfect social media feed card inspired by modern platforms. Includes support for images, metrics, and nested comments.
Open in Lab
MCP
vanilla-js css react tailwind vue svelte
Targets: TS JS HTML React Vue Svelte
Code
:root {
--feed-bg: rgba(255, 255, 255, 0.04);
--feed-border: rgba(255, 255, 255, 0.08);
--feed-text-main: #f8fafc;
--feed-text-sub: #94a3b8;
--feed-accent: #60a5fa;
--feed-surface: rgba(255, 255, 255, 0.03);
--feed-input-bg: rgba(255, 255, 255, 0.06);
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
display: grid;
place-items: center;
background: #0b1221;
padding: 1rem;
}
.feed-card {
background: var(--feed-bg);
border: 1px solid var(--feed-border);
border-radius: 20px;
max-width: 500px;
margin: 0 auto;
font-family: "Inter", system-ui, sans-serif;
overflow: hidden;
backdrop-filter: blur(16px);
}
.card-header {
padding: 1.125rem 1.125rem 0.875rem;
display: flex;
align-items: center;
gap: 0.75rem;
}
.user-avatar {
width: 42px;
height: 42px;
border-radius: 50%;
overflow: hidden;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
flex-shrink: 0;
border: 2px solid rgba(255, 255, 255, 0.08);
}
.user-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.user-info {
flex: 1;
}
.user-name {
margin: 0 0 2px;
font-size: 0.9375rem;
font-weight: 700;
color: var(--feed-text-main);
}
.post-time {
font-size: 0.75rem;
color: var(--feed-text-sub);
}
.more-options {
background: none;
border: none;
color: var(--feed-text-sub);
cursor: pointer;
padding: 0.375rem;
border-radius: 6px;
transition: all 0.2s;
}
.more-options:hover {
color: var(--feed-text-main);
background: rgba(255, 255, 255, 0.06);
}
.card-content {
padding: 0 1.125rem 1rem;
}
.post-text {
font-size: 0.9375rem;
line-height: 1.55;
color: #cbd5e1;
margin-bottom: 0.875rem;
}
.post-media {
border-radius: 12px;
overflow: hidden;
border: 1px solid var(--feed-border);
}
.post-media img {
width: 100%;
display: block;
}
.card-actions {
padding: 0.75rem 1.125rem;
display: flex;
gap: 1.625rem;
border-top: 1px solid var(--feed-border);
}
.action-btn {
background: none;
border: none;
display: flex;
align-items: center;
gap: 0.375rem;
color: var(--feed-text-sub);
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
padding: 0.25rem;
border-radius: 6px;
}
.action-btn:hover {
color: var(--feed-accent);
}
.action-btn.like:hover {
color: #f87171;
}
.action-btn.like.active {
color: #f43f5e;
}
.card-input {
padding: 0.875rem 1.125rem;
background: var(--feed-surface);
display: flex;
gap: 0.75rem;
border-top: 1px solid var(--feed-border);
align-items: center;
}
.card-input input {
flex: 1;
background: var(--feed-input-bg);
border: 1px solid var(--feed-border);
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.875rem;
outline: none;
color: var(--feed-text-main);
transition: border-color 0.2s;
}
.card-input input::placeholder {
color: var(--feed-text-sub);
}
.card-input input:focus {
border-color: rgba(96, 165, 250, 0.4);
}
.send-comment {
background: none;
border: none;
color: var(--feed-accent);
font-weight: 700;
font-size: 0.875rem;
cursor: pointer;
transition: opacity 0.2s;
}
.send-comment:disabled {
opacity: 0.3;
cursor: default;
}const likeBtn = document.querySelector(".action-btn.like");
const commentInput = document.querySelector(".card-input input");
const sendBtn = document.querySelector(".send-comment");
let liked = false;
let likesCount = 42;
likeBtn.addEventListener("click", () => {
liked = !liked;
const countEl = likeBtn.querySelector(".count");
const iconEl = likeBtn.querySelector(".icon");
if (liked) {
likesCount++;
likeBtn.style.color = "#ef4444";
iconEl.textContent = "♥";
} else {
likesCount--;
likeBtn.style.color = "";
iconEl.textContent = "♡";
}
countEl.textContent = likesCount;
});
commentInput.addEventListener("input", () => {
sendBtn.disabled = commentInput.value.trim() === "";
});
sendBtn.addEventListener("click", () => {
if (commentInput.value.trim()) {
alert("Comment posted: " + commentInput.value);
commentInput.value = "";
sendBtn.disabled = true;
}
});<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Social Feed</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="feed-card">
<div class="card-header">
<div class="user-avatar">
<img src="https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=100&h=100&fit=crop"
alt="User" />
</div>
<div class="user-info">
<h4 class="user-name">Alex River</h4>
<span class="post-time">2 hours ago</span>
</div>
<button class="more-options">•••</button>
</div>
<div class="card-content">
<p class="post-text">Just finished building this social feed component! What do you think of the design? 🚀
#webdev #uiux</p>
<div class="post-media">
<img src="https://images.unsplash.com/photo-1498050108023-c5249f4df085?w=800&q=80" alt="Post content" />
</div>
</div>
<div class="card-actions">
<button class="action-btn like">
<span class="icon">♥</span> <span class="count">42</span>
</button>
<button class="action-btn comment">
<span class="icon">💬</span> <span class="count">12</span>
</button>
<button class="action-btn share">
<span class="icon">↗</span>
</button>
</div>
<div class="card-input">
<input type="text" placeholder="Add a comment..." />
<button class="send-comment">Post</button>
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { useState } from "react";
const FEED = [
{
id: 1,
user: { name: "Sarah Chen", handle: "@sarahchen", avatar: "SC", color: "#bc8cff" },
content:
"Just shipped a new feature: real-time collaboration with CRDT! The engineering challenge was immense but totally worth it. 🚀",
time: "2m ago",
likes: 47,
comments: 12,
reposts: 8,
liked: false,
},
{
id: 2,
user: { name: "Alex Rivera", handle: "@arivera", avatar: "AR", color: "#58a6ff" },
content:
"Hot take: The best documentation is code that doesn't need documentation. Write self-documenting code first, comments second.",
time: "18m ago",
likes: 128,
comments: 34,
reposts: 22,
liked: true,
},
{
id: 3,
user: { name: "Jordan Kim", handle: "@jordankim", avatar: "JK", color: "#7ee787" },
content:
"Spent 3 hours debugging a race condition only to find it was a missing `await`. We've all been there. 😅",
time: "1h ago",
likes: 215,
comments: 41,
reposts: 67,
liked: false,
},
];
type Post = (typeof FEED)[0];
function PostCard({ post }: { post: Post }) {
const [liked, setLiked] = useState(post.liked);
const [likes, setLikes] = useState(post.likes);
function toggleLike() {
setLiked((l) => !l);
setLikes((c) => (liked ? c - 1 : c + 1));
}
return (
<div className="bg-[#161b22] border border-[#30363d] rounded-xl p-4">
<div className="flex items-start gap-3 mb-3">
<div
className="w-9 h-9 rounded-full flex items-center justify-center text-xs font-bold text-[#0d1117] flex-shrink-0"
style={{ background: post.user.color }}
>
{post.user.avatar}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-baseline gap-2 flex-wrap">
<span className="text-[#e6edf3] font-semibold text-sm">{post.user.name}</span>
<span className="text-[#8b949e] text-xs">{post.user.handle}</span>
<span className="text-[#484f58] text-xs ml-auto">{post.time}</span>
</div>
<p className="text-[#e6edf3] text-sm mt-1.5 leading-relaxed">{post.content}</p>
</div>
</div>
<div className="flex items-center gap-5 ml-12">
{[
{
icon: (
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill={liked ? "#ff6b6b" : "none"}
stroke={liked ? "#ff6b6b" : "#8b949e"}
strokeWidth="2"
>
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
</svg>
),
count: likes,
active: liked,
onClick: toggleLike,
activeColor: "#ff6b6b",
},
{
icon: (
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="#8b949e"
strokeWidth="2"
>
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
),
count: post.comments,
active: false,
onClick: () => {},
activeColor: "#58a6ff",
},
{
icon: (
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="#8b949e"
strokeWidth="2"
>
<polyline points="17 1 21 5 17 9" />
<path d="M3 11V9a4 4 0 0 1 4-4h14" />
<polyline points="7 23 3 19 7 15" />
<path d="M21 13v2a4 4 0 0 1-4 4H3" />
</svg>
),
count: post.reposts,
active: false,
onClick: () => {},
activeColor: "#7ee787",
},
].map((action, i) => (
<button
key={i}
onClick={action.onClick}
className="flex items-center gap-1.5 text-xs text-[#8b949e] hover:text-[#e6edf3] transition-colors"
>
{action.icon}
<span style={{ color: action.active ? action.activeColor : undefined }}>
{action.count}
</span>
</button>
))}
</div>
</div>
);
}
export default function SocialFeedRC() {
return (
<div className="min-h-screen bg-[#0d1117] flex justify-center p-6">
<div className="w-full max-w-lg space-y-3">
<div className="flex items-center justify-between mb-2">
<h2 className="text-[#e6edf3] font-bold text-lg">Feed</h2>
<button className="text-xs text-[#58a6ff] hover:underline">Latest</button>
</div>
{FEED.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
</div>
);
}<script setup>
import { ref } from "vue";
const FEED = [
{
id: 1,
user: { name: "Sarah Chen", handle: "@sarahchen", avatar: "SC", color: "#bc8cff" },
content:
"Just shipped a new feature: real-time collaboration with CRDT! The engineering challenge was immense but totally worth it.",
time: "2m ago",
likes: 47,
comments: 12,
reposts: 8,
liked: false,
},
{
id: 2,
user: { name: "Alex Rivera", handle: "@arivera", avatar: "AR", color: "#58a6ff" },
content:
"Hot take: The best documentation is code that doesn't need documentation. Write self-documenting code first, comments second.",
time: "18m ago",
likes: 128,
comments: 34,
reposts: 22,
liked: true,
},
{
id: 3,
user: { name: "Jordan Kim", handle: "@jordankim", avatar: "JK", color: "#7ee787" },
content:
"Spent 3 hours debugging a race condition only to find it was a missing `await`. We've all been there.",
time: "1h ago",
likes: 215,
comments: 41,
reposts: 67,
liked: false,
},
];
const posts = ref(FEED.map((p) => ({ ...p, currentLiked: p.liked, currentLikes: p.likes })));
function toggleLike(id) {
const post = posts.value.find((p) => p.id === id);
if (!post) return;
if (post.currentLiked) {
post.currentLikes--;
} else {
post.currentLikes++;
}
post.currentLiked = !post.currentLiked;
}
</script>
<template>
<div class="wrapper">
<div class="feed">
<div class="feed-header">
<h2 class="feed-title">Feed</h2>
<button class="latest-btn">Latest</button>
</div>
<div v-for="post in posts" :key="post.id" class="post-card">
<div class="post-top">
<div class="avatar" :style="{ background: post.user.color }">
{{ post.user.avatar }}
</div>
<div class="post-body">
<div class="post-meta">
<span class="username">{{ post.user.name }}</span>
<span class="handle">{{ post.user.handle }}</span>
<span class="time">{{ post.time }}</span>
</div>
<p class="content">{{ post.content }}</p>
</div>
</div>
<div class="actions">
<button class="action-btn" @click="toggleLike(post.id)">
<svg width="14" height="14" viewBox="0 0 24 24"
:fill="post.currentLiked ? '#ff6b6b' : 'none'"
:stroke="post.currentLiked ? '#ff6b6b' : '#8b949e'"
stroke-width="2">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
</svg>
<span :style="{ color: post.currentLiked ? '#ff6b6b' : '' }">{{ post.currentLikes }}</span>
</button>
<button class="action-btn">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#8b949e" stroke-width="2">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
<span>{{ post.comments }}</span>
</button>
<button class="action-btn">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#8b949e" stroke-width="2">
<polyline points="17 1 21 5 17 9" /><path d="M3 11V9a4 4 0 0 1 4-4h14" />
<polyline points="7 23 3 19 7 15" /><path d="M21 13v2a4 4 0 0 1-4 4H3" />
</svg>
<span>{{ post.reposts }}</span>
</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.wrapper {
min-height: 100vh;
background: #0d1117;
display: flex;
justify-content: center;
padding: 1.5rem;
font-family: system-ui, -apple-system, sans-serif;
}
.feed { width: 100%; max-width: 32rem; display: flex; flex-direction: column; gap: 0.75rem; }
.feed-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.5rem; }
.feed-title { color: #e6edf3; font-weight: 700; font-size: 1.125rem; }
.latest-btn { font-size: 0.75rem; color: #58a6ff; background: none; border: none; cursor: pointer; }
.latest-btn:hover { text-decoration: underline; }
.post-card { background: #161b22; border: 1px solid #30363d; border-radius: 0.75rem; padding: 1rem; }
.post-top { display: flex; align-items: flex-start; gap: 0.75rem; margin-bottom: 0.75rem; }
.avatar {
width: 2.25rem; height: 2.25rem; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 0.75rem; font-weight: 700; color: #0d1117; flex-shrink: 0;
}
.post-body { flex: 1; min-width: 0; }
.post-meta { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
.username { color: #e6edf3; font-weight: 600; font-size: 0.875rem; }
.handle { color: #8b949e; font-size: 0.75rem; }
.time { color: #484f58; font-size: 0.75rem; margin-left: auto; }
.content { color: #e6edf3; font-size: 0.875rem; margin-top: 0.375rem; line-height: 1.6; }
.actions { display: flex; align-items: center; gap: 1.25rem; margin-left: 3rem; }
.action-btn {
display: flex; align-items: center; gap: 0.375rem;
font-size: 0.75rem; color: #8b949e;
background: none; border: none; cursor: pointer;
}
.action-btn:hover { color: #e6edf3; }
</style><script>
const FEED = [
{
id: 1,
user: { name: "Sarah Chen", handle: "@sarahchen", avatar: "SC", color: "#bc8cff" },
content:
"Just shipped a new feature: real-time collaboration with CRDT! The engineering challenge was immense but totally worth it.",
time: "2m ago",
likes: 47,
comments: 12,
reposts: 8,
liked: false,
},
{
id: 2,
user: { name: "Alex Rivera", handle: "@arivera", avatar: "AR", color: "#58a6ff" },
content:
"Hot take: The best documentation is code that doesn't need documentation. Write self-documenting code first, comments second.",
time: "18m ago",
likes: 128,
comments: 34,
reposts: 22,
liked: true,
},
{
id: 3,
user: { name: "Jordan Kim", handle: "@jordankim", avatar: "JK", color: "#7ee787" },
content:
"Spent 3 hours debugging a race condition only to find it was a missing `await`. We've all been there.",
time: "1h ago",
likes: 215,
comments: 41,
reposts: 67,
liked: false,
},
];
let posts = FEED.map((p) => ({ ...p, currentLiked: p.liked, currentLikes: p.likes }));
function toggleLike(id) {
posts = posts.map((p) => {
if (p.id !== id) return p;
const wasLiked = p.currentLiked;
return {
...p,
currentLiked: !wasLiked,
currentLikes: wasLiked ? p.currentLikes - 1 : p.currentLikes + 1,
};
});
}
</script>
<div class="wrapper">
<div class="feed">
<div class="feed-header">
<h2 class="feed-title">Feed</h2>
<button class="latest-btn">Latest</button>
</div>
{#each posts as post (post.id)}
<div class="post-card">
<div class="post-top">
<div class="avatar" style="background: {post.user.color};">
{post.user.avatar}
</div>
<div class="post-body">
<div class="post-meta">
<span class="username">{post.user.name}</span>
<span class="handle">{post.user.handle}</span>
<span class="time">{post.time}</span>
</div>
<p class="content">{post.content}</p>
</div>
</div>
<div class="actions">
<button class="action-btn" on:click={() => toggleLike(post.id)}>
<svg width="14" height="14" viewBox="0 0 24 24"
fill={post.currentLiked ? '#ff6b6b' : 'none'}
stroke={post.currentLiked ? '#ff6b6b' : '#8b949e'}
stroke-width="2">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
</svg>
<span style="color: {post.currentLiked ? '#ff6b6b' : ''}">{post.currentLikes}</span>
</button>
<button class="action-btn">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#8b949e" stroke-width="2">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
<span>{post.comments}</span>
</button>
<button class="action-btn">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#8b949e" stroke-width="2">
<polyline points="17 1 21 5 17 9" /><path d="M3 11V9a4 4 0 0 1 4-4h14" />
<polyline points="7 23 3 19 7 15" /><path d="M21 13v2a4 4 0 0 1-4 4H3" />
</svg>
<span>{post.reposts}</span>
</button>
</div>
</div>
{/each}
</div>
</div>
<style>
.wrapper {
min-height: 100vh;
background: #0d1117;
display: flex;
justify-content: center;
padding: 1.5rem;
font-family: system-ui, -apple-system, sans-serif;
}
.feed { width: 100%; max-width: 32rem; display: flex; flex-direction: column; gap: 0.75rem; }
.feed-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.5rem; }
.feed-title { color: #e6edf3; font-weight: 700; font-size: 1.125rem; }
.latest-btn { font-size: 0.75rem; color: #58a6ff; background: none; border: none; cursor: pointer; }
.latest-btn:hover { text-decoration: underline; }
.post-card { background: #161b22; border: 1px solid #30363d; border-radius: 0.75rem; padding: 1rem; }
.post-top { display: flex; align-items: flex-start; gap: 0.75rem; margin-bottom: 0.75rem; }
.avatar {
width: 2.25rem; height: 2.25rem; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 0.75rem; font-weight: 700; color: #0d1117; flex-shrink: 0;
}
.post-body { flex: 1; min-width: 0; }
.post-meta { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
.username { color: #e6edf3; font-weight: 600; font-size: 0.875rem; }
.handle { color: #8b949e; font-size: 0.75rem; }
.time { color: #484f58; font-size: 0.75rem; margin-left: auto; }
.content { color: #e6edf3; font-size: 0.875rem; margin-top: 0.375rem; line-height: 1.6; }
.actions { display: flex; align-items: center; gap: 1.25rem; margin-left: 3rem; }
.action-btn {
display: flex; align-items: center; gap: 0.375rem;
font-size: 0.75rem; color: #8b949e;
background: none; border: none; cursor: pointer;
}
.action-btn:hover { color: #e6edf3; }
</style>Social Media Feed Card
Create immersive social experiences with this versatile feed card. It includes all the essential elements of a modern post: user identity, media content, interactive metrics, and a quick-reply interface.
Features
- Responsive layout for mobile and desktop
- Integrated Like, Comment, and Share actions
- Media support (Image/Video placeholders)
- Clean typography and spacing
- Hover effects for interactive elements