UI Components Easy
Media Object
Social media style layout with avatar on the left and content (name, text, meta) on the right, supporting nested replies.
Open in Lab
MCP
css javascript vue svelte
Targets: TS JS HTML React Vue Svelte
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: Inter, system-ui, sans-serif;
background: #0a0a0a;
color: #f2f6ff;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: clamp(0.75rem, 3vw, 2rem);
}
.demo {
width: 100%;
max-width: 600px;
}
.demo-title {
font-size: 1.5rem;
font-weight: 800;
margin-bottom: 0.375rem;
}
.demo-sub {
color: #475569;
font-size: 0.875rem;
margin-bottom: 2rem;
}
.media-stack {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
/* ── Media Object ── */
.media {
display: flex;
gap: 0.875rem;
padding: 1rem;
background: #141414;
border: 1px solid #1e1e1e;
border-radius: 0.75rem;
}
.media--nested {
margin-top: 0.875rem;
background: rgba(255, 255, 255, 0.02);
border-color: #1a1a1a;
border-left: 2px solid #2a2a2a;
border-radius: 0 0.75rem 0.75rem 0;
}
/* ── Avatar ── */
.media-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 700;
color: #0a0a0a;
flex-shrink: 0;
user-select: none;
}
.media-avatar--sm {
width: 32px;
height: 32px;
font-size: 0.6875rem;
}
/* ── Body ── */
.media-body {
flex: 1;
min-width: 0;
}
.media-header {
display: flex;
align-items: baseline;
gap: 0.5rem;
margin-bottom: 0.25rem;
}
.media-name {
font-size: 0.875rem;
font-weight: 600;
color: #f2f6ff;
}
.media-time {
font-size: 0.75rem;
color: #4a4a4a;
}
.media-text {
font-size: 0.8125rem;
line-height: 1.6;
color: #94a3b8;
}
/* ── Actions ── */
.media-actions {
display: flex;
gap: 0.75rem;
margin-top: 0.5rem;
}
.media-action {
background: none;
border: none;
font-size: 0.75rem;
font-weight: 500;
font-family: inherit;
color: #4a4a4a;
cursor: pointer;
padding: 0;
transition: color 0.15s;
}
.media-action:hover {
color: #38bdf8;
}/* Media Object is a CSS layout pattern — no JavaScript required. */<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Media Object</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<h1 class="demo-title">Media Object</h1>
<p class="demo-sub">Avatar + content layout with nested replies.</p>
<div class="media-stack">
<!-- Post 1 -->
<div class="media">
<div class="media-avatar" style="background: #38bdf8;">AK</div>
<div class="media-body">
<div class="media-header">
<span class="media-name">Alex Kim</span>
<span class="media-time">2 hours ago</span>
</div>
<p class="media-text">Just shipped the new dashboard redesign. The dark mode looks incredible with the glass morphism cards.</p>
<div class="media-actions">
<button class="media-action">Reply</button>
<button class="media-action">Like</button>
<button class="media-action">Share</button>
</div>
<!-- Nested reply -->
<div class="media media--nested">
<div class="media-avatar media-avatar--sm" style="background: #22c55e;">SR</div>
<div class="media-body">
<div class="media-header">
<span class="media-name">Sara Rivera</span>
<span class="media-time">1 hour ago</span>
</div>
<p class="media-text">Looks amazing! Did you use the new backdrop-filter approach for the cards?</p>
<div class="media-actions">
<button class="media-action">Reply</button>
<button class="media-action">Like</button>
</div>
<!-- Nested reply level 2 -->
<div class="media media--nested">
<div class="media-avatar media-avatar--sm" style="background: #38bdf8;">AK</div>
<div class="media-body">
<div class="media-header">
<span class="media-name">Alex Kim</span>
<span class="media-time">45 min ago</span>
</div>
<p class="media-text">Yes! backdrop-filter with saturate(1.6) gives a really nice effect on dark backgrounds.</p>
<div class="media-actions">
<button class="media-action">Reply</button>
<button class="media-action">Like</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Post 2 -->
<div class="media">
<div class="media-avatar" style="background: #a855f7;">JD</div>
<div class="media-body">
<div class="media-header">
<span class="media-name">Jordan Doe</span>
<span class="media-time">5 hours ago</span>
</div>
<p class="media-text">Anyone have experience with the new View Transitions API? Trying to implement cross-page animations and would love some tips.</p>
<div class="media-actions">
<button class="media-action">Reply</button>
<button class="media-action">Like</button>
<button class="media-action">Share</button>
</div>
<!-- Nested reply -->
<div class="media media--nested">
<div class="media-avatar media-avatar--sm" style="background: #f59e0b;">MP</div>
<div class="media-body">
<div class="media-header">
<span class="media-name">Morgan Park</span>
<span class="media-time">3 hours ago</span>
</div>
<p class="media-text">Check out the Astro docs section on View Transitions -- they have great examples for MPA setups.</p>
<div class="media-actions">
<button class="media-action">Reply</button>
<button class="media-action">Like</button>
</div>
</div>
</div>
</div>
</div>
<!-- Post 3 — simple -->
<div class="media">
<div class="media-avatar" style="background: #ef4444;">TN</div>
<div class="media-body">
<div class="media-header">
<span class="media-name">Taylor Nguyen</span>
<span class="media-time">1 day ago</span>
</div>
<p class="media-text">Published a new blog post on CSS container queries and how they change responsive design patterns. Link in bio.</p>
<div class="media-actions">
<button class="media-action">Reply</button>
<button class="media-action">Like</button>
<button class="media-action">Share</button>
</div>
</div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { ReactNode } from "react";
interface MediaObjectProps {
avatar: string;
avatarColor?: string;
title: string;
time?: string;
content: string;
actions?: string[];
onAction?: (action: string) => void;
children?: ReactNode;
small?: boolean;
}
export function MediaObject({
avatar,
avatarColor = "#38bdf8",
title,
time,
content,
actions = [],
onAction,
children,
small = false,
}: MediaObjectProps) {
const avatarSize = small ? 32 : 40;
const fontSize = small ? "0.6875rem" : "0.75rem";
return (
<div
style={{
display: "flex",
gap: "0.875rem",
padding: "1rem",
background: small ? "rgba(255,255,255,0.02)" : "#141414",
border: small ? "1px solid #1a1a1a" : "1px solid #1e1e1e",
borderLeft: small ? "2px solid #2a2a2a" : "1px solid #1e1e1e",
borderRadius: small ? "0 0.75rem 0.75rem 0" : "0.75rem",
marginTop: small ? "0.875rem" : 0,
}}
>
<div
style={{
width: avatarSize,
height: avatarSize,
borderRadius: "50%",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize,
fontWeight: 700,
color: "#0a0a0a",
background: avatarColor,
flexShrink: 0,
userSelect: "none",
}}
>
{avatar}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
display: "flex",
alignItems: "baseline",
gap: "0.5rem",
marginBottom: "0.25rem",
}}
>
<span style={{ fontSize: "0.875rem", fontWeight: 600, color: "#f2f6ff" }}>{title}</span>
{time && <span style={{ fontSize: "0.75rem", color: "#4a4a4a" }}>{time}</span>}
</div>
<p style={{ fontSize: "0.8125rem", lineHeight: 1.6, color: "#94a3b8", margin: 0 }}>
{content}
</p>
{actions.length > 0 && (
<div style={{ display: "flex", gap: "0.75rem", marginTop: "0.5rem" }}>
{actions.map((a) => (
<button
key={a}
type="button"
onClick={() => onAction?.(a)}
style={{
background: "none",
border: "none",
fontSize: "0.75rem",
fontWeight: 500,
fontFamily: "inherit",
color: "#4a4a4a",
cursor: "pointer",
padding: 0,
}}
>
{a}
</button>
))}
</div>
)}
{children}
</div>
</div>
);
}
/* Demo */
export default function MediaObjectDemo() {
return (
<div
style={{
minHeight: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "#0a0a0a",
fontFamily: "Inter, system-ui, sans-serif",
color: "#f2f6ff",
padding: "2rem",
}}
>
<div
style={{
width: "100%",
maxWidth: 600,
display: "flex",
flexDirection: "column",
gap: "1.25rem",
}}
>
<h1 style={{ fontSize: "1.5rem", fontWeight: 800, marginBottom: "0.375rem" }}>
Media Object
</h1>
<p style={{ color: "#475569", fontSize: "0.875rem", marginBottom: "0.5rem" }}>
Avatar + content layout with nested replies.
</p>
{/* Post 1 with nested replies */}
<MediaObject
avatar="AK"
avatarColor="#38bdf8"
title="Alex Kim"
time="2 hours ago"
content="Just shipped the new dashboard redesign. The dark mode looks incredible with the glass morphism cards."
actions={["Reply", "Like", "Share"]}
>
<MediaObject
small
avatar="SR"
avatarColor="#22c55e"
title="Sara Rivera"
time="1 hour ago"
content="Looks amazing! Did you use the new backdrop-filter approach for the cards?"
actions={["Reply", "Like"]}
>
<MediaObject
small
avatar="AK"
avatarColor="#38bdf8"
title="Alex Kim"
time="45 min ago"
content="Yes! backdrop-filter with saturate(1.6) gives a really nice effect on dark backgrounds."
actions={["Reply", "Like"]}
/>
</MediaObject>
</MediaObject>
{/* Post 2 with a reply */}
<MediaObject
avatar="JD"
avatarColor="#a855f7"
title="Jordan Doe"
time="5 hours ago"
content="Anyone have experience with the new View Transitions API? Trying to implement cross-page animations."
actions={["Reply", "Like", "Share"]}
>
<MediaObject
small
avatar="MP"
avatarColor="#f59e0b"
title="Morgan Park"
time="3 hours ago"
content="Check out the Astro docs section on View Transitions -- great examples for MPA setups."
actions={["Reply", "Like"]}
/>
</MediaObject>
{/* Post 3 — simple */}
<MediaObject
avatar="TN"
avatarColor="#ef4444"
title="Taylor Nguyen"
time="1 day ago"
content="Published a new blog post on CSS container queries and how they change responsive design patterns. Link in bio."
actions={["Reply", "Like", "Share"]}
/>
</div>
</div>
);
}<script setup>
import { ref } from "vue";
const posts = ref([
{
avatar: "AK",
avatarColor: "#38bdf8",
title: "Alex Kim",
time: "2 hours ago",
content:
"Just shipped the new dashboard redesign. The dark mode looks incredible with the glass morphism cards.",
actions: ["Reply", "Like", "Share"],
replies: [
{
avatar: "SR",
avatarColor: "#22c55e",
title: "Sara Rivera",
time: "1 hour ago",
content: "Looks amazing! Did you use the new backdrop-filter approach for the cards?",
actions: ["Reply", "Like"],
replies: [
{
avatar: "AK",
avatarColor: "#38bdf8",
title: "Alex Kim",
time: "45 min ago",
content:
"Yes! backdrop-filter with saturate(1.6) gives a really nice effect on dark backgrounds.",
actions: ["Reply", "Like"],
replies: [],
},
],
},
],
},
{
avatar: "JD",
avatarColor: "#a855f7",
title: "Jordan Doe",
time: "5 hours ago",
content:
"Anyone have experience with the new View Transitions API? Trying to implement cross-page animations.",
actions: ["Reply", "Like", "Share"],
replies: [
{
avatar: "MP",
avatarColor: "#f59e0b",
title: "Morgan Park",
time: "3 hours ago",
content:
"Check out the Astro docs section on View Transitions -- great examples for MPA setups.",
actions: ["Reply", "Like"],
replies: [],
},
],
},
{
avatar: "TN",
avatarColor: "#ef4444",
title: "Taylor Nguyen",
time: "1 day ago",
content:
"Published a new blog post on CSS container queries and how they change responsive design patterns. Link in bio.",
actions: ["Reply", "Like", "Share"],
replies: [],
},
]);
</script>
<template>
<div style="min-height:100vh;display:flex;align-items:center;justify-content:center;background:#0a0a0a;font-family:Inter,system-ui,sans-serif;color:#f2f6ff;padding:2rem">
<div style="width:100%;max-width:600px;display:flex;flex-direction:column;gap:1.25rem">
<h1 style="font-size:1.5rem;font-weight:800;margin-bottom:0.375rem">Media Object</h1>
<p style="color:#475569;font-size:0.875rem;margin-bottom:0.5rem">Avatar + content layout with nested replies.</p>
<template v-for="(post, pi) in posts" :key="pi">
<div style="display:flex;gap:0.875rem;padding:1rem;background:#141414;border:1px solid #1e1e1e;border-radius:0.75rem">
<div :style="{ width:'40px',height:'40px',borderRadius:'50%',display:'flex',alignItems:'center',justifyContent:'center',fontSize:'0.75rem',fontWeight:'700',color:'#0a0a0a',background:post.avatarColor,flexShrink:'0',userSelect:'none' }">{{ post.avatar }}</div>
<div style="flex:1;min-width:0">
<div style="display:flex;align-items:baseline;gap:0.5rem;margin-bottom:0.25rem">
<span style="font-size:0.875rem;font-weight:600;color:#f2f6ff">{{ post.title }}</span>
<span style="font-size:0.75rem;color:#4a4a4a">{{ post.time }}</span>
</div>
<p style="font-size:0.8125rem;line-height:1.6;color:#94a3b8;margin:0">{{ post.content }}</p>
<div v-if="post.actions.length" style="display:flex;gap:0.75rem;margin-top:0.5rem">
<button v-for="a in post.actions" :key="a" style="background:none;border:none;font-size:0.75rem;font-weight:500;color:#4a4a4a;cursor:pointer;padding:0;font-family:inherit">{{ a }}</button>
</div>
<!-- Nested reply level 1 -->
<template v-for="(reply, ri) in post.replies" :key="ri">
<div style="display:flex;gap:0.875rem;padding:1rem;background:rgba(255,255,255,0.02);border:1px solid #1a1a1a;border-left:2px solid #2a2a2a;border-radius:0 0.75rem 0.75rem 0;margin-top:0.875rem">
<div :style="{ width:'32px',height:'32px',borderRadius:'50%',display:'flex',alignItems:'center',justifyContent:'center',fontSize:'0.6875rem',fontWeight:'700',color:'#0a0a0a',background:reply.avatarColor,flexShrink:'0',userSelect:'none' }">{{ reply.avatar }}</div>
<div style="flex:1;min-width:0">
<div style="display:flex;align-items:baseline;gap:0.5rem;margin-bottom:0.25rem">
<span style="font-size:0.875rem;font-weight:600;color:#f2f6ff">{{ reply.title }}</span>
<span style="font-size:0.75rem;color:#4a4a4a">{{ reply.time }}</span>
</div>
<p style="font-size:0.8125rem;line-height:1.6;color:#94a3b8;margin:0">{{ reply.content }}</p>
<div v-if="reply.actions.length" style="display:flex;gap:0.75rem;margin-top:0.5rem">
<button v-for="a in reply.actions" :key="a" style="background:none;border:none;font-size:0.75rem;font-weight:500;color:#4a4a4a;cursor:pointer;padding:0;font-family:inherit">{{ a }}</button>
</div>
<!-- Nested reply level 2 -->
<template v-for="(sub, si) in reply.replies" :key="si">
<div style="display:flex;gap:0.875rem;padding:1rem;background:rgba(255,255,255,0.02);border:1px solid #1a1a1a;border-left:2px solid #2a2a2a;border-radius:0 0.75rem 0.75rem 0;margin-top:0.875rem">
<div :style="{ width:'32px',height:'32px',borderRadius:'50%',display:'flex',alignItems:'center',justifyContent:'center',fontSize:'0.6875rem',fontWeight:'700',color:'#0a0a0a',background:sub.avatarColor,flexShrink:'0',userSelect:'none' }">{{ sub.avatar }}</div>
<div style="flex:1;min-width:0">
<div style="display:flex;align-items:baseline;gap:0.5rem;margin-bottom:0.25rem">
<span style="font-size:0.875rem;font-weight:600;color:#f2f6ff">{{ sub.title }}</span>
<span style="font-size:0.75rem;color:#4a4a4a">{{ sub.time }}</span>
</div>
<p style="font-size:0.8125rem;line-height:1.6;color:#94a3b8;margin:0">{{ sub.content }}</p>
<div v-if="sub.actions.length" style="display:flex;gap:0.75rem;margin-top:0.5rem">
<button v-for="a in sub.actions" :key="a" style="background:none;border:none;font-size:0.75rem;font-weight:500;color:#4a4a4a;cursor:pointer;padding:0;font-family:inherit">{{ a }}</button>
</div>
</div>
</div>
</template>
</div>
</div>
</template>
</div>
</div>
</template>
</div>
</div>
</template><script>
const posts = [
{
avatar: "AK",
avatarColor: "#38bdf8",
title: "Alex Kim",
time: "2 hours ago",
content:
"Just shipped the new dashboard redesign. The dark mode looks incredible with the glass morphism cards.",
actions: ["Reply", "Like", "Share"],
replies: [
{
avatar: "SR",
avatarColor: "#22c55e",
title: "Sara Rivera",
time: "1 hour ago",
content: "Looks amazing! Did you use the new backdrop-filter approach for the cards?",
actions: ["Reply", "Like"],
replies: [
{
avatar: "AK",
avatarColor: "#38bdf8",
title: "Alex Kim",
time: "45 min ago",
content:
"Yes! backdrop-filter with saturate(1.6) gives a really nice effect on dark backgrounds.",
actions: ["Reply", "Like"],
},
],
},
],
},
{
avatar: "JD",
avatarColor: "#a855f7",
title: "Jordan Doe",
time: "5 hours ago",
content:
"Anyone have experience with the new View Transitions API? Trying to implement cross-page animations.",
actions: ["Reply", "Like", "Share"],
replies: [
{
avatar: "MP",
avatarColor: "#f59e0b",
title: "Morgan Park",
time: "3 hours ago",
content:
"Check out the Astro docs section on View Transitions -- great examples for MPA setups.",
actions: ["Reply", "Like"],
replies: [],
},
],
},
{
avatar: "TN",
avatarColor: "#ef4444",
title: "Taylor Nguyen",
time: "1 day ago",
content:
"Published a new blog post on CSS container queries and how they change responsive design patterns. Link in bio.",
actions: ["Reply", "Like", "Share"],
replies: [],
},
];
</script>
<div style="min-height:100vh;display:flex;align-items:center;justify-content:center;background:#0a0a0a;font-family:Inter,system-ui,sans-serif;color:#f2f6ff;padding:2rem">
<div style="width:100%;max-width:600px;display:flex;flex-direction:column;gap:1.25rem">
<h1 style="font-size:1.5rem;font-weight:800;margin-bottom:0.375rem">Media Object</h1>
<p style="color:#475569;font-size:0.875rem;margin-bottom:0.5rem">Avatar + content layout with nested replies.</p>
{#each posts as post}
<div style="display:flex;gap:0.875rem;padding:1rem;background:#141414;border:1px solid #1e1e1e;border-radius:0.75rem">
<div style="width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:0.75rem;font-weight:700;color:#0a0a0a;background:{post.avatarColor};flex-shrink:0;user-select:none">{post.avatar}</div>
<div style="flex:1;min-width:0">
<div style="display:flex;align-items:baseline;gap:0.5rem;margin-bottom:0.25rem">
<span style="font-size:0.875rem;font-weight:600;color:#f2f6ff">{post.title}</span>
<span style="font-size:0.75rem;color:#4a4a4a">{post.time}</span>
</div>
<p style="font-size:0.8125rem;line-height:1.6;color:#94a3b8;margin:0">{post.content}</p>
{#if post.actions.length}
<div style="display:flex;gap:0.75rem;margin-top:0.5rem">
{#each post.actions as a}
<button style="background:none;border:none;font-size:0.75rem;font-weight:500;color:#4a4a4a;cursor:pointer;padding:0;font-family:inherit">{a}</button>
{/each}
</div>
{/if}
{#each post.replies as reply}
<div style="display:flex;gap:0.875rem;padding:1rem;background:rgba(255,255,255,0.02);border:1px solid #1a1a1a;border-left:2px solid #2a2a2a;border-radius:0 0.75rem 0.75rem 0;margin-top:0.875rem">
<div style="width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:0.6875rem;font-weight:700;color:#0a0a0a;background:{reply.avatarColor};flex-shrink:0;user-select:none">{reply.avatar}</div>
<div style="flex:1;min-width:0">
<div style="display:flex;align-items:baseline;gap:0.5rem;margin-bottom:0.25rem">
<span style="font-size:0.875rem;font-weight:600;color:#f2f6ff">{reply.title}</span>
<span style="font-size:0.75rem;color:#4a4a4a">{reply.time}</span>
</div>
<p style="font-size:0.8125rem;line-height:1.6;color:#94a3b8;margin:0">{reply.content}</p>
{#if reply.actions.length}
<div style="display:flex;gap:0.75rem;margin-top:0.5rem">
{#each reply.actions as a}
<button style="background:none;border:none;font-size:0.75rem;font-weight:500;color:#4a4a4a;cursor:pointer;padding:0;font-family:inherit">{a}</button>
{/each}
</div>
{/if}
{#if reply.replies}
{#each reply.replies as sub}
<div style="display:flex;gap:0.875rem;padding:1rem;background:rgba(255,255,255,0.02);border:1px solid #1a1a1a;border-left:2px solid #2a2a2a;border-radius:0 0.75rem 0.75rem 0;margin-top:0.875rem">
<div style="width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:0.6875rem;font-weight:700;color:#0a0a0a;background:{sub.avatarColor};flex-shrink:0;user-select:none">{sub.avatar}</div>
<div style="flex:1;min-width:0">
<div style="display:flex;align-items:baseline;gap:0.5rem;margin-bottom:0.25rem">
<span style="font-size:0.875rem;font-weight:600;color:#f2f6ff">{sub.title}</span>
<span style="font-size:0.75rem;color:#4a4a4a">{sub.time}</span>
</div>
<p style="font-size:0.8125rem;line-height:1.6;color:#94a3b8;margin:0">{sub.content}</p>
{#if sub.actions && sub.actions.length}
<div style="display:flex;gap:0.75rem;margin-top:0.5rem">
{#each sub.actions as a}
<button style="background:none;border:none;font-size:0.75rem;font-weight:500;color:#4a4a4a;cursor:pointer;padding:0;font-family:inherit">{a}</button>
{/each}
</div>
{/if}
</div>
</div>
{/each}
{/if}
</div>
</div>
{/each}
</div>
</div>
{/each}
</div>
</div>Media Object
A classic social media layout pattern — avatar on the left, content block on the right — with support for nested replies.
Features
- Flexbox layout with avatar and content areas
- Configurable avatar sizes
- Nested media objects for reply threads
- Meta line with timestamp and actions
- Dark theme styling