Blog Implementation Guides 8 min read

shadcn/ui Card Media Placeholders for Dashboards

Add shadcn card image placeholders to dashboard cards and data grids using deterministic URLs that match your shadcn design tokens and prevent blank media regions.

shadcn card imageshadcn/uiDashboard placeholdersCard componentsReact image fallback
shadcn/ui Card Media Placeholders for Dashboards

shadcn/ui's Card component uses a slot-based composition pattern — there is no built-in `image` prop. Adding a media region means placing an `<img>` or a background-image div inside `CardHeader` or above the card body. Without an explicit fallback, missing images show broken icons or collapsed regions that break the card grid layout.

This guide covers the correct slot placement for card media, how to pair it with fallback.pics URLs that match shadcn's design token colors, and how to handle loading states and aspect-ratio constraints without causing CLS.

Card anatomy

Where to place media in a shadcn Card without breaking layout

shadcn's `Card` exports `CardHeader`, `CardContent`, `CardFooter`, and `CardTitle`. None of them are specifically designed for images. The typical approach is to place an `<img>` above `CardHeader` inside the `Card` wrapper, wrapped in a div that enforces the aspect ratio.

Set `aspect-ratio: 16/9` (or `3/2`, `1/1` for square product tiles) on the wrapping div, not on the `<img>` itself. The div constrains the space whether the image loads or not, preventing CLS when the image is replaced by the fallback.

Implementation tsx
// ProductCard.tsx
import {
  Card,
  CardContent,
  CardFooter,
  CardHeader,
  CardTitle,
} from '@/components/ui/card';

const FALLBACK = 'https://fallback.pics/api/v1/320x180/F1F5F9/94A3B8?text=Product';

export function ProductCard({ image, title, price }: {
  image?: string;
  title: string;
  price: string;
}) {
  return (
    <Card className="overflow-hidden">
      {/* Media slot — aspect ratio enforced here, not on <img> */}
      <div className="aspect-video w-full overflow-hidden bg-muted">
        <img
          src={image ?? FALLBACK}
          alt={title}
          width={320}
          height={180}
          className="h-full w-full object-cover"
          onError={(e) => {
            const target = e.currentTarget;
            if (target.src !== FALLBACK) target.src = FALLBACK;
          }}
        />
      </div>
      <CardHeader>
        <CardTitle>{title}</CardTitle>
      </CardHeader>
      <CardContent>
        <p className="text-muted-foreground">{price}</p>
      </CardContent>
    </Card>
  );
}

Token alignment

Matching placeholder colors to shadcn's CSS variables

shadcn uses CSS custom properties like `--muted`, `--muted-foreground`, and `--border` for its default color scale. The default light theme maps these to specific gray shades. Pick fallback URL hex values that approximate the equivalent Tailwind gray — `#F1F5F9` for `--muted` (slate-100) and `#94A3B8` for `--muted-foreground` (slate-400).

This makes the placeholder look like an intentional loading state rather than an error. Users perceive the card as still loading rather than broken, which is the correct mental model for a placeholder.

Implementation tsx
// constants/placeholders.ts (shadcn theme-aligned)
export const SHADCN_PH = {
  // Light theme — matches slate-100 / slate-400
  cardLandscape:  'https://fallback.pics/api/v1/320x180/F1F5F9/94A3B8',
  cardSquare:     'https://fallback.pics/api/v1/square/320?text=Image',
  // Dark theme — matches zinc-800 / zinc-500
  cardLandscapeDark: 'https://fallback.pics/api/v1/320x180/27272A/71717A',
  cardSquareDark:    'https://fallback.pics/api/v1/square/320?text=Image',
  // Branded (uses shadcn primary purple)
  cardBranded:    'https://fallback.pics/api/v1/320x180/7C3AED/FFFFFF?text=Loading',
};

Dashboard grids

Skeleton-style card placeholders for data-loading states

Dashboard cards often load asynchronously. During the fetch, render the card shell with a placeholder image so the grid layout is stable. Replace the placeholder src with the real image URL once the data resolves.

Avoid conditional rendering (`{data ? <Card/> : <Skeleton/>}`) if possible. The layout shift from mounting and unmounting cards is worse than a stable card with a placeholder image. Keep the card mounted and update the `src` prop instead.

Implementation tsx
// DashboardGrid.tsx
function MetricCard({ item }: { item?: DashboardItem }) {
  const ph = 'https://fallback.pics/api/v1/320x180/F1F5F9/94A3B8';

  return (
    <Card className="overflow-hidden">
      <div className="aspect-video w-full overflow-hidden bg-muted">
        <img
          src={item?.imageUrl ?? ph}
          alt={item?.title ?? 'Loading'}
          width={320}
          height={180}
          className="h-full w-full object-cover transition-opacity duration-300"
          style={{ opacity: item ? 1 : 0.5 }}
          onError={(e) => {
            if (e.currentTarget.src !== ph) e.currentTarget.src = ph;
          }}
        />
      </div>
      <CardHeader>
        <CardTitle className={item ? '' : 'animate-pulse bg-muted h-4 w-32 rounded'}>
          {item?.title}
        </CardTitle>
      </CardHeader>
    </Card>
  );
}

Dark mode

Switching placeholder colors with Tailwind's dark variant

shadcn supports dark mode via a class on the `html` element. You cannot conditionally change a URL string with a CSS class alone, but you can use two `<img>` elements — one for light, one for dark — and toggle their visibility with `hidden dark:block`. This is the simplest approach without JavaScript.

The JavaScript alternative is to read `document.documentElement.classList.contains('dark')` on mount and pick the appropriate placeholder URL. Add a `MutationObserver` on the `classList` if the theme can change at runtime.

Further reading

Resources for shadcn image and card patterns

For avatar-specific fallbacks inside shadcn cards, see the dedicated avatar fallback guide. For background-image-based card covers, the Tailwind background fallback guide covers CSS layering and probe patterns.

Implementation text
https://fallback.pics/docs/
https://fallback.pics/placeholder-image-api/
https://fallback.pics/blog/shadcn-avatar-fallback-src/
https://fallback.pics/blog/tailwind-background-image-fallbacks/

Key takeaways

What to standardize before shipping

  • Wrap card media in a fixed-aspect-ratio div (not on the `<img>`) so the layout is stable whether the image loads, fails, or shows the placeholder.
  • Match placeholder hex values to shadcn's CSS custom property equivalents (slate-100/slate-400) so the fallback looks like an intentional loading state.
  • Keep cards mounted during data loads and update the `src` prop rather than conditionally unmounting — this avoids a second layout shift.
  • Provide both light and dark placeholder URLs and toggle them with Tailwind's `dark:` variant or a `MutationObserver` for runtime theme switches.
  • Add an `onError` guard that checks `currentTarget.src !== fallback` before swapping to prevent infinite error loops.

Production fallback layer

Use fallback.pics anywhere an image URL is accepted.

Start with one deterministic URL and standardize fallback behavior across your design system.

Read the docs