Summit Themes
Blog

How to add a Claude help button to your website with JavaScript

A help button powered by Claude can replace a static FAQ page with something genuinely useful: a small chat widget that answers questions about your product, services, or documentation without the user ever leaving the page. The Anthropic Messages API is straightforward to call, and a minimal working widget is less code than you might expect.

This guide walks through two approaches: a quick browser-direct version useful for internal tools or prototypes, and a thin server-side proxy that keeps your API key off the client — which is what you want for anything public-facing.

What you are building

A floating "?" button sits in the corner of the page. Clicking it opens a small chat panel. The user types a question; your JavaScript POSTs to the Anthropic API (or your proxy); the response streams back and appears in the panel. The whole thing is self-contained — no framework required, no build step, works in any HTML page.

Prerequisites

  • An Anthropic account with an API key.
  • A model to call. This guide uses claude-haiku-4-5 — it is the fastest current model and costs $1 / $5 per million input / output tokens, which is appropriate for a help widget that handles many short questions. Swap in claude-sonnet-4-6 if you need stronger reasoning.
  • For the production approach: a tiny backend you can write in Node.js, Cloudflare Workers, or any server that can make outbound HTTPS requests.

Option 1: Browser-direct (prototypes and internal tools only)

Anthropic added CORS support for the Messages API in 2024 via the anthropic-dangerous-direct-browser-access: true header. That header name is intentionally alarming — it exists to make developers think twice. If you use it, your API key is visible in the browser's network tab, which means anyone who visits your page can read it and rack up charges on your account. Use this only for local demos or internal tools behind authentication.

That said, it works, and it is the simplest way to understand the API shape before adding a proxy:

async function askClaude(userMessage, systemPrompt) {
  const response = await fetch("https://api.anthropic.com/v1/messages", {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "x-api-key": "sk-ant-YOUR_KEY_HERE",           // never do this in production
      "anthropic-version": "2023-06-01",
      "anthropic-dangerous-direct-browser-access": "true"
    },
    body: JSON.stringify({
      model: "claude-haiku-4-5",
      max_tokens: 512,
      system: systemPrompt,
      messages: [{ role: "user", content: userMessage }]
    })
  });

  if (!response.ok) {
    throw new Error(`API error ${response.status}`);
  }

  const data = await response.json();
  return data.content[0].text;
}

The system parameter is where you focus the model on your site. Keep it short and specific: "You are a help assistant for Ridgeline Roofing. Answer questions about our services, service areas, and pricing. If you do not know something, say so." A tight system prompt dramatically reduces off-topic responses without any additional filtering logic.

The safer pattern is to expose a single endpoint on your own server that accepts the user's message, appends your system prompt server-side, calls Anthropic, and returns the reply. Your API key never leaves the server.

Here is a minimal Cloudflare Worker (deploy with wrangler deploy), but the same logic works in Express or any other runtime:

// worker.js — deploy as a Cloudflare Worker
// Set your API key as a secret: wrangler secret put ANTHROPIC_API_KEY

export default {
  async fetch(request, env) {
    // Allow your domain only
    const origin = request.headers.get("Origin") || "";
    const allowed = ["https://yourdomain.com"];
    if (!allowed.includes(origin)) {
      return new Response("Forbidden", { status: 403 });
    }

    if (request.method === "OPTIONS") {
      return new Response(null, {
        headers: {
          "Access-Control-Allow-Origin": origin,
          "Access-Control-Allow-Methods": "POST",
          "Access-Control-Allow-Headers": "Content-Type"
        }
      });
    }

    if (request.method !== "POST") {
      return new Response("Method not allowed", { status: 405 });
    }

    let body;
    try {
      body = await request.json();
    } catch {
      return new Response("Bad request", { status: 400 });
    }

    const { message } = body;
    if (!message || typeof message !== "string" || message.length > 2000) {
      return new Response("Invalid message", { status: 400 });
    }

    const anthropicResponse = await fetch("https://api.anthropic.com/v1/messages", {
      method: "POST",
      headers: {
        "content-type": "application/json",
        "x-api-key": env.ANTHROPIC_API_KEY,
        "anthropic-version": "2023-06-01"
      },
      body: JSON.stringify({
        model: "claude-haiku-4-5",
        max_tokens: 512,
        system: "You are a help assistant for [Your Business]. Answer questions about our services concisely. If you do not know, say so.",
        messages: [{ role: "user", content: message }]
      })
    });

    if (!anthropicResponse.ok) {
      return new Response("Upstream error", { status: 502 });
    }

    const data = await anthropicResponse.json();
    const reply = data.content[0].text;

    return new Response(JSON.stringify({ reply }), {
      headers: {
        "Content-Type": "application/json",
        "Access-Control-Allow-Origin": origin
      }
    });
  }
};

The widget HTML and JavaScript

With the proxy in place, the browser code is simple. Drop this at the bottom of your page's <body> and replace the proxy URL:

