EU AI Act, Article 50

The notice, in one line.

Tells your visitors they are interacting with an AI system, and labels AI-generated content — in all 24 official EU languages, automatically. No dependencies, 7 kB, and nothing leaves the page.

01 Live demo

This is the real widget, not a picture of one.

Change the language and watch every string follow. The label on generated content switches on hover.

synthetic_image.png

Marked with data-ai-generated plus data-ai-overlay.

Hi! I’m a demo assistant. Ask me anything.

This paragraph was drafted by a generative model, so it carries an inline label.

The floating notice at the bottom is the Article 50(1) disclosure. Replies you trigger are labelled automatically — the widget watches for new content, so streamed chat output is covered without extra code.

02 Install

Two minutes, and no build step.

First, decide where to load it from

recommended

Host it yourself

Download disclose.min.js, drop it next to your other assets, point the tag at it. One file, no dependencies, no network calls — there is nothing a CDN adds. And your notice can never break because someone else’s domain lapsed.

convenient

Load from a CDN

Fine if you would rather not manage the file. Pin the version so an update can never change behaviour under you.

  1. Add the script before </body>

    Self-hosted:

    <script src="/js/disclose.min.js"
            data-mode="both" data-lang="auto" data-theme="auto"></script>

    Or pinned from a CDN:

    <script src="https://cdn.jsdelivr.net/npm/@kalbydot/kalby.disclose@1.0.0/dist/disclose.min.js">
    </script>

    That single line already gives you the chatbot notice. Everything below is optional.

  2. Mark anything a model produced

    One attribute. Add data-ai-overlay on images and video to place the label on the media instead of after it.

    <img src="hero.png" data-ai-generated>
    <p data-ai-generated>Drafted by our assistant.</p>
    <video src="clip.mp4" data-ai-generated="deepfake" data-ai-overlay></video>

    Use ="deepfake" for artificially generated or manipulated depictions of real people, places or events — it carries the fuller Article 50(4) sentence.

  3. If your chat lives in a widget, anchor the notice to it

    By default the notice floats at the bottom of the page. To put it inside your chat panel instead, give it the container’s selector.

    <script src="/js/disclose.min.js" data-chat-target="#support-chat"></script>
  4. Check it actually worked

    Open your site in a private window — a dismissal is remembered, so a normal window can hide the notice you are looking for.

    You should see the notice within a second of load, and a label on everything you marked. In the browser console:

    KalbyDisclose.getAuditLog()   // timestamped record of what was shown
    KalbyDisclose.languages       // the 24 codes it can render
  5. WordPress

    No plugin needed. Install a footer-snippet plugin such as WPCode, then Code Snippets → Header & Footer and paste the script into the Footer box. Most themes and builders have their own custom-scripts field, which works just as well.

    On a child theme, passing the site locale keeps the notice in your site’s language rather than the visitor’s:

    add_action( 'wp_footer', function () { ?>
      <script src="/js/disclose.min.js"
              data-lang="<?php echo esc_attr( substr( get_locale(), 0, 2 ) ); ?>"></script>
    <?php } );

    To mark content in the block editor, select the block, open its options menu and choose Edit as HTML, then add the attribute to the tag. The block stays valid — Gutenberg does not flag it.

    This whole route is tested end-to-end on WordPress 6.4 with a block theme (a child theme of Twenty Twenty-Three): the snippet above, the site locale flowing into the notice, and hand-marked blocks surviving the editor.

If nothing appears. Clear your caching plugin — it is serving the old page. Then check the tag really made it into the source, and that you are not looking at a notice you dismissed earlier: KalbyDisclose.resetDismissal() in the console brings it back.

03 Options

All optional, all on the script tag.

AttributeDefaultWhat it does
data-modebothChatbot notice, content labels, or both
data-langautoReads <html lang>, then the browser
data-themeautoFollows the visitor’s system preference
data-positionbottomWhere the floating notice sits
data-chat-targetRender inline inside your chat UI
data-accent#5b5bd6Match your brand colour
data-learn-more-urlAdds a link to your AI policy
data-remembersessionHow long a dismissal sticks: session, persistent, always
data-label-stylehoverhover, plain, or brand — see below

04 The label

Plain by default. Point at it for the dog.

At rest the label reads the translated term with the sparkles the field has settled on — AI-generated, AI-gegenereerd, KI-generiert. Move a pointer over it and it crossfades to “AI sniffed” with Kalby’s dog. Try it on the labels in the demo above.

That order is deliberate. The chip is the legally operative label under Article 50(2) and 50(4), and “AI sniffed” is wordplay: on its own it does not tell a reader the content was produced by AI, and it says nothing at all to someone who does not read English. So the disclosing wording is what sits on the page, and the dog is a reward for looking closer — never a substitute.

Both faces share one grid cell, so the chip is as wide as the wider of the two and the text around it never reflows on hover.

Nothing moves on its own. The reveal is pure CSS :hover — no timer, no auto-updating content, so none of the WCAG rules about moving content apply. Touch devices, which have no hover, simply show the translated term. The accessible name and the tooltip carry the full translated disclosure in every case. Prefer it fixed? data-label-style="plain" drops the hover face, brand gives you the dog alone.