Components / Gallery
Photo Contact Sheet
Free Gallery React component — PhotoContactSheetCard. MIT licensed, copy-paste ready for Next.js and Tailwind CSS.
Film contact sheet with a 2×2 numbered frame grid and roll label header. Photographer portfolio vibes, straight from the darkroom.
Preview
Contact sheet — Roll 12A
01
03
04
02Setup
How to use
Free for personal and commercial use. No UI attribution required. Include the MIT copyright notice when copying component source into your project. Read the MIT license.
- 1
Run in your terminal:
$npm install clsx tailwind-merge lucide-react - 2
Copy
lib/cn.tsbelow. Skip if you already havecn(). - 3
Copy the code below and create
components/gallery/photo-contact-sheet-card.tsxin your project. - 4
Import and render:
Exampleimport { PhotoContactSheetCard } from "@/components/gallery/photo-contact-sheet-card";
lib/cn.ts
import { type ClassValue, clsx } from "clsx";import { twMerge } from "tailwind-merge";export function cn(...inputs: ClassValue[]) {return twMerge(clsx(inputs));}
components/gallery/photo-contact-sheet-card.tsx
import Image from "next/image";import { forwardRef, type ComponentPropsWithoutRef } from "react";import { cn } from "@/lib/cn";export type ContactSheetFrame = {src: string;number: string;};export type PhotoContactSheetCardProps = Readonly<{rollLabel?: string;frames?: ContactSheetFrame[];} & ComponentPropsWithoutRef<"div">>;const defaultFrames: ContactSheetFrame[] = [{ src: "/wallpaper-15.png", number: "01" },{ src: "/wallpaper-3.png", number: "03" },{ src: "/wallpaper-11.png", number: "04" },{ src: "/wallpaper-2.png", number: "02" },];// Production-ready Photo Contact Sheet component — styled with Tailwind CSS.export const PhotoContactSheetCard = forwardRef<HTMLDivElement,PhotoContactSheetCardProps>(({className,rollLabel = "Contact sheet — Roll 12A",frames = defaultFrames,...props},ref,) => (<divref={ref}data-slot="photo-contact-sheet-card"className={cn("w-xs bg-neutral-100 p-3 font-mono", className)}{...props}><p className="mb-2 text-[9px] tracking-wider text-neutral-500 uppercase">{rollLabel}</p><div className="grid grid-cols-2 gap-2">{frames.map((frame) => (<divkey={frame.number}className="group relative aspect-4/5 overflow-hidden border border-neutral-300 bg-white"><Imagesrc={frame.src}alt={`Frame ${frame.number}`}fillsizes="160px"className="object-cover transition-transform duration-500 group-hover:scale-105"/><span className="absolute top-1 left-1 bg-white/90 px-1 py-px text-[8px] font-bold text-neutral-800">{frame.number}</span></div>))}</div></div>),);PhotoContactSheetCard.displayName = "PhotoContactSheetCard";