Web InnoventixFreeCode

Polaroid Effect

Original · free

A polaroid frame with a caption that tilts on hover.

byWeb InnoventixReact + Tailwind
polaroidimageeffect
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/imgfx-polaroid.json
imgfx-polaroid.tsx
"use client";

import { useRef, type CSSProperties } from "react";
import { motion, useInView, useReducedMotion } from "motion/react";

const shots = [
  {
    src: "/img/gallery/g07.webp",
    alt: "Golden light spilling across a quiet mountain ridge at dusk",
    caption: "Ridge, last light",
    date: "07 · 14",
    rot: "-5deg",
  },
  {
    src: "/img/gallery/g18.webp",
    alt: "Rain-slicked city street reflecting neon signage after dark",
    caption: "Downtown, after rain",
    date: "05 · 02",
    rot: "3deg",
  },
  {
    src: "/img/gallery/g25.webp",
    alt: "Wildflowers catching morning sun in an open meadow",
    caption: "Meadow, 6am",
    date: "06 · 21",
    rot: "-2deg",
  },
];

export default function ImgfxPolaroid() {
  const prefersReduced = useReducedMotion();
  const ref = useRef<HTMLDivElement>(null);
  const inView = useInView(ref, { once: true, margin: "-80px" });

  return (
    <section className="relative w-full overflow-hidden bg-neutral-100 px-6 py-24 dark:bg-neutral-950 sm:px-10">
      <style>{`
        .polx-card {
          transition:
            transform 420ms cubic-bezier(0.22, 1, 0.36, 1),
            box-shadow 420ms cubic-bezier(0.22, 1, 0.36, 1);
          will-change: transform;
        }
        .polx-card:hover,
        .polx-card:focus-visible {
          transform: rotate(0deg) translateY(-14px) scale(1.03) !important;
          box-shadow: 0 34px 60px -22px rgba(0, 0, 0, 0.55);
          z-index: 20;
          outline: none;
        }
        .polx-cap {
          transition: transform 420ms cubic-bezier(0.22, 1, 0.36, 1);
          transform-origin: left center;
        }
        .polx-card:hover .polx-cap,
        .polx-card:focus-visible .polx-cap {
          transform: rotate(-3.5deg) translateX(2px);
        }
        .polx-sheen {
          opacity: 0;
          transition: opacity 420ms ease;
        }
        .polx-card:hover .polx-sheen,
        .polx-card:focus-visible .polx-sheen {
          opacity: 1;
        }
        @keyframes polx-rise {
          from { opacity: 0; transform: translateY(28px) rotate(var(--polx-r)); }
          to   { opacity: 1; transform: translateY(0) rotate(var(--polx-r)); }
        }
        @media (prefers-reduced-motion: reduce) {
          .polx-card,
          .polx-cap,
          .polx-sheen { transition: none !important; }
          .polx-card:hover,
          .polx-card:focus-visible { transform: rotate(0deg) !important; }
        }
      `}</style>

      <div className="mx-auto max-w-5xl text-center">
        <p className="text-xs font-semibold uppercase tracking-[0.35em] text-rose-500 dark:text-rose-400">
          Image Effect
        </p>
        <h2 className="mt-4 text-3xl font-semibold tracking-tight text-neutral-900 dark:text-neutral-50 sm:text-4xl">
          Polaroid Tilt
        </h2>
        <p className="mt-3 text-sm text-neutral-500 dark:text-neutral-400">
          Hover a print — it straightens, lifts, and the handwritten caption kicks back.
        </p>
      </div>

      <div
        ref={ref}
        className="mx-auto mt-16 flex max-w-4xl flex-wrap items-center justify-center gap-8 sm:gap-6"
      >
        {shots.map((shot, i) => {
          const style = prefersReduced
            ? { transform: "rotate(0deg)" as const }
            : ({
                "--polx-r": shot.rot,
                transform: `rotate(${shot.rot})`,
                animation: inView
                  ? `polx-rise 620ms ${i * 120}ms cubic-bezier(0.22,1,0.36,1) both`
                  : "none",
                opacity: inView ? undefined : 0,
              } as CSSProperties);

          return (
            <motion.div
              key={shot.src}
              tabIndex={0}
              aria-label={`Polaroid: ${shot.caption}`}
              style={style}
              className="polx-card group relative w-60 cursor-pointer rounded-sm bg-white p-3 pb-0 shadow-[0_18px_40px_-20px_rgba(0,0,0,0.45)] ring-1 ring-black/5 focus-visible:ring-2 focus-visible:ring-rose-400 dark:bg-neutral-100"
            >
              <div className="relative overflow-hidden bg-neutral-200">
                {/* eslint-disable-next-line @next/next/no-img-element */}
                <img
                  src={shot.src}
                  alt={shot.alt}
                  loading="lazy"
                  draggable={false}
                  className="aspect-square w-full select-none object-cover"
                />
                <div className="polx-sheen pointer-events-none absolute inset-0 bg-gradient-to-tr from-transparent via-white/25 to-transparent" />
                <div className="pointer-events-none absolute inset-0 shadow-[inset_0_0_30px_rgba(0,0,0,0.12)]" />
              </div>

              <div className="flex items-end justify-between px-1 py-5">
                <span className="polx-cap font-serif text-lg italic text-neutral-800">
                  {shot.caption}
                </span>
                <span className="pb-1 font-mono text-[10px] tracking-wider text-neutral-400">
                  {shot.date}
                </span>
              </div>
            </motion.div>
          );
        })}
      </div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →