# ask-ai-widget > Zero-dependency "Ask AI about this page" button row (also called "Open in ChatGPT" or "Summarize with AI" buttons). Each button opens ChatGPT, Claude, Perplexity, Gemini, Grok, Mistral or DeepSeek in a new tab with a prompt containing the page title and URL (Gemini and DeepSeek: the prompt is copied to the clipboard instead). One ` ``` Vanilla (the target element must exist before `mount()` runs; it returns null otherwise): ```js import { mount } from "ask-ai-widget"; mount("#ask-ai", { services: ["claude", "chatgpt"], label: "Ask AI about Acme" }); ``` React: ```tsx import { AskAI } from "ask-ai-widget/react"; ``` Vue: ```vue ``` Svelte: ```svelte
``` Lite (tree-shaken, only the logos you import): ```js import { mount } from "ask-ai-widget/lite"; import { claude, chatgpt } from "ask-ai-widget/services"; mount("#ask-ai", { services: [claude, chatgpt] }); ``` CMS recipes (no build step): WordPress: add the script through a header/footer code plugin or the theme and put the `` tag in a Custom HTML block. Shopify: script in `theme.liquid`, tag in a Custom Liquid section. Ghost: code injection plus an HTML card. Webflow: custom code in the footer plus an Embed element. Squarespace: Code Injection plus a Code block. Dropdown instead of a row: ```html ``` JS: `mount("#w", { layout: "menu" })`; React ``; Vue ``; Svelte `use:askAI={{ layout: "menu" }}`. Let visitors type a question: ```html ``` The `input` attribute is read as a selector only when it starts with `#` or `.`; anything else (`input="[name=q]"`) renders the built-in field instead. JS equivalents: `input: true`, `input: { placeholder, required, multiline }`, `input: "#q"`, `input: element`, `input: { target: "#q" | element, required: false }` (bound field that may be empty). ## Options (JS name / `` attribute) - `services` / `services="a,b"`: keys or ServiceDef objects, in display order. Default: all 7 built-ins. Keys: chatgpt, claude, perplexity, gemini, grok, mistral, deepseek (case-insensitive). - `label` / `label`: text next to the buttons. - `labelPosition` / `label-position` (CDN `data-label-position`): `left` (default) | `top` | `right` | `bottom` | `none`. `none` hides the visible text; the label stays the accessible name. The label is always first in the DOM, CSS places it. With the question field, field + buttons stay on one row (buttons right of the field); under ~440px a left/right label moves above. Ignored by the menu layout (the label is the trigger text). - `variant` (soft|outline|solid|ghost|brand|custom name), `shape` (circle|rounded|square), `size` (sm|md|lg), `theme` (light|dark|auto; default light). - `layout` / `layout`: `row` (default) or `menu`. Menu = one "Ask AI" trigger button that opens a dropdown listing every service (logo + name); choosing one behaves like clicking it in the row. `label` is then the trigger text (default `strings.menu`, "Ask AI"). `menuAlign` / `menu-align` (`start` default | `end`). `menuIcons` / `menu-icons` (`stack` default: up to 3 overlapping logos in the trigger | `none`). CDN: `data-layout="menu"`. Keyboard: ArrowDown/Up/Enter/Space on the trigger, arrows/Home/End/type-ahead/Escape/Tab in the menu. Panel colors: `--aai-menu-bg`, `--aai-menu-fg`, `--aai-menu-border`, `--aai-menu-shadow`, `--aai-menu-radius`, `--aai-menu-z`. - `showLabels` / `show-labels`: boolean. - `prompt`, `instructions`, `context`, `template` / same-named attributes. Strings with `{question} {url} {title} {description} {selection} {service}` and any key of `vars`; or functions `(ctx) => string` (JS only). `context: false` / `context="none"` removes page context. - `references` / `references="/llms.txt, /docs"`: URLs the agent should read; relative URLs resolve against the page. - `vars` / `vars='{"k":"v"}'` (JSON), `page` / `page='{"url":"...","title":"..."}'` (JSON). - `maxLength` / `max-length`: default 4000 chars; too long: context is cut first, then references (whole lines), then instructions, and the prompt is hard-clipped last. - `input` / `input`, `placeholder`, `required`, `multiline` (`input="#q" required="false"` = bound field, empty allowed); `submitService` / `submit-service`. - `strings` / `strings='{"ask":"Frage {service}"}'` (JSON): translates the widget's own texts. Keys `ask` (button aria-label/title, default "Ask {service}"), `question` (input aria-label, default "Your question"), `copied` (clipboard hint, default "Prompt copied — paste it into {service}"), `menu` (trigger text of the menu layout when no `label`, default "Ask AI"). `{service}` = service label. - `className` / `class-name`; `unstyled` / `unstyled`. - With the CDN auto-mount every option also works as `data-` on any element with `data-ask-ai`: `
`. Call `AskAI.init()` after inserting more such elements. - `onOpen(detail)` (JS property or prop only). Vue: `@open`. DOM event `ask-ai:open` (bubbles) with `detail = { service, prompt, url }`. - React-only props: `question`, `defaultQuestion`, `onQuestionChange`. Vue-only: `v-model:question`. Default prompt: "Explain this page." (or "{question}" when `input` is set). Default context: "Context: {title} — {url}". Default template: "{instructions}\n\n{prompt}\n\n{context}\n\n{references}". ## Services Each opens `?q=`: chatgpt https://chatgpt.com/, claude https://claude.ai/new, perplexity https://www.perplexity.ai/search, grok https://grok.com/, mistral https://chat.mistral.ai/chat (prefill unverified), gemini https://gemini.google.com/app and deepseek https://chat.deepseek.com/ (no URL prefill: the prompt is copied to the clipboard and a hint is shown; the clipboard needs HTTPS). Custom: `registerService({ key, label, url, param, icon?, color?, prefill? })` or pass such an object inline in `services`. `icon` is SVG path data for a 24x24 viewBox. ## Common mistakes 1. Prompts must be synchronous. Do not pass async functions; the tab is opened inside the click handler so popup blockers stay quiet. 2. The `lite*` entries have NO built-in services. `services: ["claude"]` fails there (warns and renders nothing); pass objects imported from `ask-ai-widget/services`, or call `registerService(claude)` first. 3. On the `` element use the `class-name` attribute for the `className` option (or plain `class`). `el.className` is the DOM's own. 4. For SSR (Next.js, Nuxt, SvelteKit) import `ask-ai-widget/style.css` to avoid unstyled flash, and pass `page={{ url, title }}` (React) / `:page` (Vue) so server hrefs match the client. 5. In Next.js Server Components you cannot pass functions to `AskAI`; use string templates, or wrap it in your own "use client" component for `onOpen`/function props. 6. `ask-ai-widget` exports `registry` (the object of services added via `registerService`, empty by default). The built-in service objects live in `ask-ai-widget/services`. 7. Both element entries (`element`, `lite/element`) define the same `` tag; import only one per page. 8. Set the public `--aai-*` custom properties anywhere: `:root`, an ancestor, the widget or inline all work (the library never declares them; it reads `var(--aai-x, private default)`). Do not use the private `--_*` names. Host rules on bare elements (`a:visited`, `.prose a`, `button:hover`) do not leak in. A host rule with an id or at least two classes plus an element (`#content a`, `.post .body a`) can beat the button rule; scope it (`a:not(.aai__btn)`), because `--aai-fg` cannot help when the host sets `color` directly. 9. Unknown `{placeholders}` render as an empty string. A literal `{text}` in a prompt vanishes. 10. A bare selector or element in `input` is always required (empty question blocks opening). To allow empty, use `input: { target: "#q", required: false }`; the built-in field uses `input: { required: false }`. 11. Strict CSP: the widget injects a `