Q3 product summary
Revenue grew 18% quarter over quarter, led by the new team plan. Churn held steady at 2.1%.
An AI-generated label in chip, image-overlay and inline forms that opens a provenance card: model, time, and whether a person edited it.
Revenue grew 18% quarter over quarter, led by the new team plan. Churn held steady at 2.1%.
"use client";
import * as React from "react";
import { AnimatePresence, motion } from "motion/react";
import { Sparkles } from "lucide-react";
import { cn } from "@/lib/utils";
export interface AiDisclosureDetails {
/** The model or feature that produced the content. */
model?: string;
/** When it was generated, already formatted ("Today, 14:02"). */
generatedAt?: string;
/** Who reviewed or edited it after generation. Changes the label to "AI-assisted". */
editedBy?: string;
/** A short caveat, such as "May contain mistakes. Check important facts." */
note?: string;
}
export interface AiDisclosureBadgeProps {
/** Defaults to "AI-generated", or "AI-assisted" once a person has edited it. */
label?: string;
details?: AiDisclosureDetails;
/**
* "chip" — a bordered pill next to a title or in a card header.
* "overlay" — a translucent pill for the corner of an image or video.
* "inline" — icon and label only, for a byline or under a paragraph.
*/
variant?: "chip" | "overlay" | "inline";
className?: string;
}
export function AiDisclosureBadge({ label, details, variant = "chip", className }: AiDisclosureBadgeProps) {
const [open, setOpen] = React.useState(false);
const [pinned, setPinned] = React.useState(false);
const rootRef = React.useRef<HTMLSpanElement>(null);
const closeTimer = React.useRef<number | undefined>(undefined);
const id = React.useId();
const text = label ?? (details?.editedBy ? "AI-assisted" : "AI-generated");
const hasDetails = Boolean(details && (details.model || details.generatedAt || details.editedBy || details.note));
React.useEffect(() => {
if (!pinned) return;
function handle(e: MouseEvent) {
if (rootRef.current && !rootRef.current.contains(e.target as Node)) {
setPinned(false);
setOpen(false);
}
}
document.addEventListener("mousedown", handle);
return () => document.removeEventListener("mousedown", handle);
}, [pinned]);
React.useEffect(() => () => window.clearTimeout(closeTimer.current), []);
function show() {
window.clearTimeout(closeTimer.current);
setOpen(true);
}
function hide() {
if (pinned) return;
// A short delay lets the pointer travel from the badge into the card.
closeTimer.current = window.setTimeout(() => setOpen(false), 120);
}
const badgeClass = cn(
"inline-flex items-center gap-1 text-xs font-medium whitespace-nowrap",
variant === "chip" && "rounded-full border bg-background px-2 py-0.5 text-muted-foreground",
variant === "overlay" && "rounded-full bg-black/55 px-2 py-0.5 text-white backdrop-blur-sm",
variant === "inline" && "text-muted-foreground",
hasDetails && "cursor-pointer transition-colors",
hasDetails && variant === "chip" && "hover:text-foreground",
hasDetails && variant === "inline" && "underline decoration-dotted underline-offset-2 hover:text-foreground"
);
const content = (
<>
<Sparkles className="size-3" aria-hidden />
{text}
</>
);
if (!hasDetails) {
return <span className={cn(badgeClass, className)}>{content}</span>;
}
return (
<span
ref={rootRef}
className={cn("relative inline-flex", className)}
onMouseEnter={show}
onMouseLeave={hide}
>
<button
type="button"
aria-expanded={open}
aria-controls={`${id}-details`}
onClick={() => {
// Hover previews the card; a click pins it open until the next click.
setPinned(!pinned);
setOpen(!pinned);
}}
onFocus={show}
onBlur={(e) => {
if (!rootRef.current?.contains(e.relatedTarget as Node)) {
setPinned(false);
setOpen(false);
}
}}
onKeyDown={(e) => {
if (e.key === "Escape") {
setPinned(false);
setOpen(false);
}
}}
className={cn(badgeClass, "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring")}
>
{content}
</button>
<AnimatePresence>
{open && (
<motion.span
id={`${id}-details`}
role="note"
initial={{ opacity: 0, y: -3 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -3 }}
transition={{ duration: 0.12, ease: "easeOut" }}
className="absolute top-full left-0 z-20 mt-1.5 block w-60 rounded-xl border bg-popover p-3 text-left text-xs font-normal whitespace-normal text-popover-foreground shadow-lg"
>
<span className="block font-medium text-foreground">
{details?.editedBy ? "Created with AI, reviewed by a person" : "Created with AI"}
</span>
<dl className="mt-2 grid grid-cols-[auto_1fr] gap-x-3 gap-y-1">
{details?.model && <Row term="Model" value={details.model} />}
{details?.generatedAt && <Row term="Generated" value={details.generatedAt} />}
{details?.editedBy && <Row term="Edited by" value={details.editedBy} />}
</dl>
{details?.note && <span className="mt-2 block border-t pt-2 text-pretty text-muted-foreground">{details.note}</span>}
</motion.span>
)}
</AnimatePresence>
</span>
);
}
function Row({ term, value }: { term: string; value: string }) {
return (
<>
<dt className="text-muted-foreground">{term}</dt>
<dd className="text-foreground">{value}</dd>
</>
);
}
A UX spec for this pattern — written for agents implementing or reusing it, not the code.
# AI Disclosure Badge
## Summary
A small "AI-generated" label attached to content an AI produced, which opens a card with its provenance: which model, when, and whether a person reviewed or edited it. It makes the origin of content clear without getting in the way of reading it.
## When to use
- Content an AI wrote, drew or summarised that sits next to human-made content — docs, feeds, product listings, support replies.
- Generated media that may be shared outside the product (images, video, audio), where the label should travel with it.
- Anywhere regulation or policy requires AI content to be labelled.
## When not to use
- Inside a chat with an assistant, where every reply is obviously AI. Labelling each message is noise.
- As the only safeguard for generated media that leaves the product — pair it with a machine-readable watermark or content credentials.
- As a quality or confidence signal. Use Confidence Indicator for "how sure", this pattern only says "who made it".
## Anatomy
- Badge: a sparkle mark and a short label, in one of three variants — chip (next to a title), overlay (corner of an image), inline (byline under text).
- Provenance card: a one-line headline ("Created with AI" / "Created with AI, reviewed by a person"), then model, generated time and editor rows.
- Optional caveat line at the bottom of the card.
## Behavior
- Hover or focus previews the card; a click pins it open until the next click, a click outside or Escape.
- When a person edits the content, the label changes from "AI-generated" to "AI-assisted" and the card names the editor. Don't drop the badge after an edit.
- With no details, the badge renders as plain text and is not interactive.
- The badge stays visible when content is copied into another surface in the product (quotes, embeds, exports).
## Content guidelines
- Use plain words: "AI-generated", "AI-assisted". Avoid "Magic" or brand-only names that don't say AI.
- Keep the caveat factual and short: "May contain mistakes. Check important facts."
- Show times in the user's locale and relative where it helps ("Today, 14:02").
## Accessibility
- The label is real text, not only an icon, so it is read by screen readers and survives high-contrast modes.
- With details, the badge is a button with `aria-expanded` and `aria-controls` pointing at the card; the card is reachable on keyboard focus.
- The overlay variant keeps WCAG AA contrast on any image by sitting on a dark translucent backing.
## Related patterns
- **Confidence Indicator** — Use it as well when AI-made content also needs a signal of how reliable it is.
- **Inline Citation** — Use it as well when people need to see what a claim is based on, not just who made it.
- **Sources Stack** — Use it as well to show every source an AI answer drew on.
- **Source Trust Card** — Use it instead when the question is how trustworthy a source is rather than whether the content is AI-made.
Confidence Indicator
Use it as well when AI-made content also needs a signal of how reliable it is.
Inline Citation
Use it as well when people need to see what a claim is based on, not just who made it.
Sources Stack
Use it as well to show every source an AI answer drew on.
Source Trust Card
Use it instead when the question is how trustworthy a source is rather than whether the content is AI-made.