AI chatbot disclosure in React (EU AI Act Article 50)

Last verified: 20 October 2026. Technical guidance, not legal advice.

Since 2 August 2026, any chatbot or AI agent that talks to people in the EU must tell them they're interacting with an AI system. The notice must be clear and given at the latest at the first interaction (Article 50(1) and 50(5) of the EU AI Act). It applies to non-EU companies too.

The law doesn't prescribe exact wording. This tutorial builds a disclosure pattern that is visible before the first message, repeated in the first reply, translated, and accessible.

What we'll build

  1. A persistent AI label in the chat header
  2. A first-message notice from the assistant
  3. Translations for common EU languages
  4. Accessibility: real text, aria support, and no reliance on an icon alone
  5. A server-side guard so the notice can't be skipped

1. Disclosure strings

// ai-disclosure.ts
export const AI_DISCLOSURE = {
  en: { label: "AI assistant", notice: "You're chatting with an AI system, not a human." },
  de: { label: "KI-Assistent", notice: "Sie chatten mit einem KI-System, nicht mit einem Menschen." },
  fr: { label: "Assistant IA", notice: "Vous discutez avec un système d'IA, pas avec un humain." },
  es: { label: "Asistente de IA", notice: "Estás chateando con un sistema de IA, no con una persona." },
  it: { label: "Assistente IA", notice: "Stai chattando con un sistema di IA, non con una persona." },
  nl: { label: "AI-assistent", notice: "Je chat met een AI-systeem, niet met een mens." },
  pl: { label: "Asystent AI", notice: "Rozmawiasz z systemem AI, a nie z człowiekiem." },
} as const;

export type Locale = keyof typeof AI_DISCLOSURE;

export function getDisclosure(locale: string) {
  const short = locale.slice(0, 2) as Locale;
  return AI_DISCLOSURE[short] ?? AI_DISCLOSURE.en;
}

Have a native speaker check each translation before shipping.

2. Persistent label in the chat header

// AiLabel.tsx
import { getDisclosure } from "./ai-disclosure";

export function AiLabel({ locale }: { locale: string }) {
  const d = getDisclosure(locale);
  return (
    <div role="note" aria-label={d.notice} className="ai-label">
      <span aria-hidden="true" className="ai-label__icon">AI</span>
      <span className="ai-label__text">{d.label}</span>
      <span className="ai-label__notice">{d.notice}</span>
    </div>
  );
}
.ai-label { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap;
  padding: .5rem .75rem; border-bottom: 1px solid #e5e5e5; font-size: .875rem; }
.ai-label__icon { font-weight: 700; border: 1.5px solid currentColor;
  border-radius: 4px; padding: 0 .3rem; font-size: .75rem; }
.ai-label__text { font-weight: 600; }
.ai-label__notice { color: #555; }

The label is always visible: no dismiss button and no hover-only tooltip.

3. First-message notice

Show the notice as the first assistant message, before the user types anything.

// Chat.tsx
import { useMemo, useState } from "react";
import { AiLabel } from "./AiLabel";
import { getDisclosure } from "./ai-disclosure";

type Message = { id: string; role: "assistant" | "user"; content: string; disclosure?: boolean };

export function Chat({ locale = "en", assistantName = "Acme AI" }: { locale?: string; assistantName?: string }) {
  const d = getDisclosure(locale);

  const firstMessage = useMemo<Message>(() => ({
    id: "disclosure",
    role: "assistant",
    content: `Hi, I'm ${assistantName}. ${d.notice} How can I help?`,
    disclosure: true,
  }), [assistantName, d.notice]);

  const [messages, setMessages] = useState<Message[]>([firstMessage]);
  const [input, setInput] = useState("");

  async function send() {
    if (!input.trim()) return;
    const userMsg: Message = { id: crypto.randomUUID(), role: "user", content: input };
    setMessages((m) => [...m, userMsg]);
    setInput("");
    // call your AI endpoint here and append the reply
  }

  return (
    <section aria-label={d.label} className="chat">
      <AiLabel locale={locale} />
      <ol className="chat__messages" aria-live="polite">
        {messages.map((m) => (
          <li key={m.id} data-role={m.role} data-disclosure={m.disclosure || undefined}>
            {m.content}
          </li>
        ))}
      </ol>
      <form onSubmit={(e) => { e.preventDefault(); send(); }}>
        <label htmlFor="chat-input" className="sr-only">Message</label>
        <input id="chat-input" value={input} onChange={(e) => setInput(e.target.value)} />
        <button type="submit">Send</button>
      </form>
    </section>
  );
}

Notes:

4. Server-side guard

Clients can be customised, embedded or rebuilt. Make the API add the disclosure on the first turn, so every client gets it, including mobile apps and third-party embeds.

// app/api/chat/route.ts (Next.js, framework-agnostic idea)
import { getDisclosure } from "@/lib/ai-disclosure";

export async function POST(req: Request) {
  const { messages, locale = "en" } = await req.json();
  const isFirstTurn = !messages.some((m: { role: string }) => m.role === "assistant");

  const reply = await generateReply(messages); // your model call

  return Response.json({
    reply: isFirstTurn ? `${getDisclosure(locale).notice}\n\n${reply}` : reply,
    ai_generated: true,              // machine-readable flag for clients and logs
    disclosure_version: "2026-10-20" // lets you prove what wording was live and when
  });
}

declare function generateReply(messages: unknown[]): Promise<string>;

5. Voice agents

Say it at the start of every call, before anything else:

"Hi, this is Acme's AI assistant. You're speaking with an automated AI system."

Log that the greeting played (call ID and timestamp) so you can prove it later.

Checklist

FAQ

Is there official wording? No. The law requires a clear disclosure at the latest at the first interaction. "You're interacting with an AI system" is a safe, plain pattern.

What if it's obviously an AI? There's an exception when it's obvious to a reasonably well-informed person from the context. It's read narrowly, so disclose anyway. It costs one line of text.

Does this cover AI-generated images in the chat? No. Generated images, audio, video and text also need machine-readable labels under Article 50(2). See the Article 50 developer guide.

Does this apply if we're not in the EU? Yes, if people in the EU use your chatbot.


Disclosure is the easy part. Labelling generated images and text, and proving it, is where teams get stuck. Label50 does disclosure, labelling and evidence in one SDK. Join the waitlist →

Technical guidance, not legal advice.