AI content label
ProA label for content that is wholly or partly AI-generated: "AI-generated", "AI-modified", or "AI involvement not recorded" when the app has no record. Its two levels follow the categories of the EU's voluntary Code of Practice on marking and labelling AI-generated content (final, 10 June 2026): fully AI-generated, and partially AI-modified. It uses Candor's own mark; the EU's optional icons aren't bundled. The level comes from the app's records, never guessed. Use provenanceText, withProvenance, provenanceMetadata and useCopyWithProvenance to keep the label on copied and downloaded content. Whether a label meets any rule stays the deployer's call.
Preview
Install
First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/ai-content-label
Brings in: locale, use-copy, utils · npm: lucide-react@^1.67.0
Usage
"use client"
import { Copy } from "lucide-react"
import { AiContentLabel, provenanceMetadata, useCopyWithProvenance, type AiProvenance } from "@/components/ai-content-label"
import { Button } from "@/components/ui/button"
const summary: AiProvenance = { involvement: "generated", by: "Drafting agent", at: "2026-11-05T10:00:00Z" }
export default function Example() {
const { state, copy } = useCopyWithProvenance(summary)
return (
<div className="flex max-w-xl flex-col gap-5">
<div className="flex flex-col gap-2 rounded-lg border p-3 text-sm">
<div className="flex items-center justify-between gap-2">
<AiContentLabel {...summary} />
<Button type="button" variant="ghost" size="xs" onClick={() => copy("Quarterly support volume fell 12%, mostly in billing questions.")}>
<Copy aria-hidden />
{state === "copied" ? "Copied" : state === "failed" ? "Copy failed" : "Copy with label"}
</Button>
</div>
<p>Quarterly support volume fell 12%, mostly in billing questions.</p>
</div>
<div className="flex flex-wrap gap-2">
<AiContentLabel involvement="modified" by="Writing assistant" />
<AiContentLabel involvement="none" />
<AiContentLabel involvement="unknown" />
</div>
<figure className="flex flex-col gap-2">
<div aria-hidden className="h-24 rounded-md bg-muted" />
<figcaption>
<AiContentLabel size="caption" involvement="generated" by="Image tool" at="2026-11-04T16:30:00Z" note="Illustration only; not a photo of a real place." />
</figcaption>
</figure>
<pre className="overflow-x-auto rounded-md bg-muted p-2 text-2xs">{JSON.stringify(provenanceMetadata(summary), null, 2)}</pre>
</div>
)
}<AiContentLabel>
| Prop | Type | Default | Description |
|---|---|---|---|
| involvementrequired | AiInvolvement | — | How much AI went into it, from the app's records. |
| by | string | undefined | — | The tool or agent that was used, named by the app ("Drafting agent"). Shown for "generated" and "modified" only. |
| at | Date | string | undefined | — | When it was made or changed, as a Date or ISO string. Shown for "generated" and "modified" only. |
| note | string | undefined | — | The app's own plain-text note, e.g. "Figures checked by finance". Shown in the caption; not part of copied text. |
| size | "inline" | "caption" | undefined | "inline" | "inline" (the default): a small chip beside a message or file name. "caption": a line under an image or file, with the note. |
| className | string | undefined | — | Extra classes for the outer element. |
Functions
provenanceText(label: AiProvenance, options?: ProvenanceOptions)The label as one plain line: "AI-generated with Drafting agent, 5 Nov 2026", or "AI involvement not recorded".
withProvenance(text: string, label: AiProvenance, options?: ProvenanceOptions)Text for the clipboard with its provenance line after it, so the label travels with the words.
provenanceMetadata(label: AiProvenance)The label as a small JSON-able object for downloads and sidecar files, under keys that don't assume XMP or IPTC: "ai:involvement" always; "ai:by" (the app's name for the tool) and "ai:at" (ISO 8601) when known. Map them to your format's own fields if it has them. Words aren't included: the values are the records, not the UI's English.
useCopyWithProvenance(label: AiProvenance, resetAfter?: number)useCopy, with the label's provenance line added under whatever is copied. copy(text) resolves to "copied" only after the clipboard write does; provenance is the line it adds, for showing beside the copy button.
Strings
The fixed words, in English by default. Change any of them under aiContentLabel in <LocaleProvider strings={{ … }}>; the table is exported as aiContentLabelStrings.
| Key | English |
|---|---|
| generated | AI-generated |
| modified | AI-modified |
| none | No AI used |
| unknown | AI involvement not recorded |
| withBy | {label} with {by} |
| withAt | {text}, {at} |
| clipboard | — {provenance} |
Types
- AiInvolvement
- How much AI went into a piece of content, from the app's records, never guessed: - "generated": made by an AI system, with no substantive human authorship ("AI-generated"). - "modified": a person's work that an AI system changed or helped write ("AI-modified"). - "none": the records say no AI system was used. - "unknown": the app has no record either way ("AI involvement not recorded"). Use it whenever you can't say.
"generated" | "modified" | "none" | "unknown"- AiProvenance
- What the app knows about one piece of content's AI involvement.
{ /** How much AI went into it, from the app's records. */ involvement: AiInvolvement /** The tool or agent that was used, named by the app ("Drafting agent"). Shown for "generated" and "modified" only. */ by?: string | undefined /** When it was made or changed, as a Date or ISO string. Shown for "generated" and "modified" only. */ at?: Date | string | undefined /** The app's own plain-text note,- ProvenanceOptions
- Options for the provenance helpers outside React, where LocaleProvider can't reach.
{ /** Overrides for `aiContentLabelStrings`, e.g. the same table you pass LocaleProvider under `aiContentLabel`. */ strings?: Partial<StringTable> | undefined /** The locale for the date. Default "en-GB" ("5 Nov 2026"). */ locale?: string | undefined /** The time zone for the date. Default "UTC", so server and browser agree. */ timeZone?: string | undefined }