Web InnoventixFreeCode

Corner Tag Effect

Original · free

A corner tag expands into a full caption bar on hover.

byWeb InnoventixReact + Tailwind
cornertagimageeffect
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-corner-tag.json
imgfx-corner-tag.tsx
"use client";

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

type CornerTagItem = {
  src: string;
  alt: string;
  index: string;
  label: string;
  caption: string;
};

const ITEMS: CornerTagItem[] = [
  {
    src: "/img/gallery/g07.webp",
    alt: "Soft morning light falling across a quiet interior scene",
    index: "01",
    label: "Interior",
    caption: "Morning light study — shot on a wide prime, no fill.",
  },
  {
    src: "/img/gallery/g18.webp",
    alt: "Textured outdoor landscape with layered depth and natural tones",
    index: "02",
    label: "Landscape",
    caption: "Layered depth at golden hour, straight out of camera.",
  },
  {
    src: "/img/gallery/g23.webp",
    alt: "Close detail composition with rich colour and contrast",
    index: "03",
    label: "Detail",
    caption: "A tight crop that lets colour and contrast carry the frame.",
  },
];

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

  return (
    <section className="relative w-full bg-white px-6 py-20 sm:px-8 sm:py-24 dark:bg-neutral-950">
      <style>{`
        .imgfx-corner-tag-bar {
          transition: max-width 520ms cubic-bezier(0.16, 1, 0.3, 1),
            background-color 320ms ease, box-shadow 320ms ease;
        }
        .imgfx-corner-tag-reveal {
          display: grid;
          grid-template-columns: 0fr;
          transition: grid-template-columns 520ms cubic-bezier(0.16, 1, 0.3, 1),
            opacity 300ms ease;
          opacity: 0;
        }
        .group:hover .imgfx-corner-tag-reveal,
        .group:focus-within .imgfx-corner-tag-reveal {
          grid-template-columns: 1fr;
          opacity: 1;
        }
        .imgfx-corner-tag-media {
          transition: transform 640ms cubic-bezier(0.16, 1, 0.3, 1);
        }
        .group:hover .imgfx-corner-tag-media,
        .group:focus-within .imgfx-corner-tag-media {
          transform: scale(1.04);
        }
        @media (prefers-reduced-motion: reduce) {
          .imgfx-corner-tag-bar,
          .imgfx-corner-tag-reveal,
          .imgfx-corner-tag-media {
            transition: none !important;
          }
        }
      `}</style>

      <div ref={sectionRef} className="mx-auto max-w-6xl">
        <motion.header
          initial={reduceMotion ? false : { opacity: 0, y: 16 }}
          animate={
            reduceMotion || inView ? { opacity: 1, y: 0 } : { opacity: 0, y: 16 }
          }
          transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
          className="mb-12 max-w-2xl"
        >
          <p className="text-xs font-semibold uppercase tracking-[0.2em] text-indigo-600 dark:text-indigo-400">
            Corner Tag
          </p>
          <h2 className="mt-3 text-2xl font-semibold tracking-tight text-neutral-900 sm:text-3xl dark:text-neutral-50">
            A tiny label that unfolds into the full story
          </h2>
          <p className="mt-3 text-sm text-neutral-600 dark:text-neutral-400">
            Each frame carries a small corner tag. Hover any image and the tag
            slides open into a full caption bar.
          </p>
        </motion.header>

        <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
          {ITEMS.map((item, i) => (
            <motion.figure
              key={item.src}
              initial={reduceMotion ? false : { opacity: 0, y: 24 }}
              animate={
                reduceMotion || inView
                  ? { opacity: 1, y: 0 }
                  : { opacity: 0, y: 24 }
              }
              transition={{
                duration: 0.55,
                ease: [0.16, 1, 0.3, 1],
                delay: reduceMotion ? 0 : 0.08 * i,
              }}
              className="group relative aspect-[4/5] overflow-hidden rounded-2xl bg-neutral-200 ring-1 ring-neutral-900/10 focus-within:ring-2 focus-within:ring-indigo-500 dark:bg-neutral-800 dark:ring-white/10"
              tabIndex={0}
              aria-label={`${item.label}: ${item.caption}`}
            >
              {/* eslint-disable-next-line @next/next/no-img-element */}
              <img
                src={item.src}
                alt={item.alt}
                loading="lazy"
                draggable={false}
                className="imgfx-corner-tag-media h-full w-full object-cover"
              />

              <div
                className="pointer-events-none absolute inset-x-0 bottom-0 h-2/3 bg-gradient-to-t from-neutral-950/70 via-neutral-950/10 to-transparent"
                aria-hidden="true"
              />

              <figcaption className="absolute bottom-3 left-3 right-3">
                <div className="imgfx-corner-tag-bar flex max-w-[6.5rem] items-center gap-2 overflow-hidden rounded-full bg-white/95 py-2 pl-2 pr-3 shadow-lg shadow-neutral-950/20 backdrop-blur group-hover:max-w-full group-focus-within:max-w-full dark:bg-neutral-900/95">
                  <span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-indigo-600 text-[0.65rem] font-bold text-white dark:bg-indigo-500">
                    {item.index}
                  </span>
                  <span className="shrink-0 whitespace-nowrap text-xs font-semibold text-neutral-900 dark:text-neutral-100">
                    {item.label}
                  </span>
                  <div className="imgfx-corner-tag-reveal min-w-0">
                    <span className="overflow-hidden">
                      <span className="block whitespace-nowrap border-l border-neutral-300 pl-3 text-xs text-neutral-600 dark:border-neutral-700 dark:text-neutral-300">
                        {item.caption}
                      </span>
                    </span>
                  </div>
                </div>
              </figcaption>
            </motion.figure>
          ))}
        </div>

        <p className="mt-8 text-center text-xs text-neutral-500 dark:text-neutral-500">
          Hover or focus a frame to expand its tag.
        </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 →