# 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 `