Web InnoventixFreeCode

Offset Border Effect

Original · free

An offset outline shifts behind the image on hover.

byWeb InnoventixReact + Tailwind
offsetborderimageeffect
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-offset-border.json
imgfx-offset-border.tsx
"use client";

import { useReducedMotion } from "motion/react";

type Shot = {
  src: string;
  alt: string;
  caption: string;
};

const SHOTS: Shot[] = [
  {
    src: "/img/gallery/g07.webp",
    alt: "Sunlit modern staircase curving against a concrete wall",
    caption: "Architecture",
  },
  {
    src: "/img/gallery/g19.webp",
    alt: "Portrait lit by warm window light against a plain backdrop",
    caption: "Portrait",
  },
  {
    src: "/img/gallery/g28.webp",
    alt: "Quiet coastline at dusk with long reflections on wet sand",
    caption: "Landscape",
  },
];

export default function ImgfxOffsetBorder() {
  const reduce = useReducedMotion();

  return (
    <section className="relative w-full overflow-hidden bg-neutral-50 px-6 py-20 text-neutral-900 sm:px-10 sm:py-28 dark:bg-neutral-950 dark:text-neutral-100">
      <style>{`
        .iob-card {
          --iob-shift: 14px;
          transition: transform 480ms cubic-bezier(0.22, 1, 0.36, 1);
        }
        .iob-frame {
          transition: transform 480ms cubic-bezier(0.22, 1, 0.36, 1),
            border-color 480ms ease;
        }
        .iob-photo {
          transition: transform 480ms cubic-bezier(0.22, 1, 0.36, 1),
            box-shadow 480ms ease;
        }
        .iob-card:hover .iob-frame,
        .iob-card:focus-within .iob-frame {
          transform: translate(var(--iob-shift), var(--iob-shift));
        }
        .iob-card:hover .iob-photo,
        .iob-card:focus-within .iob-photo {
          transform: translate(calc(var(--iob-shift) * -0.35), calc(var(--iob-shift) * -0.35));
        }
        @media (prefers-reduced-motion: reduce) {
          .iob-card,
          .iob-frame,
          .iob-photo {
            transition: none !important;
          }
          .iob-card:hover .iob-frame,
          .iob-card:focus-within .iob-frame,
          .iob-card:hover .iob-photo,
          .iob-card:focus-within .iob-photo {
            transform: none !important;
          }
        }
      `}</style>

      <div className="relative mx-auto max-w-6xl">
        <div className="mb-14 max-w-2xl">
          <p className="mb-3 text-xs font-semibold uppercase tracking-[0.28em] text-indigo-600 dark:text-indigo-400">
            Image Effect / Offset Border
          </p>
          <h2 className="text-3xl font-semibold tracking-tight sm:text-4xl">
            A frame that steps out behind the photo.
          </h2>
          <p className="mt-4 text-base leading-relaxed text-neutral-600 dark:text-neutral-400">
            Hover any image — an outlined frame slides out from underneath while
            the photo lifts the opposite way, opening a crisp depth gap.
          </p>
        </div>

        <div className="grid grid-cols-1 gap-x-10 gap-y-16 sm:grid-cols-2 lg:grid-cols-3">
          {SHOTS.map((shot) => (
            <figure
              key={shot.src}
              tabIndex={0}
              className="iob-card group relative isolate rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-4 focus-visible:ring-offset-neutral-50 dark:focus-visible:ring-offset-neutral-950"
            >
              <span
                aria-hidden="true"
                className="iob-frame pointer-events-none absolute inset-0 -z-10 rounded-xl border-2 border-indigo-500 dark:border-indigo-400"
              />
              <div className="iob-photo overflow-hidden rounded-xl bg-neutral-200 shadow-sm ring-1 ring-black/5 dark:bg-neutral-800 dark:ring-white/10">
                {/* eslint-disable-next-line @next/next/no-img-element */}
                <img
                  src={shot.src}
                  alt={shot.alt}
                  loading="lazy"
                  draggable={false}
                  className="aspect-[4/5] h-full w-full object-cover"
                />
              </div>
              <figcaption className="mt-5 flex items-center justify-between text-sm">
                <span className="font-medium text-neutral-800 dark:text-neutral-200">
                  {shot.caption}
                </span>
                <span className="text-xs uppercase tracking-widest text-neutral-400 dark:text-neutral-500">
                  Hover
                </span>
              </figcaption>
            </figure>
          ))}
        </div>

        <p className="mt-14 text-sm text-neutral-500 dark:text-neutral-500">
          {reduce
            ? "Reduced motion is on — the offset frame stays put."
            : "Tip: keyboard-focus a card to trigger the same offset."}
        </p>
      </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 →