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-cardUsage
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
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 }