Blog Card
Original · freeA blog post card with cover image, tag and meta row.
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/card-blog.json"use client";
import { useState } from "react";
import { motion, AnimatePresence, useReducedMotion } from "motion/react";
type Post = {
id: string;
img: string;
tag: string;
tagTone: "indigo" | "emerald" | "rose" | "amber" | "sky" | "violet";
title: string;
excerpt: string;
author: string;
initials: string;
date: string;
readMins: number;
href: string;
};
const POSTS: Post[] = [
{
id: "rsc",
img: "/img/gallery/g07.webp",
tag: "Engineering",
tagTone: "indigo",
title: "React Server Components in production: what actually broke",
excerpt:
"We shipped RSC to 40M monthly users. Here are the caching gotchas, the streaming wins, and the three patterns we now ban in code review.",
author: "Priya Nair",
initials: "PN",
date: "Jul 9, 2026",
readMins: 11,
href: "https://example.com/blog/rsc-in-production",
},
{
id: "css",
img: "/img/gallery/g14.webp",
tag: "Design",
tagTone: "violet",
title: "Container queries killed our breakpoint spaghetti",
excerpt:
"One component, four contexts, zero media queries. A field report on rebuilding a design system around the element, not the viewport.",
author: "Marcus Feld",
initials: "MF",
date: "Jun 28, 2026",
readMins: 7,
href: "https://example.com/blog/container-queries",
},
{
id: "pg",
img: "/img/gallery/g22.webp",
tag: "Databases",
tagTone: "emerald",
title: "Postgres at 2 billion rows without a rewrite",
excerpt:
"Partial indexes, BRIN, and the partitioning strategy that let us skip the Cassandra migration our board kept asking for.",
author: "Lena Osei",
initials: "LO",
date: "Jun 15, 2026",
readMins: 14,
href: "https://example.com/blog/postgres-2b-rows",
},
];
const TONE: Record<
Post["tagTone"],
{ chip: string; dot: string }
> = {
indigo: {
chip:
"bg-indigo-50 text-indigo-700 ring-indigo-200 dark:bg-indigo-500/10 dark:text-indigo-300 dark:ring-indigo-400/20",
dot: "bg-indigo-500",
},
emerald: {
chip:
"bg-emerald-50 text-emerald-700 ring-emerald-200 dark:bg-emerald-500/10 dark:text-emerald-300 dark:ring-emerald-400/20",
dot: "bg-emerald-500",
},
rose: {
chip:
"bg-rose-50 text-rose-700 ring-rose-200 dark:bg-rose-500/10 dark:text-rose-300 dark:ring-rose-400/20",
dot: "bg-rose-500",
},
amber: {
chip:
"bg-amber-50 text-amber-800 ring-amber-200 dark:bg-amber-500/10 dark:text-amber-300 dark:ring-amber-400/20",
dot: "bg-amber-500",
},
sky: {
chip:
"bg-sky-50 text-sky-700 ring-sky-200 dark:bg-sky-500/10 dark:text-sky-300 dark:ring-sky-400/20",
dot: "bg-sky-500",
},
violet: {
chip:
"bg-violet-50 text-violet-700 ring-violet-200 dark:bg-violet-500/10 dark:text-violet-300 dark:ring-violet-400/20",
dot: "bg-violet-500",
},
};
function HeartIcon({ filled }: { filled: boolean }) {
return (
<svg
viewBox="0 0 24 24"
className="h-[18px] w-[18px]"
fill={filled ? "currentColor" : "none"}
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="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 1 0-7.8 7.8l1 1L12 21l7.8-7.6 1-1a5.5 5.5 0 0 0 0-7.8Z" />
</svg>
);
}
function BookmarkIcon({ filled }: { filled: boolean }) {
return (
<svg
viewBox="0 0 24 24"
className="h-[18px] w-[18px]"
fill={filled ? "currentColor" : "none"}
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M6 3h12a1 1 0 0 1 1 1v17l-7-4-7 4V4a1 1 0 0 1 1-1Z" />
</svg>
);
}
function LinkIcon() {
return (
<svg
viewBox="0 0 24 24"
className="h-[18px] w-[18px]"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M10 13a5 5 0 0 0 7.07 0l2.83-2.83a5 5 0 0 0-7.07-7.07L11.5 4.5" />
<path d="M14 11a5 5 0 0 0-7.07 0L4.1 13.83a5 5 0 0 0 7.07 7.07L12.5 19.5" />
</svg>
);
}
function CheckIcon() {
return (
<svg
viewBox="0 0 24 24"
className="h-[18px] w-[18px]"
fill="none"
stroke="currentColor"
strokeWidth={2.4}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="m5 13 4 4L19 7" />
</svg>
);
}
function ArrowIcon() {
return (
<svg
viewBox="0 0 24 24"
className="h-4 w-4"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M5 12h14" />
<path d="m13 6 6 6-6 6" />
</svg>
);
}
const iconBtn =
"inline-flex h-9 w-9 items-center justify-center rounded-full text-slate-500 transition-colors outline-none hover:bg-slate-100 hover:text-slate-800 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-slate-400 dark:hover:bg-white/10 dark:hover:text-slate-100 dark:focus-visible:ring-offset-slate-900";
function TagChip({ post }: { post: Post }) {
const tone = TONE[post.tagTone];
return (
<span
className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-semibold ring-1 ring-inset ${tone.chip}`}
>
<span className={`h-1.5 w-1.5 rounded-full ${tone.dot}`} aria-hidden="true" />
{post.tag}
</span>
);
}
function Avatar({ post }: { post: Post }) {
const tone = TONE[post.tagTone];
return (
<span
className={`inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[11px] font-bold text-white ${tone.dot}`}
aria-hidden="true"
>
{post.initials}
</span>
);
}
function CardActions({ post }: { post: Post }) {
const reduce = useReducedMotion();
const [liked, setLiked] = useState(false);
const [saved, setSaved] = useState(false);
const [copied, setCopied] = useState(false);
const [likeBurst, setLikeBurst] = useState(0);
async function copyLink() {
try {
await navigator.clipboard.writeText(post.href);
setCopied(true);
window.setTimeout(() => setCopied(false), 1600);
} catch {
setCopied(false);
}
}
function toggleLike() {
setLiked((v) => {
if (!v) setLikeBurst((n) => n + 1);
return !v;
});
}
return (
<div className="flex items-center gap-1">
<button
type="button"
onClick={toggleLike}
aria-pressed={liked}
aria-label={liked ? "Unlike this post" : "Like this post"}
className={`${iconBtn} relative ${
liked ? "text-rose-500 hover:text-rose-600 dark:text-rose-400" : ""
}`}
>
<motion.span
key={likeBurst}
initial={reduce ? false : { scale: 1 }}
animate={reduce ? undefined : liked ? { scale: [1, 1.35, 1] } : { scale: 1 }}
transition={{ duration: 0.32, ease: "easeOut" }}
className="inline-flex"
>
<HeartIcon filled={liked} />
</motion.span>
</button>
<button
type="button"
onClick={() => setSaved((v) => !v)}
aria-pressed={saved}
aria-label={saved ? "Remove from saved" : "Save for later"}
className={`${iconBtn} ${
saved ? "text-amber-500 hover:text-amber-600 dark:text-amber-400" : ""
}`}
>
<BookmarkIcon filled={saved} />
</button>
<button
type="button"
onClick={copyLink}
aria-label={copied ? "Link copied" : "Copy article link"}
className={`${iconBtn} ${copied ? "text-emerald-500 dark:text-emerald-400" : ""}`}
>
<AnimatePresence mode="wait" initial={false}>
<motion.span
key={copied ? "done" : "copy"}
initial={reduce ? false : { opacity: 0, scale: 0.6 }}
animate={{ opacity: 1, scale: 1 }}
exit={reduce ? undefined : { opacity: 0, scale: 0.6 }}
transition={{ duration: 0.14 }}
className="inline-flex"
>
{copied ? <CheckIcon /> : <LinkIcon />}
</motion.span>
</AnimatePresence>
</button>
</div>
);
}
function MetaRow({ post }: { post: Post }) {
return (
<div className="flex items-center gap-2 text-xs text-slate-500 dark:text-slate-400">
<Avatar post={post} />
<span className="font-medium text-slate-700 dark:text-slate-300">
{post.author}
</span>
<span aria-hidden="true" className="text-slate-300 dark:text-slate-600">
·
</span>
<time>{post.date}</time>
<span aria-hidden="true" className="text-slate-300 dark:text-slate-600">
·
</span>
<span>{post.readMins} min read</span>
</div>
);
}
function VerticalCard({ post }: { post: Post }) {
return (
<article className="group relative flex flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm transition-all duration-300 hover:-translate-y-1 hover:shadow-xl hover:shadow-slate-900/10 dark:border-white/10 dark:bg-slate-900 dark:hover:shadow-black/40">
<div className="relative aspect-[16/10] overflow-hidden bg-slate-100 dark:bg-slate-800">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={post.img}
alt={`Cover illustration for ${post.title}`}
loading="lazy"
draggable={false}
className="cardblog-fade h-full w-full object-cover transition-transform duration-500 ease-out group-hover:scale-[1.05]"
/>
<div className="absolute left-3 top-3">
<TagChip post={post} />
</div>
</div>
<div className="flex flex-1 flex-col p-5">
<a
href={post.href}
target="_blank"
rel="noopener"
className="rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900"
>
<h3 className="text-lg font-semibold leading-snug tracking-tight text-slate-900 decoration-2 underline-offset-2 group-hover:underline dark:text-white">
{post.title}
</h3>
</a>
<p className="mt-2 line-clamp-2 text-sm leading-relaxed text-slate-600 dark:text-slate-400">
{post.excerpt}
</p>
<div className="mt-4 border-t border-slate-100 pt-4 dark:border-white/10">
<MetaRow post={post} />
</div>
<div className="mt-4 flex items-center justify-between">
<a
href={post.href}
target="_blank"
rel="noopener"
className="inline-flex items-center gap-1.5 rounded-md text-sm font-semibold text-indigo-600 outline-none transition-colors hover:text-indigo-700 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-indigo-400 dark:hover:text-indigo-300 dark:focus-visible:ring-offset-slate-900"
>
Read article
<span className="transition-transform duration-200 group-hover:translate-x-0.5">
<ArrowIcon />
</span>
</a>
<CardActions post={post} />
</div>
</div>
</article>
);
}
function HorizontalCard({ post }: { post: Post }) {
return (
<article className="group relative flex flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm transition-all duration-300 hover:shadow-lg hover:shadow-slate-900/10 sm:flex-row dark:border-white/10 dark:bg-slate-900 dark:hover:shadow-black/40">
<div className="relative aspect-[16/10] overflow-hidden bg-slate-100 sm:aspect-auto sm:w-56 sm:shrink-0 dark:bg-slate-800">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={post.img}
alt={`Cover illustration for ${post.title}`}
loading="lazy"
draggable={false}
className="h-full w-full object-cover transition-transform duration-500 ease-out group-hover:scale-[1.05]"
/>
</div>
<div className="flex flex-1 flex-col p-5">
<div>
<TagChip post={post} />
</div>
<a
href={post.href}
target="_blank"
rel="noopener"
className="mt-3 rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900"
>
<h3 className="text-lg font-semibold leading-snug tracking-tight text-slate-900 decoration-2 underline-offset-2 group-hover:underline dark:text-white">
{post.title}
</h3>
</a>
<p className="mt-2 line-clamp-2 text-sm leading-relaxed text-slate-600 dark:text-slate-400">
{post.excerpt}
</p>
<div className="mt-auto flex items-center justify-between pt-4">
<MetaRow post={post} />
<CardActions post={post} />
</div>
</div>
</article>
);
}
function CompactCard({ post }: { post: Post }) {
return (
<article className="group relative flex items-center gap-4 rounded-xl border border-slate-200 bg-white p-3 shadow-sm transition-colors hover:border-slate-300 hover:bg-slate-50 dark:border-white/10 dark:bg-slate-900 dark:hover:border-white/20 dark:hover:bg-slate-800/60">
<div className="relative h-16 w-16 shrink-0 overflow-hidden rounded-lg bg-slate-100 dark:bg-slate-800">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={post.img}
alt=""
loading="lazy"
draggable={false}
className="h-full w-full object-cover"
/>
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 text-[11px] font-semibold uppercase tracking-wide">
<span className={`h-1.5 w-1.5 rounded-full ${TONE[post.tagTone].dot}`} aria-hidden="true" />
<span className="text-slate-500 dark:text-slate-400">{post.tag}</span>
<span aria-hidden="true" className="text-slate-300 dark:text-slate-600">
·
</span>
<span className="text-slate-400 dark:text-slate-500">{post.readMins} min</span>
</div>
<a
href={post.href}
target="_blank"
rel="noopener"
className="mt-1 block rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900"
>
<h3 className="truncate text-sm font-semibold text-slate-900 group-hover:text-indigo-600 dark:text-white dark:group-hover:text-indigo-400">
{post.title}
</h3>
</a>
</div>
<span className="text-slate-300 transition-transform duration-200 group-hover:translate-x-0.5 group-hover:text-slate-500 dark:text-slate-600 dark:group-hover:text-slate-300">
<ArrowIcon />
</span>
</article>
);
}
export default function CardBlog() {
return (
<section className="relative w-full bg-slate-50 px-4 py-16 sm:px-6 sm:py-24 dark:bg-slate-950">
<style>{`
@keyframes cardblog-rise {
from { opacity: 0; transform: translateY(14px); }
to { opacity: 1; transform: translateY(0); }
}
.cardblog-fade { animation: cardblog-rise 0.5s ease-out both; }
@media (prefers-reduced-motion: reduce) {
.cardblog-fade { animation: none !important; }
}
`}</style>
<div className="mx-auto max-w-6xl">
<header className="mx-auto max-w-2xl text-center">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-indigo-600 dark:text-indigo-400">
From the blog
</p>
<h2 className="mt-3 text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl dark:text-white">
Field notes on shipping fast
</h2>
<p className="mt-3 text-base leading-relaxed text-slate-600 dark:text-slate-400">
Post cards with cover art, a topic tag and an author meta row. Like,
save and copy the link right from the card.
</p>
</header>
<div className="mt-12 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{POSTS.map((post) => (
<VerticalCard key={post.id} post={post} />
))}
</div>
<div className="mt-8">
<HorizontalCard post={POSTS[0]} />
</div>
<div className="mx-auto mt-8 grid max-w-3xl grid-cols-1 gap-3 sm:grid-cols-2">
{POSTS.map((post) => (
<CompactCard key={post.id} post={post} />
))}
</div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
3D Tilt Cards
OriginalA responsive grid of cards that tilt in 3D toward your cursor with a light glare and lifted depth layers, powered by Framer Motion springs.

Spotlight Follow Cards
OriginalFeature cards where a soft radial spotlight and a glowing border chase the cursor on hover, with a staggered scroll reveal and an animated shimmer line.

3D Flip Cards
OriginalPricing-style cards that flip in 3D on hover or tap to reveal details on the back, keyboard accessible with a staggered entrance animation.

Animated Gradient Border Cards
OriginalCards wrapped in a live conic gradient border that rotates and speeds up on hover, with a shimmer sweep and a scrolling tag marquee.

Glow Effect Card
primitivesA reusable glow effect card for React and Tailwind, with hover and motion.

Neon Gradient Card
gradient-card.tsxA reusable neon gradient card for React and Tailwind, with hover and motion.

Spotlight Magic Card
card.tsxA reusable spotlight magic card for React and Tailwind, with hover and motion.

Tilt Spotlight Card
primitivesA reusable tilt spotlight card for React and Tailwind, with hover and motion.

Profile Card
OriginalA user profile card with avatar, stats and a follow toggle.

Product Card
OriginalA product card with image, price and add-to-cart feedback.

Pricing Single Card
OriginalA single pricing plan card with feature list and CTA.

Stat Card
OriginalA metric card with value, trend arrow and a small sparkline.

