@qadar-ai/disclose
v0.8.0
Published
Disclose by Qadar AI — a drop-in JavaScript widget that adds EU AI Act Art. 50-aligned "AI-generated" transparency labels to any website. Transparency tool, not a compliance guarantee.
Maintainers
Readme
Disclose by Qadar AI
A small (~20 kB gzip, all 24 EU languages inline), dependency-free JavaScript widget that adds EU AI Act Art. 50-aligned "AI-generated" transparency labels to any website's images, video, articles and pages. You mark content with an HTML attribute; Disclose renders the official EU AI-label icon as an accessible badge that expands to a plain-language disclosure.
Disclose is a transparency tool, not a compliance guarantee. It renders the deployer visible-disclosure layer (Art. 50(4)). It does not perform the machine-readable provider marking of Art. 50(2), and using it does not by itself make you compliant. The badge uses the European Commission's official AI-labelling icons unmodified; displaying them does not by itself signal adherence to the Commission's Code of Practice. It is provided by Qadar AI.
Install
Via CDN (no build step):
<script
src="https://cdn.qadar.ai/disclose/disclose.umd.js"
data-locale="en"
data-theme="auto"
crossorigin="anonymous"
></script>Use the browser build disclose.umd.js for <script> embeds — CDNs serve the .cjs variant
as application/node, which browsers refuse to execute. Pin the exact build with
Subresource Integrity using
the hash from dist/sri.json (integrity="sha384-…"). The public CDN origin is finalized at deploy
time — coordinate the canonical URL before going live.
Via npm:
npm install @qadar-ai/discloseimport { init } from '@qadar-ai/disclose';
init({ locale: 'de' });Ships both an ESM build (import) and a UMD/CJS build (require), with TypeScript types
(dist/index.d.ts). The published tarball contains the dist build only — no source.
Via jsDelivr / unpkg (auto-mirrored from npm, no account):
<script
src="https://cdn.jsdelivr.net/npm/@qadar-ai/disclose"
crossorigin="anonymous"
></script>
<!-- or: https://unpkg.com/@qadar-ai/disclose -->The bare package URL resolves to the browser build (disclose.umd.js). Pin a version and add
integrity from the release's sri.json for production use, e.g.
https://cdn.jsdelivr.net/npm/@qadar-ai/[email protected]/dist/disclose.umd.js.
Mark your content
Add data-ai-disclose with one of four levels to any element:
| Value | Meaning |
| --- | --- |
| none | No AI used — nothing is rendered |
| ai-assisted | AI-assisted |
| ai-generated | AI-generated |
| ai-reviewed | AI-generated & human-reviewed (text only — see note) |
| ai-modified | AI modified — real content manipulated with AI (uses the official AI MODIFIED icon) |
<img src="art.png" data-ai-disclose="ai-generated" />
<article data-ai-disclose="ai-assisted">…</article>
<video data-ai-disclose="ai-generated"></video>
<body data-ai-disclose="ai-generated" data-ai-disclose-type="page">…</body>Content type is inferred from the tag (img/picture → image, video, audio, article or
[role=article] → article, body/html → page). Override it with data-ai-disclose-type.
Deepfake rule (binding): for AI-generated image, audio or video, the
ai-reviewedlevel never suppresses or softens the visible disclosure — human review does not remove a deepfake disclosure. The "human-reviewed" wording is only shown for text (article/page).
Badge size and position (per element)
Two optional attributes tune each badge. Both are additive — omit them and nothing changes.
| Attribute | Values | Default | Applies to |
| --- | --- | --- | --- |
| data-ai-disclose-size | sm, md, lg, xl, 2xl, 3xl | md | all badges |
| data-ai-disclose-position | top-left, top-right, bottom-left, bottom-right, center | top-left | media overlays |
Sizes are fixed pixel steps (~1.4× per step), not a percentage of the container, so the label stays legible. The official EU icons carry generous built-in whitespace, so these are total badge heights:
| Size | Badge height |
| --- | --- |
| sm | 27 px |
| md (default) | 38 px |
| lg | 53 px |
| xl | 74 px |
| 2xl | 104 px |
| 3xl | 146 px |
The detail popover re-anchors per position so it never clips the viewport, at every size.
<!-- EN: a large badge in the top-right corner of a hero image -->
<!-- DE: ein großes Badge in der oberen rechten Ecke eines Hero-Bilds -->
<img src="hero.jpg" data-ai-disclose="ai-generated" data-ai-disclose-size="lg"
data-ai-disclose-position="top-right" />For fine-tuning beyond the steps, set the --ai-disclose-scale CSS custom property on the widget
host (or any ancestor); it multiplies the chosen step:
.disclose-host { --ai-disclose-scale: 1.15; }Badge tone (per element)
The official EU icon ships in two tones — a dark (black) pill for light backgrounds and a
light (white) pill for dark ones. By default the tone follows the resolved theme; override it
per element with data-ai-disclose-tone (parity with Disclose Stamp's --badge-tone):
| Value | Effect |
| --- | --- |
| dark | Always the dark/black pill. |
| light | Always the light/white pill. |
| auto | Pick the pill that contrasts with the underlying media — best-effort luminance sampling. |
<img src="dark-hero.jpg" data-ai-disclose="ai-generated" data-ai-disclose-tone="auto" />auto is same-origin only. It samples the media's pixels on a <canvas>; a cross-origin
image (or one without permissive CORS headers) taints the canvas, so sampling is refused and the
badge falls back to the theme default. It applies to image/video overlays; for text/pages there
is nothing to sample. To make auto work on remote images, serve them with
Access-Control-Allow-Origin and load them crossorigin="anonymous".
(DE) Badge-Ton (pro Element). Das offizielle EU-Icon gibt es in zwei Tönen — eine dunkle
(schwarze) Pille für helle Hintergründe und eine helle (weiße) für dunkle. Standardmäßig folgt
der Ton dem Theme; mit data-ai-disclose-tone (dark / light / auto, Parität mit Stamps
--badge-tone) pro Element überschreibbar. auto funktioniert nur same-origin: Es tastet die
Pixel des Mediums per <canvas> ab; ein cross-origin-Bild „verunreinigt" den Canvas, das Abtasten
wird verweigert und der Ton fällt auf den Theme-Standard zurück. Für auto bei entfernten Bildern:
Access-Control-Allow-Origin senden und crossorigin="anonymous" laden.
How the panel opens (per element)
Three optional attributes tune the interaction. All are additive — omit them for the default click-to-open behavior.
| Attribute | Values | Effect |
| --- | --- | --- |
| data-ai-disclose-trigger | click (default), hover | hover also opens the panel on mouse-over. It's an addition, not a replacement — the badge stays a real button, so click, Enter/Space, and Esc always work (essential for keyboard and touch users). |
| data-ai-disclose-auto-open | present = on | Opens the panel once, when the badge first scrolls into view. |
| data-ai-disclose-panel | off | Badge-only mode: show the badge with no detail panel. The badge becomes a static labelled image (role="img" with the disclosure as its accessible name) — no button, nothing to open. |
<img src="art.png" data-ai-disclose="ai-generated" data-ai-disclose-trigger="hover" />
<article data-ai-disclose="ai-reviewed" data-ai-disclose-auto-open>…</article>
<img src="thumb.jpg" data-ai-disclose="ai-modified" data-ai-disclose-panel="off" />(DE) Wie sich das Panel öffnet (pro Element). data-ai-disclose-trigger="hover" öffnet das Panel
zusätzlich beim Überfahren — ergänzend, nicht ersetzend: Klick, Enter/Leertaste und Esc
funktionieren weiterhin (wichtig für Tastatur- und Touch-Bedienung). data-ai-disclose-auto-open
öffnet das Panel einmalig, sobald das Badge erstmals in den sichtbaren Bereich scrollt.
data-ai-disclose-panel="off" = Nur-Badge-Modus: Badge ohne Panel, als statisches beschriftetes
Bild (role="img") — kein Button, nichts zum Öffnen.
Provenance metadata (per element)
Add optional detail about how the content was made. It shows in the panel (below the level note) and enriches the JSON-LD marker:
| Attribute | Example | Panel | JSON-LD |
| --- | --- | --- | --- |
| data-ai-disclose-tool | Midjourney | Tool | creator (SoftwareApplication) |
| data-ai-disclose-date | 2026-07-01 | Created (localized) | dateCreated (only if a valid ISO date) |
| data-ai-disclose-note | Generated with v6 | Note | disambiguatingDescription |
<img src="art.png" data-ai-disclose="ai-generated"
data-ai-disclose-tool="Midjourney" data-ai-disclose-date="2026-07-01"
data-ai-disclose-note="Upscaled, minor retouch" />Safety: all three are free text rendered escaped — panel values are set via textContent
(never parsed as HTML), and the JSON-LD serializer escapes <, so a value can't break out of the
<script> element or inject markup. Labels are authoritative in EN + DE; other locales fall back to
English for these three words.
(DE) Provenienz-Metadaten (pro Element). Optionale Angaben zur Entstehung: data-ai-disclose-tool
(z. B. Midjourney), data-ai-disclose-date (ISO), data-ai-disclose-note (kurzer Freitext). Sie
erscheinen im Panel (unter dem Hinweis) und reichern den JSON-LD-Marker an (creator,
dateCreated nur bei gültigem ISO-Datum, disambiguatingDescription). Sicherheit: alle drei sind
Freitext und werden escaped gerendert (Panel via textContent, JSON-LD escaped <) — kein
HTML-/Script-Ausbruch möglich.
Mark without touching the HTML (selector config)
When you can't (or don't want to) add attributes to each element — a CMS you don't control, a
third-party theme, marking via a tag manager — map CSS selectors → disclosure instead. Set
window.DiscloseConfig.mark before the widget loads, and/or pass a JSON data-mark on the loader
script:
<script>
window.DiscloseConfig = {
mark: {
'.blog-hero img': 'ai-generated',
'article.news': { level: 'ai-reviewed', size: 'lg' },
},
};
</script>
<script src="https://cdn.jsdelivr.net/npm/@qadar-ai/disclose" defer></script>Each value is a level string, or an object { level, size?, position?, type? }. It works by
stamping the normal marking attributes onto matching elements, so everything else behaves
identically — and a real per-element attribute always wins (precedence: element attribute >
selector-config > script-tag default > built-in). It's SPA-safe: elements added later
(client-side routing, infinite scroll) are matched and marked by the same observer.
Google Tag Manager recipe
Create a Custom HTML tag (fire on the pages you want) that sets the config and injects the loader — the global is read when the script initializes:
<script>
window.DiscloseConfig = { mark: { 'main img.ai': 'ai-generated' } };
var s = document.createElement('script');
s.src = 'https://cdn.jsdelivr.net/npm/@qadar-ai/disclose';
s.defer = true;
document.head.appendChild(s);
</script>(DE) Ohne HTML-Änderung markieren (Selector-Config). Wenn Sie die Elemente nicht anfassen können
(fremdes CMS, Theme, Tag-Manager), ordnen Sie CSS-Selektoren → Kennzeichnung zu: window.DiscloseConfig.mark
vor dem Laden setzen und/oder ein JSON-data-mark am Loader-Script. Wert ist ein Level-String oder
{ level, size?, position?, type? }. Ein echtes Element-Attribut gewinnt immer (Vorrang: Element-Attribut >
Selector-Config > Script-Default > eingebaut). SPA-sicher — später eingefügte Elemente werden vom
Observer mitmarkiert. Für Google Tag Manager: ein Custom-HTML-Tag, das window.DiscloseConfig setzt
und den Loader einfügt (siehe Snippet oben).
Languages
Disclose ships full catalogs for all 24 official EU languages, inline — so it keeps its
zero runtime network calls with no locale to fetch. The locale is auto-detected; override it
with data-ai-disclose-lang (or data-locale) on the loader script, or init({ locale }).
Locale precedence: init({ locale }) → data-ai-disclose-lang → data-locale → <html lang>
→ navigator.language → en. Region variants normalize to the base language (pt-BR → pt,
de-AT → de); an unsupported language falls back to English.
Supported: bg cs da de el en es et fi fr ga hr hu it lt lv mt nl pl pt ro sk sl sv.
Brand tokens stay English in every language — AI (never a localized abbreviation), EU AI
Act, Disclose, Qadar AI — while the surrounding copy and the Article-50 legal terms
use each language's official wording.
Translation review status. English and German are authoritative. The other 22 are drafted with each language's official EU AI Act (Reg. (EU) 2024/1689) Article-50 terminology and are pending native review before production legal use. Confidence per language: High (anchor terms verified against the official OJ/EUR-Lex text):
da de en fr it lt lv nl sv. Medium (official anchor terms from knowledge, prose idiomatic — native check recommended):bg cs el es et fi ga hr hu pl pt ro sk sl. Low (native review required):mt.
Machine-readable page marker (JSON-LD)
Alongside the visible badge, Disclose injects one consolidated
<script type="application/ld+json"> into the page <head> (light DOM — so search engines and
AI crawlers can read it, unlike the Shadow-DOM badge). It describes each marked element with its
schema.org type (ImageObject / VideoObject / AudioObject / Article /
CreativeWork), a resolvable contentUrl, and the IPTC digitalSourceType — the same
machine-readable vocabulary Disclose Stamp embeds in-file
(trainedAlgorithmicMedia for AI-generated/-reviewed; compositedWithTrainedAlgorithmicMedia
for AI-assisted/-modified). It is built inline from the DOM — zero network calls, no visible
change, and it updates when new marked content appears.
What this is — and isn't. This is a standards-based, page-level machine-readable marker, complementary to provider-side EU AI Act Article 50(2) marking. It does not by itself discharge the provider's Article 50(2) duty (that is the generator's job, at source). Disclose adds an on-page, crawlable signal on the deployer's side.
Publishers who manage their own structured data can turn it off with
data-ai-disclose-jsonld="off" on the loader script (default on).
(DE) Maschinenlesbarer Seiten-Marker (JSON-LD). Neben dem sichtbaren Badge fügt Disclose einen
gebündelten <script type="application/ld+json"> in den <head> ein (Light DOM, also für
Suchmaschinen und AI-Crawler lesbar). Er beschreibt jedes markierte Element mit seinem
schema.org-Typ, einer auflösbaren contentUrl und dem IPTC-digitalSourceType — demselben
maschinenlesbaren Vokabular, das Disclose Stamp in die Datei einbettet. Alles inline, ohne
Netzwerkaufrufe, ohne sichtbare Änderung. Das ist ein standardbasierter, seitenweiter
maschinenlesbarer Marker, ergänzend zur anbieterseitigen Kennzeichnung nach Art. 50 Abs. 2 EU AI
Act — er erfüllt die Anbieterpflicht aus Art. 50 Abs. 2 nicht selbst. Abschalten mit
data-ai-disclose-jsonld="off" am Loader-Script (standardmäßig an).
Global defaults (per page)
Set a page-wide fallback for badge size, overlay position, or content type on the loader script, so you don't repeat the same attribute on every element:
<script src="https://cdn.jsdelivr.net/npm/@qadar-ai/disclose" defer
data-default-size="lg" data-default-position="bottom-right" data-default-type="image">
</script>Precedence (the rule): element attribute > selector-config > script-tag default > built-in
default. A per-element data-ai-disclose-size (etc.) always wins; the script-tag data-default-*
fills in only where an element is silent; the built-in default (md, top-left, inferred type)
applies when neither is set. For content type, a confident tag/role (<img>, <video>,
<article> …) is element-level signal and still outranks data-default-type — the default only
resolves ambiguous elements like a bare <div>.
(DE) Globale Defaults. data-default-size, data-default-position, data-default-type am
Loader-Script setzen seitenweite Fallbacks. Vorrang-Regel: Element-Attribut > Selector-Config >
Script-Tag-Default > eingebauter Default. Ein eindeutiges Tag (<img> …) schlägt
data-default-type; der Typ-Default greift nur bei mehrdeutigen Elementen (z. B. <div>).
What the panel explains (and the "Learn more" link)
The detail panel answers the visitor's core question in-panel and in their language — an "About these labels" block (localized in all 24 languages) lists the three official label families in one line each and states that the labels are applied by the website's operator. No click and no off-site hop are needed to understand a label.
Below that, an optional "Learn about AI transparency" link points to Qadar's visitor explainer
page qadar.ai/ai-label. It is locale-gated: shown only where that page exists (launch:
en, de) — for any other locale there is no default link (the in-panel block already covers
it; never a silent language fallback). The link is rel="nofollow noopener".
Publishers can point "Learn more" at their own AI-transparency policy with data-learn-more-url
— on the loader script (page-wide) or per element. A publisher override always shows, in any
locale; only the link target changes, never the (localized) label text. Unsafe URLs
(javascript:, data: …) are rejected.
<img src="art.png" data-ai-disclose="ai-generated" data-learn-more-url="/legal/ai-transparency" />
<script src="…/disclose" defer data-learn-more-url="https://acme.example/ai-policy"></script>(DE) Was das Panel erklärt (und der „Mehr erfahren"-Link). Das Panel beantwortet die Kernfrage
im Panel und in der Sprache der besuchenden Person: ein „Über diese Kennzeichnungen"-Block
(in allen 24 Sprachen) nennt die drei offiziellen Label-Familien je einlinig und stellt klar, dass
die Kennzeichnungen vom Betreiber der Website vergeben werden — ohne Klick, ohne externen Sprung.
Darunter steht optional der „Mehr über AI-Transparenz"-Link auf Qadars Besucher-Erklärseite
qadar.ai/ai-label, locale-gated (Start: en, de) — für andere Sprachen kein
Standard-Link (keine stille Sprach-Rückfall-Lösung). Mit data-learn-more-url zeigt er auf eine
eigene Seite (am Script oder pro Element; Override wird immer angezeigt, in jeder Sprache;
nur das Ziel ist konfigurierbar, der Text bleibt fest). rel="nofollow noopener".
Theming the panel
Match the detail panel to your brand with a small, controlled set of CSS custom properties.
They inherit through the Shadow DOM, so set them anywhere on the page (e.g. :root):
:root {
--ai-disclose-accent: #e11d48; /* level pill + links + focus ring */
--ai-disclose-bg: #fff7ed; /* panel background */
--ai-disclose-text: #7c2d12; /* panel text */
--ai-disclose-radius: 2px; /* panel corner radius */
--ai-disclose-font: Georgia, serif; /* panel font */
}Each is optional and falls back to the built-in look (which still respects light/dark and
prefers-color-scheme). What you can theme is deliberately limited to the panel chrome. The
official EU AI-label icon and the disclosure wording are never themeable — they're the trust
anchor and must look the same everywhere. Contrast is your responsibility: if you override
colors, keep the panel text readable (aim for WCAG AA) in both light and dark.
(DE) Panel gestalten. Passen Sie das Detail-Panel mit einem kleinen, kontrollierten Satz
CSS-Custom-Properties an Ihre Marke an (--ai-disclose-accent, --ai-disclose-bg,
--ai-disclose-text, --ai-disclose-radius, --ai-disclose-font) — sie wirken durch das Shadow
DOM, z. B. auf :root gesetzt. Jede ist optional und fällt auf das eingebaute Aussehen zurück (inkl.
Light/Dark). Nur das Panel-Chrome ist gestaltbar — das offizielle EU-AI-Label-Icon und der
Kennzeichnungstext bleiben unveränderbar (Vertrauensanker). Kontrast liegt in Ihrer
Verantwortung (Ziel WCAG AA, in Light und Dark).
Configure
Options resolve as JS override > script data-* > <html lang> > navigator.language > default.
| Option | data-* | Values | Default |
| --- | --- | --- | --- |
| Locale | data-ai-disclose-lang / data-locale | any of the 24 EU languages | auto-detected → en |
| Theme | data-theme | auto, light, dark | auto |
| Page-bar position | data-position | top, bottom | bottom |
| Default badge size | data-default-size | sm…3xl | md |
| Default overlay position | data-default-position | top-left…center | top-left |
| Default content type | data-default-type | image/video/audio/article/page | inferred |
| Policy link | data-learn-more-url (also per element) | any safe URL | Qadar Disclose page |
| Selector marks | data-mark (JSON) / window.DiscloseConfig.mark | selector → level or { level, size?, … } | none |
| JSON-LD marker | data-ai-disclose-jsonld | off = don't emit | on |
| Manual init | data-manual | present = don't auto-init | auto-init |
API
import { init, refresh, version } from '@qadar-ai/disclose';
init(); // decorate existing content + observe for dynamically-added content
init({ locale: 'de', theme: 'dark' });
refresh(); // re-scan on demand (e.g. after a manual DOM update); returns the count decoratedDynamically-added and lazily-loaded content is picked up automatically via a MutationObserver.
Bundle & privacy
Disclose is ~20 kB gzip and never phones home — no network calls, no tracking, no third-party requests at runtime. Everything (all 24 language catalogs and the official EU icons) is embedded, so a marked page renders with zero runtime network calls, and the whole widget self-hosts as a single file if you don't want to load it from a CDN at all.
That "nothing phones home" property is deliberate and is why the language catalogs are inline rather than lazy-loaded: fetching a per-language chunk at runtime would make every non-English visitor's browser call the widget's origin, leaking their IP + host site + language to it — the opposite of a transparency tool's promise. Keeping locales inline costs bundle size but keeps the privacy guarantee absolute.
Approximate gzip budget: language catalogs ~10 kB · official EU icons ~4–5 kB · core logic ~5–6 kB.
At ~20 kB total the widget is still tiny (smaller than most single images), so we optimize for the
privacy promise and one-line embed over raw kB. If a deployment ever needs a sub-10 kB single-locale
build, per-locale artifacts (disclose.<lang>.js, still zero-network, still one SRI) are the planned
option — lazy runtime loading is intentionally not offered.
Accessibility
Every badge is a real focusable button with an accessible label; the signal is carried by the
official EU icon and the button's text alternative (not color alone). The icon ships in both light
and dark treatments and follows the resolved theme (and prefers-color-scheme under auto); a
semi-transparent variant is used when the badge overlays media. The detail panel is a labelled
dialog, keyboard-operable, closable with Esc, and restores focus to the badge. Targets WCAG 2.2
AA. Styling is isolated in a Shadow DOM, so host CSS neither bleeds in nor is affected. The icons are
embedded as data URIs, so the widget makes zero network calls at runtime.
Develop
npm install
npm test # vitest (jsdom)
npm run build # ESM + UMD + .d.ts + SRI hashes
npm run lint # eslint + prettierSee examples/index.html for a live demo (run npm run build first, then serve the repo root).
Releasing (maintainers)
Publishing to npm is gated and manual — the Publish to npm workflow only runs via
workflow_dispatch and defaults to a dry run. It uses npm OIDC Trusted Publishing
(no long-lived NPM_TOKEN), so the following one-time setup is required on the npm side:
- npm account + org. Sign in to npmjs.com; ensure the
qadar-aiorg exists (the package is@qadar-ai/disclose). - First publish creates the package. Trusted-publisher settings live under a
package's settings, so the package must exist first. Do one manual publish from a
trusted machine with 2FA:
npm publish(version0.1.0or anrc). - Add the Trusted Publisher. In the package settings → Trusted Publisher, add:
- Organization/user:
qadar-ai· Repository:disclose - Workflow filename:
publish.yml· Environment name:npm-publish - Allowed action:
npm publish
- Organization/user:
- Every later publish is CI-only. Push a tag, then dispatch the
Publish to npmworkflow withdry_run=false. No token needed; npm authenticates via OIDC. - Provenance attaches automatically once the repository is public; it is skipped while the repo is private.
Requirements (handled by the workflow): Node ≥ 22.14, npm ≥ 11.5.1, id-token: write.
License
MIT © Qadar AI
