"use client"
import * as React from "react"
import { Autocomplete } from "@/components/ui/autocomplete"
const frameworks = [
"Next.js",
"Vite",
"Astro",
"Remix",
"TanStack Start",
"SvelteKit",
"Nuxt",
"SolidStart",
]
export default function AutocompleteDemo() {
const [value, setValue] = React.useState("")
return (
<div className="w-full max-w-xs">
<Autocomplete
options={frameworks}
value={value}
onValueChange={setValue}
placeholder="Search framework..."
aria-label="Framework"
/>
</div>
)
}Installation
npx logic2b@latest add autocompleteWorking with an AI assistant? Copy this prompt into Claude Code, Cursor or Copilot — it carries the install steps, a no-CLI fallback and a verification checklist.
Add the "autocomplete" item from the logic2b ui registry (https://ui.logic2b.com) to this project. Follow every step and verify at the end. ## Steps 1. If the project has no `components.json` yet, set up logic2b ui first: ```bash npx logic2b@latest init ``` 2. Install the item (the CLI resolves registry dependencies automatically): ```bash npx logic2b@latest add autocomplete ``` 3. Install any npm dependencies the command prints. ## If the CLI is not available Do it manually from the registry payload: 1. Fetch https://ui.logic2b.com/r/autocomplete.json. 2. Write every entry in `files[]` to its mapped project path (see below). 3. Repeat (recursively) for each name in `registryDependencies`. 4. Install every package listed in `dependencies` across those payloads. ## Registry reference - Index of every item: https://ui.logic2b.com/r/index.json - Item payload (full source): https://ui.logic2b.com/r/<name>.json - Usage examples per item: https://ui.logic2b.com/r/demos/index.json → https://ui.logic2b.com/r/demos/<demo>.json - Docs index for agents: https://ui.logic2b.com/llms.txt (full docs: https://ui.logic2b.com/llms-full.txt) - Every docs page is Markdown when you append `.md` to its URL. Registry file paths map to project paths like this: - `ui/*` → `@/components/ui/*` - `blocks/<name>/*` → `@/components/*` - `charts/*` → `@/components/charts/*` - `hooks/*` → `@/hooks/*` - `lib/utils.ts` → `@/lib/utils.ts` - `theme.css` → next to the project's Tailwind entry stylesheet ## Conventions (do not break these) - Tailwind CSS v4 (CSS-first config; no tailwind.config.js needed) and React 19. - All colors go through semantic tokens (`bg-primary`, `text-muted-foreground`, `border-border`, …). Never hardcode hex/oklch values in components. - Dark mode is class-based: toggle `.dark` on `<html>`. - Icons come from lucide-react. ## Verify before finishing 1. The dev server starts and the app renders without console errors. 2. TypeScript compiles (`tsc --noEmit` or the framework's check). 3. Components use the theme: a `<Button>` shows the primary token color, and toggling `.dark` on `<html>` switches the palette.
Copy into ui/autocomplete.tsx:
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
export interface AutocompleteProps
extends Omit<
React.ComponentProps<"input">,
"value" | "defaultValue" | "onChange"
> {
/** The suggestion list, filtered against what the user types. */
options: string[]
value?: string
defaultValue?: string
onValueChange?: (value: string) => void
/** Shown when nothing matches. Default `"No results."`. */
emptyMessage?: string
}
/**
* A free-text input with a filtered suggestion list — the editable ARIA
* combobox pattern. Unlike a select-style combobox it accepts arbitrary text;
* the list just offers matches. Fully keyboard-driven (↑/↓ to move, Enter to
* pick, Esc to close) and dependency-free.
*/
function Autocomplete({
className,
options,
value,
defaultValue,
onValueChange,
emptyMessage = "No results.",
onFocus,
onBlur,
onKeyDown,
...props
}: AutocompleteProps) {
const isControlled = value !== undefined
const [internal, setInternal] = React.useState(defaultValue ?? "")
const query = isControlled ? (value as string) : internal
const [open, setOpen] = React.useState(false)
const [active, setActive] = React.useState(-1)
const reactId = React.useId()
const listId = `${reactId}-list`
const filtered = React.useMemo(
() =>
options.filter((o) => o.toLowerCase().includes(query.toLowerCase())),
[options, query]
)
const setQuery = (v: string) => {
if (!isControlled) setInternal(v)
onValueChange?.(v)
}
const choose = (v: string) => {
setQuery(v)
setOpen(false)
setActive(-1)
}
return (
<div data-slot="autocomplete" className={cn("relative", className)}>
<input
role="combobox"
aria-expanded={open}
aria-controls={listId}
aria-autocomplete="list"
aria-activedescendant={
open && active >= 0 ? `${reactId}-opt-${active}` : undefined
}
data-slot="autocomplete-input"
value={query}
onChange={(e) => {
setQuery(e.target.value)
setOpen(true)
setActive(-1)
}}
onFocus={(e) => {
setOpen(true)
onFocus?.(e)
}}
onBlur={(e) => {
setOpen(false)
onBlur?.(e)
}}
onKeyDown={(e) => {
onKeyDown?.(e)
if (e.key === "ArrowDown") {
e.preventDefault()
setOpen(true)
setActive((i) => Math.min(i + 1, filtered.length - 1))
} else if (e.key === "ArrowUp") {
e.preventDefault()
setActive((i) => Math.max(i - 1, 0))
} else if (e.key === "Enter" && open && active >= 0) {
e.preventDefault()
choose(filtered[active])
} else if (e.key === "Escape") {
setOpen(false)
setActive(-1)
}
}}
className={cn(
"border-input placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
)}
{...props}
/>
{open && (
<ul
role="listbox"
id={listId}
data-slot="autocomplete-list"
className="bg-popover text-popover-foreground absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md border p-1 shadow-md"
>
{filtered.length === 0 ? (
<li className="text-muted-foreground px-2 py-1.5 text-sm">
{emptyMessage}
</li>
) : (
filtered.map((opt, i) => (
<li
key={opt}
id={`${reactId}-opt-${i}`}
role="option"
aria-selected={i === active}
data-slot="autocomplete-option"
onMouseDown={(e) => {
// Keep focus on the input so onBlur doesn't close before the pick.
e.preventDefault()
choose(opt)
}}
onMouseEnter={() => setActive(i)}
className={cn(
"cursor-pointer rounded-sm px-2 py-1.5 text-sm outline-none",
i === active && "bg-accent text-accent-foreground"
)}
>
{opt}
</li>
))
)}
</ul>
)}
</div>
)
}
export { Autocomplete }Usage
import { Autocomplete } from "@/components/ui/autocomplete"
const [value, setValue] = React.useState("")
<Autocomplete
options={["Next.js", "Vite", "Astro"]}
value={value}
onValueChange={setValue}
placeholder="Search framework..."
/>
The list filters against what the user types (case-insensitive includes), but
the input accepts arbitrary text — that’s what separates it from the
select-style Combobox. Reach for Autocomplete
when the suggestions are hints (a search box, a tag entry); reach for Combobox
when the value must come from a fixed set.
Keyboard
- ↓ / ↑ move the active suggestion.
- Enter picks the active suggestion.
- Esc closes the list, keeping what you typed.
API
| Prop | Type | Default |
|---|---|---|
options |
string[] |
— (required) |
value |
string |
— (controlled) |
defaultValue |
string |
"" |
onValueChange |
(value: string) => void |
— |
emptyMessage |
string |
"No results." |
| …props | React.ComponentProps<"input"> |
— |