@magicx-eng/ai-autocomplete-react
v0.1.43
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
- Two tiers of integration — use the full
<AIAutocomplete />component or go headless withuseAIAutocomplete()+<AIAutocompleteDropdown /> - 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
- 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-domTwo 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: Headless | useAIAutocomplete() — state, actions, spread props | The input and layout JSX | You need a custom input or want full control over rendering |
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: Headless
Use the hook and dropdown separately for full control over the input and rendering:
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: { endpoint: "https://api.example.com/ac/suggest", apiKey: "your_api_key" },
});
return (
<div>
<textarea {...inputProps} />
<AIAutocompleteDropdown {...dropdownProps} />
</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.
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). |
| 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" | "inline" | 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. |
| 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). |
| 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(). |
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. Accepts the same props as <AIAutocomplete /> except for the rendering-only ones: className, ref, pillPlacement, mode, optionsPosition, animations, and autoFocus (those belong to the wrapping component — the hook doesn't own the input element). 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. |
| 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. |
| error | Error \| null | Last fetch error. |
Actions
| Field | Type | Description |
|---|---|---|
| setActivePill | (index: number) => void | Move pill at index to front (active). |
| 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. |
Spread Props
| Field | Type | Description |
|---|---|---|
| inputProps | object | Spread onto a <textarea>. Includes value, placeholder, onChange, onKeyDown, and ARIA attributes. |
| dropdownProps | AIAutocompleteDropdownProps | Spread onto <AIAutocompleteDropdown />. Includes options, highlight, selection, pills, and open state. |
<AIAutocompleteDropdown />
The dropdown component for Tier 2. Spread dropdownProps from the hook.
| 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. |
| 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. |
| 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. |
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 |
| --aia-submit-color | #ffffff | #000000 | Submit button icon color |
| --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-skeleton-bg | rgba(189, 189, 189, 0.51) | #333539 | 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-option] | Each suggestion option |
| [data-aia-dropdown] | The dropdown root (listbox) |
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
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.
