UI 组件 简单
Media Object
Social media style layout with avatar on the left and content (name, text, meta) on the right, supporting nested replies.
在 Lab 中打开
MCP
css javascript vue svelte
目标: TS JS HTML React Vue Svelte
代码
*,
*::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