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.
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.jsonUsage
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