abdxdev/cn

Image Tagger

Mark points on an image with draggable labeled badges, so one photo can point at each of the options it shows.

Click the photo to drop the active badge, drag one to move it, × to take one back off.

A circle, a square and a triangle

Click the photo to place Circle

0/3 placed
import { useState } from "react";
import { ImageTagger, type ImageTag } from "@/components/image-tagger";

const INITIAL: ImageTag[] = [
  { id: "circle", label: "Circle", x: null, y: null },
  { id: "square", label: "Square", x: null, y: null },
  { id: "triangle", label: "Triangle", x: null, y: null },
];

export function ImageTaggerDemo() {
  const [tags, setTags] = useState<ImageTag[]>(INITIAL);

  return (
    <ImageTagger
      src="/shape-variants.jpg"
      alt="A circle, a square and a triangle"
      tags={tags}
      onTagsChange={setTags}
      className="mx-auto max-w-md"
    />
  );
}

Installation

npx shadcn@latest add https://abdxdev-cn.vercel.app/r/image-tagger-ts.json

Usage

Tags are yours — the component holds no state beyond which badge is active. A tag with x: null is unplaced, and clicking the photo drops the active badge there before moving on to the next unplaced one:

const [tags, setTags] = useState<ImageTag[]>([
  { id: "circle", label: "Circle", x: null, y: null },
  { id: "square", label: "Square", x: null, y: null },
]);

<ImageTagger src={photo} tags={tags} onTagsChange={setTags} />;

Positions are stored as 0–1 fractions of the image, so they survive any rendered size — store them next to the photo URL and re-render the same badges in an overlay anywhere else:

<img src={photo} alt="" className="relative" />
{tags
  .filter((t) => t.x !== null)
  .map((t) => (
    <span
      key={t.id}
      className="absolute -translate-x-1/2 -translate-y-1/2 rounded-full bg-black/70 px-2 py-1 text-xs text-white"
      style={{ left: `${t.x! * 100}%`, top: `${t.y! * 100}%` }}
    >
      {t.label}
    </span>
  ))}

API Reference

Prop

Type

Prop

Type

On this page