@magicx-eng/ai-autocomplete-react
v0.12.1
Published
AI Autocomplete React SDK — guided autocomplete with pill-based input and dropdown suggestions
Maintainers
Readme
@magicx-eng/ai-autocomplete-react
A React/TypeScript SDK that provides a guided AI-powered autocomplete experience with pill-based input and dropdown suggestions. Powered by @magicx-eng/ai-autocomplete-vanilla under the hood.
Features
- Three tiers of integration — the full
<AIAutocomplete />component,useAIAutocomplete()+<AIAutocompleteDropdown />(our dropdown, your input), oruseAIAutocomplete()alone (render the dropdown yourself) - Rich inline input (Tier 1) — a single
contentEditablesurface: typed text, bold completed params, and inline pills share one editing context - Pill-based input — non-editable inline pills for unfilled parameters, bold inline text for completed ones
- Instant exact-match bolding — typing the full text of an option immediately promotes it to a completed param (no debounced fetch wait). Works in the normal typing flow and while re-editing an existing completed param.
- Re-edit completed params — tap a bold completed param to replace it; the dropdown re-opens with the cached options the server originally returned for that param
- Pill placement — render pills inline in the input or inside the dropdown
- Light/dark mode — built-in themes with
prefers-color-schemesupport, fully overridable via CSS variables - Inherits your font — defaults to the host page's font, with
--aia-font-familyto pin a specific font on the library - Access token auth — short-lived tokens with automatic refresh, single-flight deduplication, and 401 retry
- Keyboard navigation — arrow keys, enter to submit, tab to autocomplete, backspace to un-bold the last completed param
- IME-safe — composition events are buffered so input text is committed once, after composition ends
- Client-side filtering — instant substring filtering on every keystroke
- Option overrides — inject or dynamically generate client-side options per suggestion type
- Product strip (opt-in) — plug in any platform's product search and the dropdown renders a horizontal row of product cards below the options; the SDK owns the UI, your integration owns only
fetchandtransform - Controlled & uncontrolled — works out of the box or integrates with external state
- Ref forwarding — imperative
focus(),blur(),reset(), andsetMode()via ref - Accessible — ARIA combobox 1.2 pattern with
role="listbox",aria-activedescendant - Animations — option selection streak animation, text shimmer on newly added params
- Loading skeleton — while a fetch is in flight, the dropdown and inline pills keep the previous layout (same count and widths) with their text masked and a shimmer pulse. The skeleton is held back until the selection streak animation finishes, so taps don't visually "stutter" into loading.
- Lightweight — styles auto-injected at runtime
- TypeScript first — full type definitions shipped with the package
Installation
pnpm add @magicx-eng/ai-autocomplete-reactPeer Dependencies
React 17 or later:
pnpm add react react-domThree Tiers
| Tier | What you get | What you own | Use when |
|---|---|---|---|
| Tier 1: Full | <AIAutocomplete /> — input, dropdown, pills, state | Nothing — drop in and go | You want a complete widget with zero setup |
| Tier 2: Hook + dropdown | useAIAutocomplete() + <AIAutocompleteDropdown /> | The input element and layout | You need a custom input but want our dropdown UI |
| Tier 3: Headless | useAIAutocomplete() alone — state, actions, dropdownProps data | Everything, including the dropdown | You need full control over every piece of the UI |
Tier 1: Full Component
Drop-in component that owns the input, pills, dropdown, and all state:
import { AIAutocomplete } from "@magicx-eng/ai-autocomplete-react";
function App() {
return (
<AIAutocomplete
apiConfig={{ endpoint: "https://api.example.com/ac/suggest", apiKey: "your_api_key" }}
onSubmit={(result) => {
console.log(result.query); // "Create a email"
console.log(result.raw_query); // "Create a {{TASK_1}}"
console.log(result.completed_params); // [{ placeholder: "{{TASK_1}}", type: "task", ... }]
}}
className="my-autocomplete"
/>
);
}Controlled Mode
const [text, setText] = useState("");
const [params, setParams] = useState([]);
<AIAutocomplete
value={text}
onChange={setText}
completedParams={params}
onParamsChange={setParams}
onSubmit={(result) => console.log(result)}
/>Imperative Handle
import { useRef } from "react";
import { AIAutocomplete, type AIAutocompleteHandle } from "@magicx-eng/ai-autocomplete-react";
const ref = useRef<AIAutocompleteHandle>(null);
// ref.current?.focus()
// ref.current?.blur()
// ref.current?.reset()
// ref.current?.setMode("dark")
<AIAutocomplete ref={ref} onSubmit={handleSubmit} />Focus Control
The component auto-focuses the contentEditable editor on mount. To opt out, pass autoFocus={false}. Listen to focus changes with onFocus / onBlur:
<AIAutocomplete
autoFocus={false}
onFocus={() => setIsActive(true)}
onBlur={() => setIsActive(false)}
onSubmit={handleSubmit}
/>Backspace into a completed param
Pressing Backspace while the caret is inside or immediately after a bold completed param drops the param's "completed" status and removes one grapheme before the caret. The remaining text stays in the editor as plain (un-bold) text so the user can keep editing instead of losing the whole phrase.
Re-edit a completed param
Tapping a bold completed param enters re-edit mode — the dropdown re-opens with the cached options the server originally returned for that param. From there:
- Typing atomically replaces the bold with what you type. If what you type exactly matches one of the cached options, it's re-promoted to a bold completed param immediately.
- Clicking an option replaces the bold with the new selection.
- Arrow keys, Escape, or clicking outside the param exit re-edit mode without changing anything.
After a completed param is added (by any means — option click, exact-match typing, or re-edit), the caret always lands right after the trailing space following the bold so typing can continue immediately. A space is inserted if one wasn't already there.
Tier 2: Hook + Dropdown
Use the hook to drive state and render our dropdown; you own the input element and layout:
import { useAIAutocomplete, AIAutocompleteDropdown } from "@magicx-eng/ai-autocomplete-react";
function App() {
const {
completedParams,
suggestionPills,
segments,
inputProps,
dropdownProps,
isLoading,
error,
reset,
} = useAIAutocomplete({
onSubmit: (result) => {
handleMySubmit(result);
reset(); // start a new session
},
apiConfig: { apiKey: "your_api_key" },
});
return (
<div style={{ position: "relative" }}>
<textarea {...inputProps} />
{/* `mode` styles + themes the dropdown on its own — no wrapper needed */}
<AIAutocompleteDropdown {...dropdownProps} mode="auto" />
</div>
);
}Always call
reset()after handling submit. It clears the input and rotates the per-sessionsession_id. This applies whether the submit was triggered by Enter, a custom button, or any other mechanism.
Standalone dropdown styling (mode)
<AIAutocompleteDropdown /> reads its design tokens + color mode from a .magicx-aia ancestor. When you render it on its own (Tier 2/3), pass mode ("light" | "dark" | "auto") and it self-scopes those tokens to its own root — no .magicx-aia wrapper required. "auto" follows prefers-color-scheme. Leave mode unset only when the dropdown is already inside a .magicx-aia element (e.g. Tier 1). optionsPosition flows through dropdownProps, so above/below placement works with no extra CSS.
Custom / rich-text inputs
inputProps is shaped for a <textarea>. For a contentEditable or rich-text editor (Tiptap, ProseMirror, Lexical…), don't spread inputProps — call the actions directly: handleTextChange(text) on every edit, setFocused(bool) on focus/blur, handleKeyDown(event) for Arrow/Enter/Tab/Escape while the dropdown is open, and handleCaretMove(offset) so arrow keys can move into the dropdown.
Tier 3: Headless
Skip <AIAutocompleteDropdown /> and render the suggestions UI yourself. dropdownProps carries the data + actions — the active suggestion's options, the highlighted index, isOpen, and onSelect / onHighlight:
import { useAIAutocomplete } from "@magicx-eng/ai-autocomplete-react";
function App() {
const { inputProps, dropdownProps, reset } = useAIAutocomplete({
apiConfig: { apiKey: "your_api_key" },
});
const { suggestions, activeIndex, isOpen, onSelect, onHighlight } = dropdownProps;
const options = suggestions[0]?.options ?? [];
return (
<div style={{ position: "relative" }}>
<textarea {...inputProps} />
{isOpen && (
<ul className="my-dropdown" role="listbox">
{options.map((option, i) => (
<li
key={option.text}
role="option"
aria-selected={i === activeIndex}
onMouseEnter={() => onHighlight(i)}
onMouseDown={(e) => {
e.preventDefault(); // keep focus in the input
onSelect(option);
}}
>
{option.text}
</li>
))}
</ul>
)}
</div>
);
}API Reference
<AIAutocomplete />
| Prop | Type | Default | Description |
|---|---|---|---|
| onSubmit | (result: AutocompleteResult) => void | required | Called on Enter or submit button. |
| onError? | (error: Error) => void | — | Called when a fetch fails. |
| apiConfig? | APIConfig | — | Runtime API configuration (see below). |
| additionalContext? | Record<string, unknown> | — | Optional user context. Include whatever you know about the user (a profile, preferences, workspace, anything) to personalize suggested parameters and options to them. |
| optionOverrides? | Record<string, (query: string) => SuggestionOption[]> | — | Override options per suggestion type. |
| maskCompletedText? | boolean | false | When true, omits completed params' literal text from API requests (for masking PII/sensitive values from the server). |
| className? | string | — | CSS class applied to the container. |
| columns? | number | 2 | Number of columns in the dropdown grid. |
| pillPlacement? | "inline" \| "dropdown" \| "hidden" | "dropdown" | Where to render unfilled pills. "hidden" hides pills entirely. |
| mode? | "light" \| "dark" \| "auto" | "auto" | Color mode. "auto" follows prefers-color-scheme. |
| optionsPosition? | "above" \| "below" | "below" | Where the dropdown opens relative to the input. |
| animations? | boolean | true | Enable/disable all SDK animations (streak + shimmer). |
| dropdownTrigger? | "auto" \| "manual" \| "hidden" | "auto" | When the dropdown appears. "auto" = when options available. "manual" = only on pill tap, closes after selection. "hidden" = never shows. |
| closeDropdownOnBlur? | boolean | true | When true, the dropdown closes if the input loses focus. Set to false to keep it open whenever options are available, regardless of focus. |
| showNonTappableOptions? | boolean | true | When true, non-tappable options are rendered alongside tappable ones in the dropdown. Set to false to hide non-tappable options entirely. |
| showSkipButton? | boolean | true | When true, the dropdown's pill bar ends in a small "skip" button that dismisses the active pill — same action as pressing → at the end of the input. It sits top-right when the dropdown opens below the input, bottom-right when optionsPosition is "above", and appears once the input has text (alongside the footer's "→ to skip" hint). Set to false to hide it. |
| autoFocus? | boolean | true | Focus the input on mount. Set to false to leave focus to the consumer. |
| onFocus? | () => void | — | Called when the input gains focus. |
| onBlur? | () => void | — | Called when the input loses focus. |
| value? | string | — | Controlled text value. |
| completedParams? | CompletedParamState[] | — | Controlled completed params. |
| onChange? | (value: string) => void | — | Called when text changes (controlled mode). |
| onParamsChange? | (params: CompletedParamState[]) => void | — | Called when params change (controlled mode). |
| products? | ProductsConfig | — | Opt-in product strip — see Product strip. Omit it and nothing about the dropdown changes. |
| onProductSelect? | (product: Product) => void | — | Called when a product card is activated. The SDK never navigates. |
| submitButton? | ReactNode | — | Custom submit button. Pass any ReactNode to replace the default arrow button. Pass null to render no button. Clicks bubble up and trigger submit, so consumer-supplied buttons work without re-wiring onClick. |
| ref? | Ref<AIAutocompleteHandle> | — | Imperative handle with focus(), blur(), reset(), and setMode(). |
Product strip
Opt in with products and the dropdown renders a horizontal row of product
cards below the options grid. Every platform (Shopify today, others later) has
its own search endpoint and its own response shape, so the SDK owns the UI and
the integration owns only the fetching and the mapping.
import { AIAutocomplete, type Product } from "@magicx-eng/ai-autocomplete-react";
const products = {
// You own the request entirely — auth headers, GraphQL body, locale
// prefixes. Honour the signal: the SDK aborts it as soon as a newer query
// supersedes this one.
fetch: (query: string, signal: AbortSignal) =>
fetch(`/api/products?q=${encodeURIComponent(query)}`, { signal }).then((r) => r.json()),
// Pure mapping, kept out of `fetch` so you can unit-test it without a
// network.
transform: (raw: unknown): Product[] =>
(raw as ApiResponse).items.map((item) => ({
id: item.id,
title: item.title,
url: item.url,
imageUrl: item.image?.src ?? null, // null renders the placeholder tile
price: formatMoney(item.price), // you know the currency
vendor: item.brand,
})),
limit: 8, // applied by the SDK, after transform
};
<AIAutocomplete
onSubmit={handleSubmit}
products={products}
// Selection emits — the SDK never navigates.
onProductSelect={(product) => router.push(product.url)}
/>;Product is exactly six fields; only id, title and url are required:
type Product = {
id: string;
title: string;
url: string;
imageUrl?: string | null;
price?: string;
vendor?: string;
};What the SDK guarantees
- One fetch cadence. The product search rides the same debounce, the same
AbortControllerand the same version guard as/suggest— there is no second timer to drift out of step. - Empty queries never reach you. On mount and after
reset()the strip clears. - Out-of-order responses are dropped. A slow response for an older query is never rendered over a newer one.
- Failure is silent. A rejected
fetchor a throwingtransformclears the strip, logs once, and leaves the suggestions half untouched — noerrorstate, noonError, no effect onisLoading. - The two halves are independent. The panel opens if either has content,
so products keep it open when option filtering empties the option list.
dropdownTrigger(manual/hidden) still gates the panel as before. - Inline configs are safe.
products={{ fetch, transform }}written inline re-creates the object on every render; the hook forwards through a stable proxy, so only turning the strip on or off reaches the core. Two deliberate consequences: swapping one live config for another leaves the previous integration's cards up until the next request replaces them (toggleproductsoff and back on to clear immediately), and turning the strip on mid-session shows nothing until the next/suggestrequest fires — the product search rides that one scheduler, and enabling the strip doesn't itself change the query.
Selection and links. Cards are real <a href> elements, so cmd/ctrl-click,
middle-click and "copy link address" behave natively. A plain left click is
intercepted (preventDefault) and emits onProductSelect instead.
product.url is used verbatim — the SDK trusts your transform output and
does not sanitise it, so validate the URL there if the platform response isn't
fully under your control. (Angular additionally runs its own [href]
sanitiser, so unrecognised schemes like myapp://… are rewritten to
unsafe:… in that package only.)
Accessibility. The strip is a role="group" labelled "Products"; each card
is a role="option" in the tab order, activated with Enter or Space, with a
visible focus ring. Tabbing into the strip does not close the panel. The row
scrolls horizontally by trackpad, wheel, touch and keyboard; the page never
scrolls sideways and the scrollbar chrome is hidden in all engines.
Tier 2 gets the same thing: pass products / onProductSelect to
useAIAutocomplete() and the strip renders inside <AIAutocompleteDropdown />
via dropdownProps. Tier 3 consumers read products from the hook and call
selectProduct(product) from their own cards.
Custom submit button
<AIAutocomplete
onSubmit={handleSubmit}
submitButton={<button className="my-button">Go</button>}
/>
// Or hide the button entirely:
<AIAutocomplete onSubmit={handleSubmit} submitButton={null} />APIConfig
A discriminated union: APIKeyConfig | AccessTokenConfig.
API Key Mode (default)
{ apiKey: "your_api_key", authScheme: "Bearer", endpoint: "/ac/suggest" }| Field | Type | Description |
|---|---|---|
| type? | "apiKey" | Optional discriminator. Default when omitted. |
| apiKey? | string | API key for Authorization header. |
| authScheme? | "Bearer" \| "Basic" | Auth header scheme. Default: "Bearer". |
| endpoint? | string | Full URL for the suggest endpoint. Default: "https://api.ai-autocomplete.com/api/suggest". |
| appIdentifier? | string | Value for the X-App-Identifier header. |
| headers? | Record<string, string> | Additional headers merged into every request. |
Access Token Mode
<AIAutocomplete
apiConfig={{
type: "accessToken",
getAccessToken: async () => {
const res = await fetch("/api/ac-token");
const { access_token, expires_at } = await res.json();
return { accessToken: access_token, expiresAt: expires_at };
},
}}
onSubmit={handleSubmit}
/>| Field | Type | Description |
|---|---|---|
| type | "accessToken" | Required discriminator. |
| getAccessToken | () => Promise<AccessTokenResult> | Required. Called when the SDK needs a token. |
| accessToken? | string | Initial token. Avoids one round-trip on mount. |
| endpoint? | string | Suggest endpoint URL. Default: "https://api.ai-autocomplete.com/api/suggest". |
| appIdentifier? | string | Value for the X-App-Identifier header. |
| headers? | Record<string, string> | Additional headers merged into every request. |
The SDK handles token refresh transparently: 401 → getAccessToken → retry (once). Concurrent 401s share a single refresh. Tokens refresh proactively 30s before expiresAt.
useAIAutocomplete(options)
The headless hook for Tier 2 and Tier 3. Accepts the same options as <AIAutocomplete /> except for the component-only rendering props: className, ref, pillPlacement, mode, animations, and autoFocus (those belong to the wrapping component — the hook doesn't own the input element). optionsPosition is accepted: it sets the arrow-key direction and flows through dropdownProps to the dropdown. onFocus and onBlur are forwarded and fire whenever the consumer-owned textarea's focus changes (they're driven by inputProps.onFocus / inputProps.onBlur).
Return Value
State
| Field | Type | Description |
|---|---|---|
| completedParams | CompletedParamState[] | Filled parameters. |
| skippedParams | SkippedParamState[] | Suggestions the user dismissed with →. Nothing renders them — pass them to buildSubmitResult for a hand-rolled submit. |
| suggestionPills | Suggestion[] | Unfilled suggestions (pills). First item is the active pill. |
| segments | Segment[] | Input text split into typed text vs completed params — completed segments render as bold <strong> runs inside the editor. |
| newParamId | string \| null | ID of the most recently added param (for shimmer animation). |
| suggestions | Suggestion[] | All suggestions from server (including placeholder type). |
| activeIndex | number | Highlighted option index. -1 = none. |
| isLoading | boolean | True when the dropdown should render its loading skeleton — a fetch is in flight, no option-selection animation is playing, and the user is not in re-edit mode (where cached options stay visible). |
| isReady | boolean | Server indicates query is complete. |
| isDropdownOpen | boolean | Whether the dropdown should be visible. Drive your own dropdown's visibility with this in Tier 3. |
| placeholderText | string | Suggested placeholder text for the current step. |
| error | Error \| null | Last fetch error. |
| products | Product[] | Results of the latest product search. Empty unless products is configured. Already spread into dropdownProps — read it directly only if you render your own strip. |
Actions
| Field | Type | Description |
|---|---|---|
| setActivePill | (index: number) => void | Move pill at index to front (active). |
| skipActivePill | () => void | Skip the active pill — same action as → / the dropdown's skip button. Records it in skippedParams; no-ops during re-edit and the post-selection window. Already wired into dropdownProps.onSkip; call it directly for a custom skip affordance (e.g. with showSkipButton: false). Also exposed on the Tier 1 imperative handle. |
| removeLastParam | () => void | Remove the last completed param from state. The text stays in the input as plain text. |
| clearNewParamId | () => void | Clear shimmer animation state. |
| reset | () => void | Clear all state, re-fetch, and start a new session (rotates session_id). Call this after handling submit. |
| selectProduct | (product: Product) => void | Announce a product selection (fires onProductSelect). The built-in dropdown calls this for you; hand-rolled strips call it themselves. Never navigates. |
Input forwarding (custom inputs) — call these instead of spreading inputProps when your input isn't a <textarea> (contentEditable / rich-text editors):
| Field | Type | Description |
|---|---|---|
| handleTextChange | (text: string) => void | Forward the input's current plain text on every edit. |
| handleKeyDown | (e: KeyboardEvent) => void | Forward a key event so the dropdown can handle Arrow/Enter/Tab/Escape while open. Check event.defaultPrevented to see if it was consumed. |
| setFocused | (focused: boolean) => void | Notify the engine the input gained or lost focus. |
| handleCaretMove | (offset: number) => void | Report the caret position (plain-text offset) so arrow keys can move into the dropdown. |
Spread Props
| Field | Type | Description |
|---|---|---|
| inputProps | object | Spread onto a <textarea>. Includes value, placeholder, onChange, onKeyDown, and ARIA attributes. |
| dropdownProps | AIAutocompleteDropdownProps | Spread onto <AIAutocompleteDropdown />. Carries the options, activeIndex, onSelect / onHighlight, pills, open state, optionsPosition, and the product strip's products / onProductSelect / onProductFocusChange — read these directly to render your own dropdown in Tier 3. |
<AIAutocompleteDropdown />
The dropdown component for Tier 2. Spread dropdownProps from the hook (and add mode when rendering standalone).
| Prop | Type | Description |
|---|---|---|
| suggestions | Suggestion[] | Suggestions to display. |
| activeIndex | number | Highlighted option index. |
| onSelect | (option: SuggestionOption) => void | Called when an option is selected. |
| onHighlight | (index: number) => void | Called on mouse hover. |
| isOpen | boolean | Whether the dropdown is visible. |
| id | string | Listbox ID for ARIA. |
| mode? | "light" \| "dark" \| "auto" | Color mode for a standalone dropdown — self-scopes the SDK tokens so no .magicx-aia wrapper is needed. Leave unset when nested inside a .magicx-aia ancestor (e.g. Tier 1). |
| optionsPosition? | "above" \| "below" | Where the dropdown opens. Provided via dropdownProps; "above" reverses the internal layout. Default: "below". |
| className? | string | CSS class applied to the dropdown. |
| pills? | Suggestion[] | Pills to render inside the dropdown. |
| onPillClick? | (index: number) => void | Called when a pill is clicked. |
| showPills? | boolean | Whether to render pills. Default: true. |
| products? | Product[] | Product cards to render below the options grid. Empty (the default) renders no strip. |
| onProductSelect? | (product: Product) => void | Called when a card is activated. |
| onProductFocusChange? | (focused: boolean) => void | Called when focus enters/leaves the strip. Wired to setFocused by dropdownProps so a tabbed-to card doesn't close the panel. |
| isLoading? | boolean | When true, the dropdown renders its pills + options as a skeleton: text masked, layout (count and widths) preserved, shimmer pulse animating. Falls back to a generic 3-bar placeholder when no pills/options are cached. |
AutocompleteResult
| Field | Type | Description |
|---|---|---|
| query | string | Plain text as the user sees it. |
| raw_query | string | Text with placeholder tokens (e.g. "Create a {{TASK_1}}"). |
| completed_params | CompletedParam[] | Filled parameter values, followed by any the user skipped (see below). |
Skipped parameters
Pressing → at the end of the input dismisses the active pill. The dismissal is reported to the server — and included in completed_params here — as an entry with no placeholder and the sentinel text "skipped":
{ placeholder: "", type: "goal", text: "skipped", kind: null }Tier 2 consumers who build their own submit payload get the raw skips from the hook as skippedParams, and can fold them in the same way with the exported buildSubmitResult(text, completedParams, skippedParams).
Reading
skippedParamsdirectly: the array is append-only untilreset(). The "drop a skip whose type got filled" rule is applied when the payload is built, not by pruning the array — so if the user skipsgoaland later fills one, the raw array still holds thegoalentry. That's deliberate: the filter self-heals if they then delete that param's text, where pruning would discard the signal for good.buildSubmitResultapplies the rule for you; to apply it elsewhere (say, a "you skipped X" badge), use the exportedwithSkippedParams(completedParams, skippedParams).
CSS Customization
Styles are auto-injected at runtime — no CSS import needed. Built-in light and dark defaults apply automatically based on mode.
CSS Variables
Override on the container (via className). All defaults use :where() (zero specificity) — your overrides always win.
| Variable | Light | Dark | Description |
|---|---|---|---|
| --aia-font-family | inherit | inherit | Font used by the library. Defaults to inherit so the library picks up your page's font automatically. Set this to pin a specific font on the library without changing the surrounding page. |
| --aia-pill-bg | #bdbdbd | #bdbdbd | Pill background |
| --aia-pill-color | #000000 | #ffffff | Pill text |
| --aia-pill-font-size | 19px | 19px | Pill font size |
| --aia-option-bg | transparent | transparent | Highlighted option background |
| --aia-option-color | #000000 | #ffffff | Option text |
| --aia-option-color-selected | #000000 | #ffffff | Highlighted option text |
| --aia-option-font-size | 19px | 19px | Option font size |
| --aia-written-text-color | #000000 | #ffffff | Input text |
| --aia-written-text-font-size | 19px | 19px | Input text font size |
| --aia-caret-color | --aia-written-text-color | --aia-written-text-color | Editor caret color. Override independently of input text color. |
| --aia-submit-bg | #000000 | #ffffff | Submit button background. Solid + full-opacity at rest, even when the input is empty/disabled. |
| --aia-submit-color | #ffffff | #000000 | Submit button arrow color |
| --aia-submit-bg-disabled | --aia-submit-bg | --aia-submit-bg | Submit background when disabled (empty input). Defaults to the enabled background so themed colors aren't washed out. Set this for a faded rest state. |
| --aia-submit-color-disabled | --aia-submit-color | --aia-submit-color | Submit arrow color when disabled. Defaults to the enabled color. |
| --aia-border | rgba(17, 24, 39, 0.14) | #505050 | Input container border color |
| --aia-shadow | inset 0 1px 0 rgba(255,255,255,0.9), 0 1px 2px rgba(16,24,40,0.1), 0 8px 24px rgba(16,24,40,0.16) | inset 0 1px 0 rgba(255,255,255,0.06), 0 1px 2px rgba(0,0,0,0.4), 0 8px 24px rgba(0,0,0,0.5) | Input container elevation (box-shadow) |
| --aia-dropdown-offset | 13px | 13px | Gap between the input box and the dropdown (the dropdown's margin toward the input). |
| --aia-footer-gap | 8px | 8px | Breathing room above the dropdown footer (badge / keyboard hints), additive to the dropdown's 8px section gap. |
| --aia-footer-chip-bg | --aia-surface at 65% | --aia-surface at 65% | Fill behind the footer's keyboard hint and AI-Autocomplete badge. The option list scrolls under the footer, so this keeps both legible over the row passing behind them. transparent on the glass surface. |
| --aia-dropdown-bg | — | — | Optional bg color the dropdown's "glass" rim shadow tints toward. Set this to the page background behind the dropdown so the bottom-corner glow blends seamlessly. |
| --aia-scrollbar-thumb | rgba(0, 0, 0, 0.3) | rgba(0, 0, 0, 0.3) | Color of the option list's scrollbar thumb (Firefox + WebKit). |
| --aia-streak-rgb | 99, 102, 241 | 255, 255, 255 | Comma-separated RGB triplet used to tint the option-selection streak animation (consumed via rgba(var(--aia-streak-rgb), …)). |
| --aia-streak-glass-bg | rgba(99, 102, 241, 0.1) | rgba(255, 255, 255, 0.1) | Background fill for the streak's glass-pill effect. |
| --aia-product-card-width | 116px | 116px | Width of a product card in the strip. The media tile is square, so this also sets its height. |
| --aia-product-gap | 8px | 8px | Gap between product cards. |
| --aia-product-bg | transparent | transparent | Product card background. |
| --aia-product-bg-active | --aia-option-bg | --aia-option-bg | Product card background on hover. |
| --aia-product-media-bg | --aia-skeleton-bg | --aia-skeleton-bg | Fill behind the product image, and of the placeholder tile when a product has no image. |
| --aia-product-placeholder-color | --aia-option-color | --aia-option-color | Glyph color of the no-image placeholder tile. |
| --aia-product-title-color | --aia-option-color-selected | --aia-option-color-selected | Product title text. Follows the option colors by default, so theming the panel moves suggestions and products together. |
| --aia-product-price-color | --aia-option-color-selected | --aia-option-color-selected | Product price text. |
| --aia-product-vendor-color | --aia-option-color | --aia-option-color | Product vendor line. |
| --aia-product-focus-ring | --aia-option-color-selected | --aia-option-color-selected | Focus ring drawn on a keyboard-focused card. |
| --aia-products-label-color | --aia-option-color | --aia-option-color | "Products" section label. |
| --aia-product-title-font-size | 12px | 12px | Product title font size. |
| --aia-product-price-font-size | 11px | 11px | Product price font size. |
| --aia-product-vendor-font-size | 10px | 10px | Product vendor font size. |
| --aia-products-label-font-size | 11px | 11px | Section label font size. |
| --aia-skeleton-bg | rgba(189, 189, 189, 0.25) | #1a1b1d | Fill color for the loading skeleton bars and the masked text in cached pills/options. |
Legacy --aia-color-* variables are still supported as fallbacks.
Per-mode Overrides
.my-autocomplete[data-mode="light"] {
--aia-pill-bg: #e2e8f0;
}
.my-autocomplete[data-mode="dark"] {
--aia-pill-bg: #334155;
}Selector Hooks
For styling beyond the CSS variables, target these stable data-aia-* attributes (CSS-module class hashes are not part of the public API):
| Attribute | Element |
|---|---|
| [data-aia-editor] | Editor area wrapping the contentEditable + inline pill list |
| [data-aia-input] | The Tier 1 contentEditable <div> that owns typed text and bold completed params. (Replaces the previous [data-aia-textarea] selector.) |
| [data-aia-pill-list-container] | Inline sibling of the editor that holds unfilled-suggestion pills |
| [data-aia-submit] | Submit button |
| [data-aia-pill] | Each unfilled-suggestion pill |
| [data-aia-pillbar] | Pill bar container inside the dropdown |
| [data-aia-pill-scroll] | Scrollable pill region inside the bar — carries the horizontal scroll and right-edge fade mask |
| [data-aia-skip] | The pill bar's trailing "skip" button. Tune via --aia-skip-font-size / --aia-skip-color / --aia-skip-color-hover / --aia-skip-hover-bg |
| [data-aia-option] | Each suggestion option |
| [data-aia-dropdown] | The dropdown root (listbox). Carries data-aia-has-products while the product strip has cards. |
| [data-aia-products] | Product strip section (label + row) |
| [data-aia-products-row] | The horizontally scrolling row of cards |
| [data-aia-product] | Each product card |
| [data-aia-product-placeholder] | Media tile of a card whose product has no image |
Completed params render as inline <strong> elements inside the editor. Override their weight with [data-aia-input] strong { font-weight: 700; } (the built-in style uses :where() so any consumer selector wins without !important).
/* Solid (non-glass) dropdown */
.my-autocomplete [data-aia-dropdown] {
background: #fff;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
backdrop-filter: none;
}Sessions
Every /api/suggest request carries a meta.session_id UUID. A session runs from mount (or the last reset()) until the next reset(). All requests in one session share the same session_id; calling reset() starts a new one.
The contract is simple: after the user submits the query, call reset(). That clears the input and rotates session_id so the next session begins clean.
Why it matters: the server uses
session_idto track each session's history — what the user has been typing sequentially, which options they've selected, and how the query evolved. That context lets the model produce better, more relevant suggestions on subsequent requests within the same session. Callingreset()at the right moment (when the user actually submits) keeps that history accurate, so your users get higher-quality results.
- Tier 1
<AIAutocomplete />does this automatically — it callsreset()for you afteronSubmitreturns, for both Enter-key and built-in-button submits. - Tier 2 & 3
useAIAutocomplete()— you own the submit flow, so callreset()from youronSubmithandler (see the example above) or from your custom button after firingonSubmit.
Option Overrides
<AIAutocomplete
optionOverrides={{
account: () => [
{ text: "Savings", is_tappable: true, kind: null },
{ text: "Checking", is_tappable: true, kind: null },
],
value: (query) => {
const digits = query.replace(/\D/g, "");
if (!digits) return [{ text: "$100", is_tappable: true, kind: null }];
return [{ text: `$${digits}`, is_tappable: true, kind: null }];
},
}}
onSubmit={handleSubmit}
/>License
Private package. All rights reserved.
