OneTag AI / Guides

AI chatbot in HTML, CSS and JavaScript: complete working code

How do I build a working AI chatbot with only HTML, CSS and JavaScript?

Save the code below as chatbot.html, put your key in it and open it. You have a chatbot that answers with a real AI model, shows the reply as it is written and remembers the conversation. It is one file of HTML, CSS and JavaScript, 77 lines, with no framework, no build step and no server.

The part most tutorials get wrong is the API key. A page that calls OpenAI or Gemini directly carries a secret key in its JavaScript, where any visitor can copy it. This version calls OneTag AI, whose key is made to sit in page source: it can only spend its own fixed budget, which on the free plan is 1,000,000 tokens a month with no card.

Live demo on a shared demo key, with the same code as below. Open it in its own tab.

Steps

  1. Get a free key

    Enter your email at onetagai.com. The key (it starts with ot_) arrives by email. One key per email, no card.

  2. Save this as chatbot.html

    The whole chatbot: the chat window in HTML, the bubbles in CSS, and the conversation in JavaScript.

    <!doctype html>
    <html lang="en">
    <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>AI chatbot</title>
    <style>
      :root { --accent: #0b5fff; --ink: #0b1020; --soft: #f5f7fb; }
      * { box-sizing: border-box; }
      body { max-width: 760px; margin: 0 auto; height: 100dvh; display: flex; flex-direction: column; font: 16px/1.5 system-ui, sans-serif; color: var(--ink); }
      header { padding: 12px 16px; border-bottom: 1px solid var(--ink); font-weight: 600; }
      #log { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 12px; }
      .msg { max-width: 80%; padding: 8px 12px; border: 1px solid var(--ink); white-space: pre-wrap; overflow-wrap: anywhere; }
      .user { align-self: flex-end; background: var(--accent); border-color: var(--accent); color: #fff; }
      .bot { align-self: flex-start; background: var(--soft); }
      .bot:empty::after { content: "…"; }
      form { display: flex; border-top: 1px solid var(--ink); }
      input { flex: 1; min-width: 0; padding: 14px 16px; border: 0; font: inherit; outline: none; }
      button { padding: 0 24px; border: 0; background: var(--accent); color: #fff; font: inherit; font-weight: 600; cursor: pointer; }
      button:disabled { opacity: .5; cursor: default; }
    </style>
    </head>
    <body>
    <header>Acme Bikes assistant</header>
    <div id="log" aria-live="polite"><div class="msg bot">Hi! Ask me anything about Acme Bikes.</div></div>
    <form id="chat">
      <input id="q" placeholder="Type a message" autocomplete="off" required>
      <button id="send">Send</button>
    </form>
    
    <script src="https://onetagai.com/v1.js" data-key="YOUR_ONETAG_KEY"></script>
    <script>
      // What the bot is and what it knows. Replace with your own facts.
      const SYSTEM = `You are the assistant of Acme Bikes, a bike shop in Portland.
    Open Monday to Saturday, 9am to 6pm. Repairs take two working days.
    A tune-up costs $60 and a full service costs $140.
    Answer in under 60 words. If you do not know, say so.`;
    
      const log = document.getElementById("log");
      const q = document.getElementById("q");
      const send = document.getElementById("send");
      const history = [];
    
      function bubble(who, text) {
        const el = document.createElement("div");
        el.className = "msg " + who;
        el.textContent = text;
        log.appendChild(el);
        log.scrollTop = log.scrollHeight;
        return el;
      }
    
      document.getElementById("chat").addEventListener("submit", async (event) => {
        event.preventDefault();
        const question = q.value.trim();
        if (!question) return;
        q.value = "";
        send.disabled = true;
        bubble("user", question);
        const bot = bubble("bot", "");
        try {
          const answer = await onetag.ask(question, null, {
            system: SYSTEM,
            history: history.slice(-20),
            stream: (delta) => { bot.textContent += delta; log.scrollTop = log.scrollHeight; },
          });
          bot.textContent = answer;
          history.push({ role: "user", content: question }, { role: "assistant", content: answer });
        } catch (error) {
          bot.textContent = error.code === "quota_exceeded" ? "The chat is resting for now. Please come back later." : "Something went wrong: " + error.message;
        }
        send.disabled = false;
        q.focus();
      });
    </script>
    </body>
    </html>

  3. Put in your key and open it

    Replace YOUR_ONETAG_KEY with your key and double-click the file. It runs from your disk, and from any host once you upload it: GitHub Pages, Netlify, your own server.

  4. Make it your bot

    Rewrite the SYSTEM text: say who the bot is, list the facts it should know (opening hours, prices, policies) and how long its answers should be. Change --accent in the CSS to your colour, and the header and greeting to your name.

How the code works

In our test on 2026-11-11 the first words appeared about 2 seconds after sending, and a follow-up question about the earlier message was answered correctly.

Why not call the ChatGPT API from the page?

The usual "ChatGPT in HTML" code sends a fetch to the model provider with the secret key written in the script. Two widely read tutorials do this (both read on 2026-11-10): CodingNepal's puts a Google Gemini key in the page's JavaScript, and GeeksforGeeks' puts an OpenAI key there. That is fine while the file stays on your computer. Once the page is public, the key is in the source for anyone to copy, and a copied key spends your account's money until you revoke it.

There are two ways out. Run a small server or serverless function that holds the key and forwards the request, or use a key that is designed to be public. This page does the second. If you need a specific OpenAI model, do the first; Can I put an AI API key in HTML? covers both.

Give the bot your own knowledge

The model knows only what you send it. Short facts go in SYSTEM. To answer from the content of a page, pass an element as the second argument and the bot reads its text:

const answer = await onetag.ask(question, document.querySelector("main"), {
  system: "Answer from the page text only. If the page does not say, say so.",
  history: history.slice(-20),
});

One call takes up to 32,000 characters of text. The sidebar layout on Chatbot HTML templates works this way. To answer from a whole site, see Add 'ask the docs' AI search.

Keep the conversation after a reload

The page forgets the chat when it is reloaded. To keep it, load the history from localStorage, draw the saved messages, and save after each answer. Replace the const history = []; line with the first two lines, and add the third after history.push(...):

const history = JSON.parse(localStorage.getItem("chat") || "[]");
history.forEach((m) => bubble(m.role === "user" ? "user" : "bot", m.content));

localStorage.setItem("chat", JSON.stringify(history));

What a conversation costs

In our test on 2026-11-10, one turn with a system line, two earlier messages and a one-sentence answer used 124 tokens. 1,000,000 tokens is about 8,000 turns of that size. The earlier messages are sent again with every turn, so long conversations cost more per message; sending only the last 20 caps that.

When a month's tokens are used up, the bot shows its "resting" line until the 1st. There is no bill on the free plan.

Free key
Price$0, no credit card
Tokens1,000,000 a month per key, input and output counted together. Starts again on the 1st (UTC)
Rate limit60 requests a minute per key, 20 a minute per IP address
Per callUp to 32,000 characters of text and one image up to 2 MB; answers up to 10,000 tokens (500 by default)
Where the key worksAny web page on any domain, and curl or server code
FeaturesText answers, streaming, JSON output, image input, chat history
ModelOne fast model served through OpenRouter. You do not choose it
DataPrompts, inputs and answers are not stored. Only token counts per key are kept
When it runs outCalls return quota_exceeded until the 1st and you get an email. Pro is $9.9 a month for 100,000,000 tokens a month; the key stays the same

Limits to know before you rely on it

Questions

Can I make a chatbot with only HTML and CSS?

No. HTML and CSS draw the chat window. Sending a message and showing an answer needs JavaScript, and something has to write the answer. In the code on this page that is about 40 lines of JavaScript and one call to a hosted AI model.

Is this ChatGPT?

No. The answers come from a fast open model served through OpenRouter, and the request is not in the OpenAI format. To use an OpenAI model you need a server that holds your OpenAI key.

Do I need Node.js, React or a backend?

No. It is one static HTML file. The only thing outside the file is the script tag that loads the OneTag AI SDK.

Does it work if I open the file from my computer?

Yes. We opened it from disk (a file:// address) on 2026-11-11 and it answered. Free keys accept requests from any page.

How do I put this chatbot on an existing website?

Use the floating chat bubble instead of the full page: see How to add an AI chatbot to an HTML website.

Can I use this code in my own project?

Yes. Copy it, change it and ship it, with no attribution needed. The AI calls count against the key you put in.

Is it safe to put the OneTag AI key in my page?

Yes, it is designed for that. The key can only spend its own token budget and cannot read or change anything on your account. On the Pro plan you can lock it to your domain so other sites cannot use it; free keys work from any page.

Does it cost anything?

A free key has 1,000,000 tokens a month, reset on the 1st, enough for a few hundred summaries a month. Pro is $9.9 a month for 100,000,000 tokens a month and the domain lock.

What happens to the text my visitors send?

It goes to the model and comes back. OneTag AI stores token counts per key for the quota and keeps no prompts, inputs or answers. The access log carries no IP addresses.

Try it on your sitefree

Get a free key by email, paste one tag, call onetag.ask. 1,000,000 tokens a month, no card.

Get a free key

Updated 2026-11-11. Something wrong or missing? Email support@odysseyx.app.