Skip to content
goodtabs UI
BrowseBrowse components

Image card

Media on top in a fixed aspect, with a caption and one mono meta line below.

npx shadcn@latest add https://ui.goodtabs.co/r/image-card.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/image-card

Dashboard, dark theme

1440 x 900, PNG

No media passed, so the 16:9 slot shows the hairline placeholder.

Square, in a grid

Icon set

124 icons

Logo marks

3 variants

Social cards

1200 x 630

Three square cards side by side. The slot keeps its ratio at any width.

With a link title

Registry installs

1,284

Stat card

Added 29 Oct 2026

A 4:3 slot holding a small live element, and a title that links somewhere.

Usage

import { ImageCard } from "@/components/ui/image-card"

<ImageCard title="Dashboard, dark theme" meta="1440 x 900" media={<img src="/shots/dashboard.png" alt="" />} />

Props

PropTypeDefaultDescription
mediaReactNodeWhat fills the slot: an img, a video, a demo. Falls back to a hairline pattern.
titlestringThe caption under the media.
metastringOne quiet mono line under the title.
hrefstring | UrlObjectMakes the title a link. Passed to next/link.
aspect"video" | "square" | "4/3""video"Shape of the media slot.

Source

image-card/image-card.tsx
import * as React from "react";
import Link from "next/link";
import { Card, CardContent } from "@/components/ui/card";
import { cn } from "@/lib/utils";

type Aspect = "video" | "square" | "4/3";

type ImageCardProps = Omit<React.ComponentProps<typeof Card>, "children" | "title"> & {
  /** What fills the media slot: an img, a video, a demo. Falls back to a hairline pattern. */
  media?: React.ReactNode;
  /** The caption under the media. */
  title: string;
  /** One quiet mono line under the title: a date, a size, a credit. */
  meta?: string;
  /** Makes the title a link. Passed to next/link. */
  href?: React.ComponentProps<typeof Link>["href"];
  /** Shape of the media slot. */
  aspect?: Aspect;
};

const ASPECT = { video: "aspect-video", square: "aspect-square", "4/3": "aspect-4/3" } as const;

/**
 * Media on top, a caption and a mono meta line below.
 * The media slot keeps its aspect ratio and clips whatever is inside to it.
 */
export function ImageCard({ media, title, meta, href, aspect = "video", className, ...props }: ImageCardProps) {
  return (
    <Card data-slot="image-card" size="sm" className={cn("gap-3 pt-0", className)} {...props}>
      <div
        data-slot="image-card-media"
        className={cn(
          "w-full overflow-hidden bg-muted [&_img]:size-full [&_img]:object-cover [&_video]:size-full [&_video]:object-cover",
          ASPECT[aspect],
        )}
      >
        {media ?? (
          <div
            aria-hidden="true"
            className="size-full bg-[repeating-linear-gradient(135deg,var(--border)_0,var(--border)_1px,transparent_1px,transparent_10px)]"
          />
        )}
      </div>
      <CardContent className="flex flex-col gap-0.5">
        <p className="m-0 truncate text-sm font-medium text-foreground">
          {href ? (
            <Link
              href={href}
              className="rounded-sm transition-colors duration-100 ease-out outline-none hover:text-foreground/85 focus-visible:ring-2 focus-visible:ring-ring/50"
            >
              {title}
            </Link>
          ) : (
            title
          )}
        </p>
        {meta && <p className="m-0 truncate font-mono text-xs text-muted-foreground">{meta}</p>}
      </CardContent>
    </Card>
  );
}

For AI agents

Hand your agent this page, the markdown at /components/image-card/prompt.md, or the Copy AI prompt button above.

When to use

  • Galleries of screenshots, templates or inspiration where every item has a picture
  • A grid of assets with a size, date or credit under each
  • Any list where the picture is the first thing to scan

When not to use

  • When there is no picture; use a link card or a plain list
  • When the whole card should be one click; put the link on the card, not the title
  • For a hero visual, which wants a frame, not a caption

Agent notes

  • Server component on shadcn's Card; an img or video passed as media is sized to cover the slot
  • The placeholder is a hairline pattern drawn with var(--border), so it re-themes; it is aria-hidden
  • Only the title becomes a link when href is set; the media stays inert so it can hold a demo
  • aspect is a fixed set: video, square or 4/3. Add a class to the slot through the card if you need another