Skip to content
ssntl/ui

Media card

Text over an image on a dark scrim, so it reads on any picture.

A quiet place to work

Studio retreat · 3 days in the valley

Installation

$ pnpm dlx shadcn@latest add @ssntl/media-card

Usage

tsx
import { MediaCard, MediaCardContent, MediaCardImage } from "@/components/ui/media-card"

<MediaCard>
  <MediaCardImage src="/retreat.webp" alt="" />
  <MediaCardContent>
    <p className="text-heading">A quiet place to work</p>
    <p className="text-caption text-muted-foreground">Studio retreat · 3 days</p>
  </MediaCardContent>
</MediaCard>
  • Text over images never depends on the picture: the content sits on a black scrim that fades upwards.
  • The content switches to the dark theme, so text and muted text stay light on any image.
  • Decorative images take alt=""; describe the picture only when it carries information.
  • Keep the text at the bottom, where the scrim is darkest; never over a face or the subject.

Code

components/ui/media-card.tsx
import * as React from "react"

import { cn } from "@/lib/utils"

// Text over an image only reads when it doesn't depend on the image. The
// content sits on a scrim, a gradient from black that fades out upwards, and
// switches to the dark theme so its text and muted text stay light on any
// picture, bright or busy. A 1px pure black (white in dark) outline at 10%
// gives the image a consistent edge on any surface.
function MediaCard({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="media-card"
      className={cn(
        "relative isolate grid min-h-80 content-end overflow-hidden rounded-lg bg-muted outline -outline-offset-1 outline-black/10 dark:outline-white/10",
        className
      )}
      {...props}
    />
  )
}

function MediaCardImage({
  className,
  alt = "",
  ...props
}: React.ComponentProps<"img">) {
  return (
    // eslint-disable-next-line @next/next/no-img-element
    <img
      data-slot="media-card-image"
      alt={alt}
      className={cn("absolute inset-0 -z-10 size-full object-cover", className)}
      {...props}
    />
  )
}

function MediaCardContent({
  className,
  ...props
}: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="media-card-content"
      data-theme="dark"
      className={cn(
        "grid gap-1 bg-linear-to-t from-black/80 via-black/50 to-transparent p-6 pt-24 text-foreground",
        className
      )}
      {...props}
    />
  )
}

export { MediaCard, MediaCardContent, MediaCardImage }