Pila de polaroids
Fotos de papel fotográfico apiladas en abanico; la elegida se endereza y sube al frente con sombra más profunda.
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
import { useState, type CSSProperties } from "react";
/** Fotos polaroid apiladas: al elegir una se endereza y sube, las demás se abren en abanico. Estilo skeuomórfico de papel fotográfico. React + TypeScript, sin dependencias. */
export type PolaroidItem = { id: string; caption: string; hue: number };
export type TarjetaPolaroidProps = {
items?: PolaroidItem[];
className?: string;
style?: CSSProperties;
};
const CSS = `
.gctpo-root { box-sizing: border-box; container-type: inline-size; display: block; width: 100%; max-width: 420px; font: 400 15px/1.2 "Caveat", "Segoe Script", "Bradley Hand", "Comic Sans MS", cursive; }
.gctpo-root *, .gctpo-root *::before, .gctpo-root *::after { box-sizing: border-box; }
.gctpo-root button { font: inherit; }
@media (prefers-reduced-motion: reduce) { .gctpo-root *, .gctpo-root *::before, .gctpo-root *::after { transition-duration: .01ms !important; } }
.gctpo-stack { position: relative; height: 330px; }
.gctpo-photo { position: absolute; left: 50%; top: 10px; width: 210px; padding: 12px 12px 8px; border: 0; background: #fbfaf5; text-align: center; cursor: pointer; color: #3a3631; box-shadow: 0 1px 0 #fff inset, 0 8px 18px -6px rgba(0, 0, 0, .45), 0 1px 3px rgba(0, 0, 0, .25); transform: translateX(-50%) translateX(calc(var(--s) * min(56px, 12cqi))) rotate(var(--r)) translateY(var(--y, 0)); transition: transform .55s cubic-bezier(.16, 1, .3, 1), box-shadow .3s; }
.gctpo-photo:focus-visible { outline: 3px solid #2563eb; outline-offset: 4px; }
.gctpo-photo[data-front="true"] { z-index: 5; --r: 0deg !important; --s: 0 !important; --y: -8px; box-shadow: 0 1px 0 #fff inset, 0 22px 30px -10px rgba(0, 0, 0, .55), 0 2px 4px rgba(0, 0, 0, .25); }
.gctpo-img { position: relative; display: block; aspect-ratio: 1; background: linear-gradient(160deg, hsl(var(--h) 70% 72%), hsl(calc(var(--h) + 40) 65% 42%)); box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .15), inset 0 6px 14px rgba(0, 0, 0, .25); overflow: hidden; }
.gctpo-img::before { content: ""; position: absolute; inset: 0; background: radial-gradient(circle at 30% 25%, rgba(255, 255, 255, .55), transparent 40%), radial-gradient(circle at 70% 85%, rgba(0, 0, 0, .3), transparent 55%); }
.gctpo-img::after { content: ""; position: absolute; left: -10%; right: -10%; bottom: -30%; height: 60%; border-radius: 50%; background: hsl(calc(var(--h) + 20) 50% 28% / .75); }
.gctpo-cap { display: block; padding: 10px 0 4px; font-size: 20px; }`;
const DEFAULT_ITEMS: PolaroidItem[] = [
{ id: "a", caption: "Primer prototipo", hue: 20 },
{ id: "b", caption: "Taller con el equipo", hue: 160 },
{ id: "c", caption: "Salida a producción", hue: 230 },
{ id: "d", caption: "Mes uno, todo en orden", hue: 320 },
];
export function TarjetaPolaroid({ items = DEFAULT_ITEMS, className = "", style }: TarjetaPolaroidProps) {
const [front, setFront] = useState(items[items.length - 1]?.id);
return (
<div className={`gctpo-root ${className}`} style={style}>
<style>{CSS}</style>
<div className="gctpo-stack">
{items.map((item, index) => {
const spread = index - (items.length - 1) / 2;
return (
<button
key={item.id}
className="gctpo-photo"
type="button"
data-front={front === item.id ? "true" : undefined}
aria-pressed={front === item.id}
onClick={() => setFront(item.id)}
style={{ "--s": spread, "--r": `${spread * 6 + (index % 2 ? 1.5 : -1.5)}deg`, "--h": item.hue, zIndex: front === item.id ? 5 : index } as CSSProperties}
>
<span className="gctpo-img" />
<span className="gctpo-cap">{item.caption}</span>
</button>
);
})}
</div>
</div>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/tarjeta-polaroidEjemplo de uso de Pila de polaroids
Después de copiar tarjeta-polaroid.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { TarjetaPolaroid } from "./tarjeta-polaroid";
export default function Ejemplo() {
return <TarjetaPolaroid />;
}Propiedades de TarjetaPolaroid
TarjetaPolaroid acepta 3 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
items? | PolaroidItem[] | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Personaliza Pila de polaroids con variables CSS
Define estas variables en el contenedor o en tu hoja de estilos para cambiar los colores sin tocar el componente. Si no las defines, se usa el valor por defecto.
| Variable | Valor por defecto |
|---|---|
--y | 0 |
Accesibilidad y rendimiento
El archivo tiene 62 líneas (4 KB) y no necesita @keyframes. Esto es lo que hace Pila de polaroids por quien lo usa:
- Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
- Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
- Expone su estado a tecnologías de apoyo con atributos aria-*.
- Es un componente de cliente de Next.js: incluye la directiva "use client".