# Image Overlay Card

> Full-bleed gradient photo card whose caption slides up on hover or focus, with tag chips and a corner action.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/image-overlay-card
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/image-overlay-card.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

```bash
npm install lucide-react
```

## Usage

```tsx title="components/image-overlay-card-demo.tsx"
import { ImageOverlayCard } from "@/components/image-overlay-card"

export default function ImageOverlayCardDemo() {
  return (
    <div className="flex flex-wrap items-start justify-center gap-6">
      <ImageOverlayCard
        title="Northern Lights"
        description="Four nights chasing the aurora across the Norwegian fjords."
        tags={["Travel", "Norway", "Night"]}
      />
      <ImageOverlayCard
        title="Golden Hour Dunes"
        description="A slow walk through the Sahara as the light turns to amber."
        tags={["Desert", "Photography"]}
        imageClassName="from-yellow-300 via-orange-500 to-rose-700"
        actionLabel="Bookmark"
      />
    </div>
  )
}
```

## Source

```tsx title="components/image-overlay-card.tsx"
"use client"

import * as React from "react"
import { Bookmark } from "lucide-react"

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

export interface ImageOverlayCardProps extends Omit<React.ComponentProps<"div">, "title"> {
  title: string
  description?: string
  tags?: string[]
  /** Tailwind gradient utilities standing in for the photo */
  imageClassName?: string
  actionLabel?: string
  onActionChange?: (active: boolean) => void
}

export function ImageOverlayCard({
  title,
  description,
  tags = [],
  imageClassName = "from-cyan-400 via-blue-600 to-indigo-900",
  actionLabel = "Save",
  onActionChange,
  className,
  ...props
}: ImageOverlayCardProps) {
  const [active, setActive] = React.useState(false)

  return (
    <div
      className={cn(
        "group relative aspect-[4/5] w-full max-w-xs overflow-hidden rounded-2xl border bg-card text-white shadow-sm",
        className
      )}
      {...props}
    >
      <div
        className={cn(
          "absolute inset-0 bg-gradient-to-br transition-transform duration-500 group-focus-within:scale-105 group-hover:scale-105 motion-reduce:transition-none",
          imageClassName
        )}
        aria-hidden
      >
        <div className="absolute left-1/2 top-1/4 size-40 -translate-x-1/2 rounded-full bg-white/25 blur-3xl" />
        <div className="absolute -bottom-8 -left-8 size-48 rounded-full bg-black/25 blur-3xl" />
      </div>
      <div
        className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent"
        aria-hidden
      />

      <button
        type="button"
        aria-pressed={active}
        aria-label={actionLabel}
        onClick={() => {
          const next = !active
          setActive(next)
          onActionChange?.(next)
        }}
        className="absolute right-3 top-3 flex size-9 items-center justify-center rounded-full bg-black/30 text-white outline-none backdrop-blur transition-colors hover:bg-black/50 focus-visible:ring-4 focus-visible:ring-white/50"
      >
        <Bookmark className={cn("size-4", active && "fill-current")} aria-hidden />
      </button>

      <div className="absolute inset-x-0 bottom-0 p-5">
        <h3 className="text-xl font-semibold leading-tight tracking-tight">{title}</h3>
        <div
          className={cn(
            "grid grid-rows-[0fr] opacity-0 transition-[grid-template-rows,opacity] duration-300 ease-out motion-reduce:transition-opacity",
            "group-focus-within:grid-rows-[1fr] group-focus-within:opacity-100 group-hover:grid-rows-[1fr] group-hover:opacity-100",
            "[@media(hover:none)]:grid-rows-[1fr] [@media(hover:none)]:opacity-100"
          )}
        >
          <div className="overflow-hidden">
            {description && <p className="mt-2 text-sm text-white/85">{description}</p>}
            {tags.length > 0 && (
              <ul className="mt-3 flex flex-wrap gap-1.5">
                {tags.map((tag) => (
                  <li
                    key={tag}
                    className="rounded-full bg-white/20 px-2.5 py-1 text-xs font-medium backdrop-blur"
                  >
                    {tag}
                  </li>
                ))}
              </ul>
            )}
          </div>
        </div>
      </div>
    </div>
  )
}
```
