AI Label
Disclosing that content was generated or assisted by a model, and giving the reader a way to find out how.
Also called AI Badge, AI Slug, Generated by AI — in this system all of them are AI Label.
Everything below is the real component. Change the controls, tab through it, and turn on Inspector Mode to read any value off the screen.
The health check failed in eu-west-2 at 14:32 after the connection pool saturated. The retry budget was exhausted before the circuit opened, so requests queued rather than failing fast. Rolling back to build 4019 restored the service in about eight seconds.
The explainer is the point
The mark says something happened; the popover says what. Provenance, review status, and a way to report that it got something wrong.
Review status changes the claim
"AI-generated" and "AI-drafted, reviewed by Ada" say different things about who is accountable. One badge for both misrepresents each of them.
The health check failed in eu-west-2 at 14:32 after the connection pool saturated. The retry budget was exhausted before the circuit opened, so requests queued rather than failing fast. Rolling back to build 4019 restored the service in about eight seconds.
The health check failed in eu-west-2 at 14:32 after the connection pool saturated. The retry budget was exhausted before the circuit opened, so requests queued rather than failing fast. Rolling back to build 4019 restored the service in about eight seconds.
Where it goes
Beside the heading of the region it applies to, not floating in the corner of the page. Its scope should be obvious from its position alone.
One per region, not one per sentence
A label on every generated element becomes wallpaper and stops being read. Disclose at the region level and say so once.
Interaction states
Every state a user can put this component into, rendered side by side. If a state is missing here, it is missing in production too.
Every part, every measurement, and the reason it is that number.
The health check failed in eu-west-2 at 14:32 after the connection pool saturated. The retry budget was exhausted before the circuit opened, so requests queued rather than failing fast. Rolling back to build 4019 restored the service in about eight seconds.
A pill carrying a sparkle and a word, pressable, opening an explainer with provenance, review status and a feedback control.
- Height22px (18px small)
Badge scale. It sits beside a heading without competing with it — a disclosure should be visible and quiet at the same time.
- Glyph11px sparkle
The sparkle has become the shared convention across the industry. Inventing a different symbol costs recognition for no gain.
- Word"AI" or "AI-generated"
The glyph alone is not a disclosure. Space permitting, spell it out — "AI-generated" is unambiguous where a sparkle is not.
- ToneAccent, not status
Accent-tinted. Using a status colour would claim the content is good, bad or risky, which the label is not saying.
- PressableOpens the explainer
What turns a mark into a disclosure. A label that cannot be interrogated tells the reader a fact they cannot act on.
- ExplainerProvenance + review + feedback
What the model saw, whether a person checked it, and a way to report that it got something wrong.
- PlacementBeside the region heading
Its scope should be readable from its position. A mark floating in a page corner labels nothing in particular.
Values are read live from the running stylesheet, so this table can never drift from the code. Click any value to copy it.
Color
| Token | Value | Used for |
|---|---|---|
| --ds-accent-subtle | — | Label fill |
| --ds-accent-border | — | Label border |
| --ds-accent-text | — | Label text and sparkle |
| --ds-success-subtle | — | A human-reviewed variant |
| --ds-warning-subtle | — | An unreviewed draft, where the risk is worth marking |
| --ds-surface-overlay | — | The explainer panel |
| --ds-fg-secondary | — | Explainer body text |
Spacing
| Token | Value | Used for |
|---|---|---|
| padding-x | Label padding |
Radius
| Token | Value | Used for |
|---|---|---|
| full | — | Pill shape, shared with Badge |
Typography
| Token | Value | Used for |
|---|---|---|
| 11px / 500 | — | Label text |
Pick a size from this table. Do not invent a new one — a fourth height is how a design system starts dying.
| Size | Height | Padding | Icon | Type | Min width | Max width | Touch target | When to use |
|---|---|---|---|---|---|---|---|---|
| Small | 18px | 0 6px | 9px | 10px | — | — | — | Inline in a table cell or beside a small heading. |
| Medium | 22px | 0 8px | 11px | 11px | — | — | 44px when pressable | The default. Beside a section or card heading. |
| Explainer | — | — | — | — | 15rem | 18rem | — | Provenance, review status and feedback. Short enough to read in one pass. |
| Region banner | — | — | — | — | 100% | — | — | When a whole view is generated, one Banner at the top beats a label per element. |
Not a checklist to run at the end. These are the requirements the component was built from.
Contrast
- The label text owes 4.5:1 at 11px. It is a disclosure, and small quiet text is exactly where that gets skipped.
- The word carries the meaning, never the sparkle or the tint alone — colour-only disclosure fails 1.4.1 outright.
- The border owes 3:1: it is what separates the label from the heading beside it.
- The reviewed and unreviewed variants must differ by wording as well as colour.
Keyboard
| Tab | Reaches the label when it opens an explainer. A static mark is not focusable. |
| Enter / Space | Opens the explainer and moves focus into it. |
| Esc | Closes it and returns focus to the label. |
| Tab | Reaches the feedback controls inside the explainer. |
Screen readers
- The disclosure must be announced with the content it applies to, not left as a visual mark beside it. aria-describedby on the region is what does that.
- Announce the scope: "AI-generated summary" tells the user which part; "AI" tells them nothing.
- Never rely on the sparkle. It is decorative and aria-hidden; the word is the entire disclosure for a non-visual reader.
Focus & touch
- A static label is not focusable; a pressable one is a real button that returns focus on close. In a list of generated items, the label must not become a tab stop per row — disclose once for the region instead.
- A pressable label needs a 44px target, which means padding around a 22px pill rather than a larger pill. Hover-only disclosure does not exist on touch, so the mark itself must always be visible — and on a phone the explainer is usually better as a bottom Drawer than as a floating panel.
| Attribute | Applied to | Notes |
|---|---|---|
| aria-label | The pressable label | "AI generated — what does this mean?". A bare "AI" announces two letters with no context. |
| aria-hidden | The sparkle | Decoration. The word is the disclosure. |
| aria-haspopup="dialog" | The label | With aria-expanded, so the explainer is discoverable rather than a surprise. |
| aria-describedby | The labelled region | Pointing at the disclosure, so the provenance is announced with the content rather than only beside it. |
| role="note" | A region-level disclosure | For a banner covering a whole generated view. |
Example usage
1import { AiLabel } from '@/ui/Display'23// The label opens the explainer — that is what makes it a disclosure rather4// than a decoration.5<Row>6 <h3>Incident summary</h3>7 <AiLabel8 provenance="Written from the deployment logs and the linked incident."9 reviewed={summary.reviewedBy}10 onFeedback={(useful) => track('ai_feedback', { id: summary.id, useful })}11 />12</Row>1314// The disclosure must be announced WITH the content, not merely beside it.15<section aria-describedby="ai-disclosure">16 <p id="ai-disclosure" className="sr-only">17 This summary was generated by a language model and has not been reviewed.18 </p>19 <p>{summary.text}</p>20</section>2122// Review status is a different claim about who is accountable.23{summary.reviewedBy ? (24 <Badge tone="success">Reviewed by {summary.reviewedBy}</Badge>25) : (26 <AiLabel />27)}2829// One disclosure per region. Twelve labels on twelve suggested tags is30// twelve marks nobody reads.31<Row>32 <h4>Suggested tags</h4>33 <AiLabel size="sm" />34</Row>35{tags.map((t) => <Badge key={t}>{t}</Badge>)}Framework-free HTML
<div class="ds-section">
<h3 id="summary-heading">Incident summary</h3>
<!-- The sparkle is decoration; the word is the disclosure. -->
<button
type="button"
class="ds-ai-label"
aria-label="AI generated — what does this mean?"
aria-haspopup="dialog"
aria-expanded="false"
>
<svg aria-hidden="true">…</svg>
AI
</button>
<!-- Announced WITH the content, not merely beside it. -->
<p id="ai-disclosure" class="sr-only">
This summary was generated by a language model and has not been
reviewed by a person.
</p>
<p aria-describedby="ai-disclosure">
The health check failed in eu-west-2 at 14:32…
</p>
</div>
<div role="dialog" aria-label="AI-generated summary">
<p>Written by a language model from the deployment logs.</p>
<button type="button">Useful</button>
<button type="button">Not useful</button>
</div>CSS
.ds-ai-label {
display: inline-flex;
align-items: center;
gap: 4px;
block-size: 22px; /* badge scale: visible and quiet */
padding-inline: 8px;
border: 1px solid var(--ds-accent-border);
border-radius: 999px;
/* Accent, never a status colour: green would claim the content is good,
amber that it is risky. The label says neither. */
background: var(--ds-accent-subtle);
color: var(--ds-accent-text);
font-size: 11px;
font-weight: 500;
}
.ds-ai-label:focus-visible {
outline: 2px solid var(--ds-focus-ring);
outline-offset: 2px;
}
/* Review status is a different claim, so it gets a different mark. */
.ds-ai-label--reviewed {
border-color: var(--ds-success-border);
background: var(--ds-success-subtle);
color: var(--ds-success-text);
}
/* The tint is gone here, so the word and border must carry it alone. */
@media (forced-colors: active) {
.ds-ai-label { border: 1px solid; }
}
/* Padding, not a bigger pill. */
@media (pointer: coarse) {
.ds-ai-label { padding-block: 11px; margin-block: -11px; }
}Component API
AiLabel
| Prop | Type | Default | Description |
|---|---|---|---|
| provenance* | string | — | What the model was given and what it produced. This is the disclosure; the pill is the entry point. |
| reviewed | string | false | false | The reviewer’s name. A reviewed item is a different claim and gets a different mark. |
| text | string | 'AI' | "AI-generated" wherever there is room. A glyph alone is not a disclosure. |
| size | 'sm' | 'md' | 'md' | Badge scale. Small for inline use in a table cell. |
| onFeedback | (useful: boolean) => void | — | Collected in the explainer, where the reader already is when they notice a problem. |
| describes | string | — | The id of the region it applies to, so the disclosure is announced with the content. |
Professional tips
- Write the provenance in plain language. "Written from the deployment logs and the linked incident" is a real answer; "powered by advanced AI" is marketing.
- Keep one mark, one wording, one placement across the whole product. Consistency is what turns a badge into a signal.
- Link to the sources the model used where you can. Provenance the reader can check is worth far more than provenance they have to take on trust.
- Record feedback against the specific generation, not the feature. Aggregate thumbs-down tells you nothing about which output was wrong.
- Revisit the label when a human edits generated content. Once a person has changed it, they are the author, and the mark should reflect that.
Performance
- Render the label statically and mount the explainer only when it opens. A list of fifty generated rows should not carry fifty hidden popovers.
- One shared explainer instance re-pointed at the active label is enough — the content differs only by the provenance string.
- Do not animate the sparkle. A permanently shimmering disclosure reads as decoration, which is exactly what it must not be.
Common mistakes
- Using the mark to advertise a feature, which destroys its meaning as a disclosure.
- A sparkle with no word, relying on a convention the reader may not know.
- A label per element instead of one per region.
- A status colour, claiming the content is good or risky.
- Hover-only disclosure, invisible on touch.
- No explainer, so the reader learns something happened but not what.
- Not distinguishing reviewed from unreviewed content.
- The disclosure visible but never announced, leaving screen-reader users unaware.
Real-world recommendations
- Disclosure requirements are tightening in several jurisdictions. Building the mark and the provenance record now is far cheaper than retrofitting them across a product later.
- Users calibrate quickly once a product is consistent: they learn the mark, learn what it predicts about quality, and adjust their trust accordingly. That is the mechanism working.
- Over-labelling is the more common failure. A product that marks every ranked list and every autocomplete has trained its users to ignore the mark by the time it matters.
- The feedback control in the explainer is the highest-signal quality data most teams have. It arrives at the moment the reader noticed the problem, attached to the exact output.