# Three Card Post Grid

> Blog post grid of three cards with cover images, category badges, titles, excerpts, read time, and author avatars.

A shadcn/ui block from the [@7ovr](https://7ovr.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/7ovr/blog-1).

- Page, with a live preview: https://shadcn.exchange/r/7ovr/blog-1
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/7ovr/blog-1-light-d0a6212622c83ab5.png
- Registry item: https://7ovr.com/r/blog-1.json
- Categories: blog
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://7ovr.com/r/blog-1.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@base-ui/react`
- Registry items: `avatar`, `badge`, `button`, `card`, `separator`

## Files

### `registry/blocks/blog/1/blog-block.tsx`

```tsx
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { Separator } from "@/components/ui/separator"
import { IconPlaceholder } from "@/components/icons/icon-placeholder"

const posts = [
  {
    category: "Engineering",
    title: "How we cut API latency by 60% with edge caching",
    excerpt:
      "Our monolith was fast enough, until it wasn't. We traced the bottleneck to cold database reads on every request and rearchitected our caching layer in three weeks.",
    author: {
      name: "Lena Park",
      initials: "LP",
      avatar: "https://i.pravatar.cc/150?img=45",
    },
    date: "Jun 9, 2026",
    readTime: "7 Min Read",
    image:
      "https://images.unsplash.com/photo-1558494949-ef010cbdcc31?w=800&q=80",
    imageAlt: "Server rack with blinking lights in a data center",
  },
  {
    category: "Design",
    title: "Building a token system that survives a rebrand",
    excerpt:
      "Semantic tokens feel abstract until the day your brand color changes. We share the naming conventions and tooling that let us ship a full rebrand in under two days.",
    author: {
      name: "Marcus Webb",
      initials: "MW",
      avatar: "https://i.pravatar.cc/150?img=12",
    },
    date: "May 28, 2026",
    readTime: "5 Min Read",
    image:
      "https://images.unsplash.com/photo-1561070791-2526d30994b5?w=800&q=80",
    imageAlt: "Colour swatches and design tokens spread on a desk",
  },
  {
    category: "Product",
    title: "What 1,200 user interviews taught us about onboarding",
    excerpt:
      "Drop-off at step two turned out to have nothing to do with the UI. Listening to customers revealed a mismatch between our mental model and theirs. Here's how we fixed it.",
    author: {
      name: "Sofia Andrade",
      initials: "SA",
      avatar: "https://i.pravatar.cc/150?img=32",
    },
    date: "May 14, 2026",
    readTime: "9 Min Read",
    image:
      "https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=800&q=80",
    imageAlt: "Two people in a user research interview session",
  },
]

export default function BlogBlock() {
  return (
    <section className="flex w-full items-center justify-center bg-background px-6 py-16 text-foreground">
      <div className="mx-auto w-full max-w-5xl">
        <div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
          <div className="flex flex-col gap-1.5">
            <span className="text-xs font-semibold tracking-[0.18em] text-muted-foreground uppercase">
              Blog
            </span>
            <h2 className="font-heading text-3xl font-bold tracking-tight sm:text-4xl">
              From the Acme team
            </h2>
          </div>
          <Button
            variant="outline"
            size="sm"
            className="group w-fit gap-1.5 transition-colors"
          >
            All Articles
            <IconPlaceholder
              lucide="ArrowRight"
              tabler="IconArrowRight"
              hugeicons="ArrowRightIcon"
              phosphor="ArrowRight"
              remixicon="RiArrowRightLine"
              className="transition-transform duration-200 group-hover:translate-x-0.5"
              size={15}
            />
          </Button>
        </div>

        <Separator className="my-8" />

        <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3">
          {posts.map((post) => (
            <Card
              key={post.title}
              className="group flex flex-col overflow-hidden border-border pt-0 transition-shadow duration-200 hover:shadow-md"
            >
              <div className="aspect-[16/9] w-full overflow-hidden border-b border-border bg-muted">
                <img
                  src={post.image}
                  alt={post.imageAlt}
                  className="size-full object-cover transition-transform duration-300 group-hover:scale-[1.03]"
                />
              </div>

              <CardHeader className="gap-2 pb-3">
                <div className="flex items-center justify-between gap-2">
                  <Badge
                    variant="secondary"
                    className="text-[11px] font-semibold tracking-wide uppercase"
                  >
                    {post.category}
                  </Badge>
                  <span className="flex shrink-0 items-center gap-1 text-xs leading-none text-muted-foreground">
                    <IconPlaceholder
                      lucide="Clock"
                      tabler="IconClock"
                      hugeicons="TimeIcon"
                      phosphor="Clock"
                      remixicon="RiTimeLine"
                      size={11}
                      className="shrink-0"
                    />
                    {post.readTime}
                  </span>
                </div>
                <CardTitle className="text-base leading-snug font-semibold">
                  {post.title}
                </CardTitle>
                <CardDescription className="line-clamp-3 text-sm leading-relaxed">
                  {post.excerpt}
                </CardDescription>
              </CardHeader>

              <CardContent className="mt-auto pt-0 pb-0" />

              <CardFooter className="border-t border-border pt-4 pb-4">
                <div className="flex w-full items-center gap-3">
                  <Avatar size="sm">
                    <AvatarImage
                      src={post.author.avatar}
                      alt={post.author.name}
                      className="grayscale"
                    />
                    <AvatarFallback className="text-xs">
                      {post.author.initials}
                    </AvatarFallback>
                  </Avatar>
                  <div className="flex flex-1 flex-col gap-0.5 overflow-hidden">
                    <span className="truncate text-xs font-semibold text-foreground">
                      {post.author.name}
                    </span>
                    <span className="text-xs text-muted-foreground">
                      <time dateTime={post.date}>{post.date}</time>
                    </span>
                  </div>
                  <IconPlaceholder
                    lucide="ArrowRight"
                    tabler="IconArrowRight"
                    hugeicons="ArrowRightIcon"
                    phosphor="ArrowRight"
                    remixicon="RiArrowRightLine"
                    size={14}
                    className="shrink-0 text-muted-foreground opacity-0 transition-opacity duration-200 group-hover:opacity-100"
                    aria-hidden="true"
                  />
                </div>
              </CardFooter>
            </Card>
          ))}
        </div>
      </div>
    </section>
  )
}
```