<!-- Help widget styles and markup -->
<style>
  #help-btn {
    position: fixed; bottom: 24px; right: 24px;
    width: 52px; height: 52px; border-radius: 50%;
    background: #1d4ed8; color: #fff; border: none;
    font-size: 22px; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,.25);
    z-index: 9999;
  }
  #help-panel {
    display: none; position: fixed; bottom: 88px; right: 24px;
    width: 320px; max-height: 480px; background: #fff;
    border: 1px solid #e5e7eb; border-radius: 12px;
    box-shadow: 0 8px 24px rgba(0,0,0,.15); z-index: 9999;
    display: flex; flex-direction: column; overflow: hidden;
  }
  #help-panel.closed { display: none !important; }
  #help-messages {
    flex: 1; overflow-y: auto; padding: 16px;
    font-size: 14px; line-height: 1.5; color: #111827;
  }
  .msg-user { text-align: right; margin-bottom: 10px; }
  .msg-user span {
    background: #1d4ed8; color: #fff;
    padding: 6px 12px; border-radius: 16px 16px 4px 16px;
    display: inline-block; max-width: 80%;
  }
  .msg-bot { margin-bottom: 10px; }
  .msg-bot span {
    background: #f3f4f6;
    padding: 6px 12px; border-radius: 16px 16px 16px 4px;
    display: inline-block; max-width: 80%;
  }
  #help-form {
    display: flex; padding: 12px; border-top: 1px solid #e5e7eb; gap: 8px;
  }
  #help-input {
    flex: 1; border: 1px solid #d1d5db; border-radius: 8px;
    padding: 8px 10px; font-size: 14px; outline: none;
  }
  #help-submit {
    background: #1d4ed8; color: #fff; border: none;
    border-radius: 8px; padding: 8px 14px; cursor: pointer; font-size: 14px;
  }
  #help-submit:disabled { opacity: .5; cursor: not-allowed; }
</style>

<button id="help-btn" aria-label="Open help chat">?</button>

<div id="help-panel" class="closed" role="dialog" aria-label="Help chat">
  <div id="help-messages">
    <div class="msg-bot"><span>Hi! Ask me anything about our services.</span></div>
  </div>
  <form id="help-form">
    <input id="help-input" type="text" placeholder="Type your question…" autocomplete="off" />
    <button id="help-submit" type="submit">Send</button>
  </form>
</div>

<script>
(function () {
  const btn = document.getElementById("help-btn");
  const panel = document.getElementById("help-panel");
  const messages = document.getElementById("help-messages");
  const form = document.getElementById("help-form");
  const input = document.getElementById("help-input");
  const submit = document.getElementById("help-submit");

  const PROXY_URL = "https://your-worker.your-subdomain.workers.dev";

  btn.addEventListener("click", () => {
    const isOpen = !panel.classList.contains("closed");
    panel.classList.toggle("closed", isOpen);
    if (!isOpen) input.focus();
  });

  function addMessage(text, role) {
    const row = document.createElement("div");
    row.className = role === "user" ? "msg-user" : "msg-bot";
    const bubble = document.createElement("span");
    bubble.textContent = text;
    row.appendChild(bubble);
    messages.appendChild(row);
    messages.scrollTop = messages.scrollHeight;
    return bubble;
  }

  form.addEventListener("submit", async (e) => {
    e.preventDefault();
    const text = input.value.trim();
    if (!text) return;

    input.value = "";
    submit.disabled = true;
    addMessage(text, "user");
    const thinking = addMessage("…", "bot");

    try {
      const res = await fetch(PROXY_URL, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ message: text })
      });

      if (!res.ok) throw new Error("Request failed");
      const data = await res.json();
      thinking.textContent = data.reply;
    } catch {
      thinking.textContent = "Sorry, something went wrong. Please try again.";
    } finally {
      submit.disabled = false;
      input.focus();
    }
  });
})();
</script>

Practical tuning tips

Write a specific system prompt

The system prompt is the single biggest lever for quality. Name the business, list the services, state what the assistant should not answer, and instruct it to keep replies short. Something like: "You are the help assistant for Copperline Plumbing. We serve Austin, Round Rock, and Cedar Park. Answer questions about our services, pricing ranges, and booking. Do not answer unrelated questions. Keep responses under three sentences."

Limit input length on both ends

The proxy above already rejects messages over 2,000 characters. On the client, you can also add maxlength="500" to the input element. This controls costs and prevents prompt-injection attempts where a user pastes a wall of text trying to override the system prompt.

Keep max_tokens low for a help widget

512 tokens is plenty for most FAQ-style answers. Lower limits mean faster responses and lower cost. If you find the model cutting off, raise it to 768; you rarely need more than 1024 for a help chat context.

Multi-turn conversations

The example above sends only the latest message. For a back-and-forth conversation, keep a local array of { role, content } pairs and send the full array in each request. Be aware that each request then costs tokens for the full history — for a help widget, resetting on panel close is usually the right call.

Conclusion

The core of a Claude help button is about 50 lines of JavaScript and a small server function. The meaningful work is in the system prompt and in deciding what questions your widget should and should not answer. Get those right and you have something more useful than a FAQ page — a widget that handles the long tail of questions you never thought to write down.