Components
Combobox
Native <input list> + <datalist>. The browser handles dropdown UI, filtering, click + keyboard selection, focus management — zero custom JS. For server-driven options, point hx-target at the <datalist>.
Installation
1. Install via the shadcn CLI
npx shadcn@latest add http://localhost/r/combobox.json2. Use it
import { Combobox } from "@/components/ui/combobox"
// Static — zero JS, browser handles dropdown + filter.
<Combobox id="lang" name="lang" placeholder="Pick a language…"
options={[{ value: "JavaScript" }, { value: "Python" }, { value: "Go" }]} />
// Server-filtered — htmx targets the <datalist>; server returns <option> tags.
<Combobox id="user" name="user" placeholder="Search users…"
hx-get="/api/users/search"
hx-trigger="input changed delay:200ms"
hx-target="#user-list"
hx-swap="innerHTML"
/>Or copy the source manually
/** @jsxImportSource hono/jsx */
import { cn, type ClassValue } from "@/registry/lib/cn"
// Combobox — shadcn-htmx, htmx v4 + Tailwind v4.
//
// **Native-first.** This component is just `<input list>` + `<datalist>`.
// The browser handles:
// - The dropdown UI
// - Filtering as the user types
// - Click + keyboard selection
// - Focus management
// - aria-controls / aria-expanded wiring (implicit)
//
// No custom JS event handlers, no MutationObserver, no race conditions.
// If you need server-driven suggestions, point htmx at the <datalist> and
// have the server return `<option>` tags — the browser uses them
// transparently.
//
// Refs:
// repos/mdn/files/en-us/web/html/reference/elements/datalist/index.md
// repos/mdn/files/en-us/web/html/reference/elements/input/index.md#list
// repos/aria-practices/content/patterns/combobox/
// `disabled` marks an option non-checkable (browsers grey it out, it gets
// no click/focus events).
// repos/mdn/files/en-us/web/html/reference/elements/option/index.md:45
export type ComboboxOption = { value: string; label?: string; disabled?: boolean }
type ComboboxProps = {
// The input's id. The <datalist> gets `${id}-list`; if you wire htmx
// to fetch options dynamically, target that id.
id: string
name?: string
// Initial options. Server-filtered comboboxes can pass [] and let
// htmx populate the datalist on input.
options?: ComboboxOption[]
// `list` is valid on these 13 input types, not just text — a url/email/
// search combobox, or a number/date/time/range/color picker with suggested
// values, are all native datalist use cases.
// repos/mdn/files/en-us/web/html/reference/elements/input/index.md:492
type?:
| "text"
| "search"
| "url"
| "tel"
| "email"
| "number"
| "date"
| "datetime-local"
| "month"
| "week"
| "time"
| "range"
| "color"
placeholder?: string
value?: string
required?: boolean
disabled?: boolean
// The user can type any value that passes validation, even one not in the
// suggestion list, so constrain the free-typed value with these.
// repos/mdn/files/en-us/web/html/reference/elements/input/index.md:505
maxlength?: number
minlength?: number
pattern?: string
// Explains the pattern to AT / on validation failure (spec accessibility note).
title?: string
// Focusable + copy-selectable but not editable. Not supported on range/color.
// repos/mdn/files/en-us/web/html/reference/elements/input/index.md:588
readonly?: boolean
ariaLabel?: string
ariaLabelledby?: string
ariaDescribedby?: string
class?: ClassValue
// htmx attrs ride onto the input element. Typical server-filter setup:
// hx-get="/api/search"
// hx-trigger="input changed delay:200ms"
// hx-target="#<id>-list" (points at the datalist)
// hx-swap="innerHTML"
[key: `hx-${string}`]: any
}
export function Combobox(props: ComboboxProps) {
const {
id,
name,
options = [],
type = "text",
placeholder,
value,
required,
disabled,
maxlength,
minlength,
pattern,
title,
readonly,
ariaLabel,
ariaLabelledby,
ariaDescribedby,
class: className,
...rest
} = props
const listId = `${id}-list`
return (
<span data-slot="combobox" class={cn("inline-block w-full", className)}>
<input
type={type}
id={id}
name={name}
list={listId}
value={value}
placeholder={placeholder}
required={required}
disabled={disabled}
maxlength={maxlength}
minlength={minlength}
pattern={pattern}
title={title}
readonly={readonly}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledby}
aria-describedby={ariaDescribedby}
// autocomplete="off" stops the browser from layering its own
// history-based suggestions on top of the datalist.
autocomplete="off"
class="flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base outline-none transition-[color,box-shadow] placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
{...rest}
/>
<datalist id={listId} data-slot="combobox-list">
{options.map((o) => (
<option value={o.value} label={o.label} disabled={o.disabled} />
))}
</datalist>
</span>
)
}
// Server-rendered single option used by htmx endpoints. Lets the server
// return a typed component instead of raw HTML strings.
export function ComboboxOption(props: ComboboxOption) {
return <option value={props.value} label={props.label} disabled={props.disabled} />
}
// Back-compat shim: the previous API exposed ComboboxNative for the
// static-options use case and Combobox for the custom richer one. We
// collapsed both into a single native Combobox; export the old name as
// an alias so existing import sites don't break.
export const ComboboxNative = Combobox
// Back-compat: ComboboxItem used to be the rich custom variant's option
// renderer. With native datalist, the right primitive is a plain
// <option> — exported via ComboboxOption above. Keeping the alias so
// existing consumers don't break.
export const ComboboxItem = ComboboxOption
1. Save the file
Copy combobox.html into templates/components/.
2. Use it
{% from "components/combobox.html" import combobox %}
{# Static options #}
{{ combobox(id="lang", name="lang", placeholder="Pick a language…",
options=[{"value": "JavaScript"}, {"value": "Python"}, {"value": "Go"}]) }}
{# Server-filter — htmx populates the <datalist> #}
{{ combobox(id="user", name="user", placeholder="Search users…",
hx_get="/api/users/search",
hx_trigger="input changed delay:200ms",
hx_target="#user-list",
hx_swap="innerHTML") }}View source
{# Combobox macro — shadcn-htmx, htmx v4 + Tailwind v4.
Native <input list> + <datalist>. Browser handles dropdown UI, filter,
click + keyboard selection, focus management. No custom JS required.
Usage (static options):
{% from "components/combobox.html" import combobox %}
{{ combobox(id="lang", name="lang",
options=[{"value": "JavaScript"}, {"value": "Python"}]) }}
Usage (htmx-driven server-filter): target the <datalist> by id and
return <option> tags from the server.
<input list="user-list" hx-get="/api/search"
hx-trigger="input changed delay:200ms"
hx-target="#user-list" hx-swap="innerHTML">
<datalist id="user-list"></datalist>
#}
{# `type`: `list` is valid on 13 input types, not just text.
repos/mdn/files/en-us/web/html/reference/elements/input/index.md:492
maxlength/minlength/pattern constrain the free-typed value (datalist
suggestions are not requirements); readonly is not supported on range/color.
repos/mdn/files/en-us/web/html/reference/elements/input/index.md:505,588 #}
{% macro combobox(
id,
name=none,
options=[],
type="text",
placeholder=none,
value=none,
required=false,
disabled=false,
maxlength=none,
minlength=none,
pattern=none,
title=none,
readonly=false,
aria_label=none,
aria_labelledby=none,
aria_describedby=none,
extra_class="",
**attrs
) %}
<span data-slot="combobox" class="inline-block w-full {{ extra_class }}">
<input
type="{{ type }}"
id="{{ id }}"
{%- if name %} name="{{ name }}"{% endif %}
list="{{ id }}-list"
{%- if value is not none %} value="{{ value }}"{% endif %}
{%- if placeholder %} placeholder="{{ placeholder }}"{% endif %}
{%- if required %} required{% endif %}
{%- if disabled %} disabled{% endif %}
{%- if maxlength is not none %} maxlength="{{ maxlength }}"{% endif %}
{%- if minlength is not none %} minlength="{{ minlength }}"{% endif %}
{%- if pattern %} pattern="{{ pattern }}"{% endif %}
{%- if title %} title="{{ title }}"{% endif %}
{%- if readonly %} readonly{% endif %}
{%- if aria_label %} aria-label="{{ aria_label }}"{% endif %}
{%- if aria_labelledby %} aria-labelledby="{{ aria_labelledby }}"{% endif %}
{%- if aria_describedby %} aria-describedby="{{ aria_describedby }}"{% endif %}
autocomplete="off"
class="flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base outline-none transition-[color,box-shadow] placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
{%- for k, v in attrs.items() %} {{ k|replace('_', '-') }}="{{ v }}"{% endfor -%}
>
<datalist id="{{ id }}-list" data-slot="combobox-list">
{% for opt in options %}<option value="{{ opt.value }}"{% if opt.label %} label="{{ opt.label }}"{% endif %}{% if opt.disabled %} disabled{% endif %}>{% endfor %}
</datalist>
</span>
{% endmacro %}
{# A single <option> — useful when an htmx endpoint returns one.
`disabled` marks it non-checkable.
repos/mdn/files/en-us/web/html/reference/elements/option/index.md:45 #}
{% macro combobox_option(value, label=none, disabled=false) -%}
<option value="{{ value }}"{% if label %} label="{{ label }}"{% endif %}{% if disabled %} disabled{% endif %}>
{%- endmacro %}
1. Save the file
Add combobox.tmpl alongside button.tmpl.
2. Use it
{{/* Static options */}}
{{template "combobox" (dict "ID" "lang" "Name" "lang" "Options" $opts)}}
{{/* Server-filter — htmx populates the <datalist> */}}
{{template "combobox" (dict "ID" "user" "Name" "user"
"HxGet" "/api/users/search" "HxTrigger" "input changed delay:200ms"
"HxTarget" "#user-list" "HxSwap" "innerHTML")}}View source
{{/*
Combobox templates — shadcn-htmx, htmx v4 + Tailwind v4.
Native <input list> + <datalist>. Browser handles everything; no JS.
type ComboboxArgs struct {
ID, Name, Placeholder, Value, AriaLabel string
// Type: `list` is valid on 13 input types, not just text (default).
// repos/mdn/files/en-us/web/html/reference/elements/input/index.md:492
Type string
Options []ComboboxOption
Required, Disabled, Readonly bool
// Constrain the free-typed value (datalist suggestions are not
// requirements). Pattern explained to AT via Title.
// repos/mdn/files/en-us/web/html/reference/elements/input/index.md:505
MaxLength, MinLength int
Pattern, Title string
AriaLabelledby, AriaDescribedby string
// For server-filter: pass HxGet/HxTrigger/HxTarget/HxSwap and
// an empty Options slice; the datalist will be populated by
// htmx response.
HxGet, HxTrigger, HxTarget, HxSwap string
}
// Disabled marks an option non-checkable.
// repos/mdn/files/en-us/web/html/reference/elements/option/index.md:45
type ComboboxOption struct{ Value, Label string; Disabled bool }
*/}}
{{define "combobox"}}
<span data-slot="combobox" class="inline-block w-full">
<input type="{{if .Type}}{{.Type}}{{else}}text{{end}}" id="{{.ID}}" {{if .Name}}name="{{.Name}}"{{end}} list="{{.ID}}-list"
{{if .Value}}value="{{.Value}}"{{end}}
{{if .Placeholder}}placeholder="{{.Placeholder}}"{{end}}
{{if .Required}}required{{end}} {{if .Disabled}}disabled{{end}}
{{if .Readonly}}readonly{{end}}
{{if .MaxLength}}maxlength="{{.MaxLength}}"{{end}}
{{if .MinLength}}minlength="{{.MinLength}}"{{end}}
{{if .Pattern}}pattern="{{.Pattern}}"{{end}}
{{if .Title}}title="{{.Title}}"{{end}}
{{if .AriaLabel}}aria-label="{{.AriaLabel}}"{{end}}
{{if .AriaLabelledby}}aria-labelledby="{{.AriaLabelledby}}"{{end}}
{{if .AriaDescribedby}}aria-describedby="{{.AriaDescribedby}}"{{end}}
{{if .HxGet}}hx-get="{{.HxGet}}"{{end}}
{{if .HxTrigger}}hx-trigger="{{.HxTrigger}}"{{end}}
{{if .HxTarget}}hx-target="{{.HxTarget}}"{{end}}
{{if .HxSwap}}hx-swap="{{.HxSwap}}"{{end}}
autocomplete="off"
class="flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base outline-none transition-[color,box-shadow] placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm">
<datalist id="{{.ID}}-list" data-slot="combobox-list">
{{range .Options}}<option value="{{.Value}}"{{if .Label}} label="{{.Label}}"{{end}}{{if .Disabled}} disabled{{end}}>{{end}}
</datalist>
</span>
{{end}}
{{define "combobox_option"}}
<option value="{{.Value}}"{{if .Label}} label="{{.Label}}"{{end}}{{if .Disabled}} disabled{{end}}>
{{end}}
1. Save the file
Drop combobox.ex into lib/my_app_web/components/.
2. Use it
<%# Static options %>
<.combobox id="lang" name="lang" placeholder="Pick a language…"
options={[%{value: "JavaScript"}, %{value: "Python"}, %{value: "Go"}]} />
<%# Server-filter — htmx populates the <datalist> %>
<.combobox id="user" name="user" placeholder="Search users…"
hx-get={~p"/api/users/search"}
hx-trigger="input changed delay:200ms"
hx-target="#user-list"
hx-swap="innerHTML" />View source
defmodule ShadcnHtmx.Components.Combobox do
@moduledoc """
Combobox — shadcn-htmx, htmx v4 + Tailwind v4 for Phoenix.
Native `<input list>` + `<datalist>`. The browser handles the dropdown
UI, filter, click + keyboard selection, focus management. No custom JS.
## Examples
# Static options
<.combobox id="lang" name="lang" placeholder="Pick a language…"
options={[%{value: "JavaScript"}, %{value: "Python"}, %{value: "Go"}]} />
# Server-filter via htmx — target the datalist by id.
<.combobox id="user" name="user" placeholder="Search users…"
hx-get={~p"/api/users/search"}
hx-trigger="input changed delay:200ms"
hx-target="#user-list"
hx-swap="innerHTML" />
# Endpoint returns: <.combobox_option value="ada" />
"""
use Phoenix.Component
attr :id, :string, required: true
attr :name, :string, default: nil
# `list` is valid on 13 input types, not just text.
# repos/mdn/files/en-us/web/html/reference/elements/input/index.md:492
attr :type, :string, default: "text"
attr :placeholder, :string, default: nil
attr :value, :string, default: nil
attr :options, :list, default: []
attr :required, :boolean, default: false
attr :disabled, :boolean, default: false
# Focusable + selectable but not editable. Not supported on range/color.
# repos/mdn/files/en-us/web/html/reference/elements/input/index.md:588
attr :readonly, :boolean, default: false
# Constrain the free-typed value — datalist suggestions are not requirements.
# repos/mdn/files/en-us/web/html/reference/elements/input/index.md:505
attr :maxlength, :integer, default: nil
attr :minlength, :integer, default: nil
attr :pattern, :string, default: nil
attr :title, :string, default: nil
attr :"aria-label", :string, default: nil
attr :"aria-labelledby", :string, default: nil
attr :"aria-describedby", :string, default: nil
attr :class, :string, default: nil
attr :rest, :global,
include: ~w(hx-get hx-post hx-trigger hx-target hx-swap hx-vals hx-headers)
def combobox(assigns) do
~H"""
<span data-slot="combobox" class={["inline-block w-full", @class]}>
<input
type={@type}
id={@id}
name={@name}
list={"#{@id}-list"}
value={@value}
placeholder={@placeholder}
required={@required}
disabled={@disabled}
readonly={@readonly}
maxlength={@maxlength}
minlength={@minlength}
pattern={@pattern}
title={@title}
aria-label={assigns[:"aria-label"]}
aria-labelledby={assigns[:"aria-labelledby"]}
aria-describedby={assigns[:"aria-describedby"]}
autocomplete="off"
class="flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base outline-none transition-[color,box-shadow] placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
{@rest}
/>
<datalist id={"#{@id}-list"} data-slot="combobox-list">
<option
:for={opt <- @options}
value={opt[:value]}
label={opt[:label]}
disabled={opt[:disabled]}
/>
</datalist>
</span>
"""
end
attr :value, :string, required: true
attr :label, :string, default: nil
# Marks the option non-checkable (browsers grey it out).
# repos/mdn/files/en-us/web/html/reference/elements/option/index.md:45
attr :disabled, :boolean, default: false
def combobox_option(assigns) do
~H"""
<option value={@value} label={@label} disabled={@disabled} />
"""
end
end
1. Save the file
Static variant: zero JS. Server-filter variant: htmx populates the <datalist>.
2. Use it
<!-- Static options -->
<input type="text" id="lang" list="lang-list" autocomplete="off" class="…">
<datalist id="lang-list">
<option value="JavaScript">
<option value="Python">
</datalist>
<!-- Server-filter via htmx -->
<input type="text" id="user" list="user-list" autocomplete="off"
hx-get="/api/users/search"
hx-trigger="input changed delay:200ms"
hx-target="#user-list"
hx-swap="innerHTML" class="…">
<datalist id="user-list"></datalist>View source
<!--
shadcn-htmx — raw HTML combobox snippet.
Native <input list> + <datalist>. The browser handles dropdown UI,
filter, click + keyboard selection, focus management. Zero JS.
Two patterns:
1. Static options — fill the <datalist> at render time.
2. Server-filter — leave the <datalist> empty and let htmx populate
it on input. Server returns <option> tags.
Native attrs you can add to the <input> (all optional):
- type: `list` is valid on 13 types (text/search/url/tel/email/number/
date/datetime-local/month/week/time/range/color), so swap type="text"
for e.g. type="url" to get a URL combobox.
repos/mdn/files/en-us/web/html/reference/elements/input/index.md:492
- maxlength / minlength / pattern / title: constrain the free-typed value
— datalist suggestions are not requirements, the user can type anything
that passes validation.
repos/mdn/files/en-us/web/html/reference/elements/input/index.md:505
- readonly: focusable + copy-selectable but not editable (not on range/color).
- aria-describedby: id of a hint / error element announced after the name.
An <option> can carry `disabled` to render a non-checkable suggestion.
repos/mdn/files/en-us/web/html/reference/elements/option/index.md:45
-->
<!-- 1. Static options -->
<span data-slot="combobox" class="inline-block w-full">
<input type="text" id="lang" name="lang" list="lang-list"
placeholder="Pick a language…" autocomplete="off"
class="flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base outline-none transition-[color,box-shadow] placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm">
<datalist id="lang-list" data-slot="combobox-list">
<option value="JavaScript">
<option value="TypeScript">
<option value="Python">
<option value="Go">
<option value="Rust">
<!-- A non-checkable suggestion (greyed out, no click/focus). -->
<option value="COBOL" disabled>
</datalist>
</span>
<!-- 2. Server-filter via htmx -->
<span data-slot="combobox" class="inline-block w-full">
<input type="text" id="user" name="user" list="user-list"
placeholder="Search users…" autocomplete="off"
hx-get="/api/users/search"
hx-trigger="input changed delay:200ms"
hx-target="#user-list"
hx-swap="innerHTML"
class="flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base outline-none transition-[color,box-shadow] placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm">
<datalist id="user-list" data-slot="combobox-list">
<!-- Server returns: -->
<!-- <option value="Ada Lovelace"> -->
<!-- <option value="Grace Hopper"> -->
</datalist>
</span>
Examples
Native — <input list> + <datalist>
Browser handles the dropdown + filter. Best for static, known lists where you don't need custom item rendering.
Native <datalist> is the simplest combobox: zero JS, full keyboard contract comes from the platform, and AT support is solid. The trade-off: option rendering is the browser's chrome, not your CSS. If you need rich items (avatar + name + label), use the htmx variant below.
<Combobox id="lang" name="lang" placeholder="Pick a language…"
options={[{ value: "JavaScript" }, { value: "Python" }, { value: "Go" }]} />{{ combobox(id="lang", placeholder="Pick a language…",
options=[{"value":"JavaScript"},{"value":"Python"},{"value":"Go"}]) }}{{template "combobox" (dict "ID" "lang" "Placeholder" "Pick a language…" "Options" $opts)}}<.combobox id="lang" placeholder="Pick a language…"
options={[%{value: "JavaScript"}, %{value: "Python"}, %{value: "Go"}]} /><div class="grid w-full max-w-md gap-2">
<label for="ex-combo-lang" class="flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50" data-slot="label">Favourite language</label>
<span data-slot="combobox" class="inline-block w-full">
<input type="text" id="ex-combo-lang" name="lang" list="ex-combo-lang-list" placeholder="Type to filter…" autocomplete="off" class="flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base outline-none transition-[color,box-shadow] placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"/>
<datalist id="ex-combo-lang-list" data-slot="combobox-list">
<option value="Bash">
</option>
<option value="C">
</option>
<option value="C++">
</option>
<option value="C#">
</option>
<option value="Clojure">
</option>
<option value="CoffeeScript">
</option>
<option value="Crystal">
</option>
<option value="Dart">
</option>
<option value="Elixir">
</option>
<option value="Elm">
</option>
<option value="Erlang">
</option>
<option value="F#">
</option>
</datalist>
</span>
</div>Further reading
Server — htmx filter into the datalist
Each keystroke (debounced 200ms) fetches /search?lang=…; the server returns <option> tags swapped into the <datalist>.
Same native primitive — the htmx-driven version just points hx-target at the <datalist> instead of populating it server-side at render time. The server returns <option value="…"> tags. Browser handles the dropdown, filter, click, and keyboard selection. **No custom JS, no race conditions.**
<Combobox id="user" name="user"
placeholder="Search users…"
hx-get="/api/users/search"
hx-trigger="input changed delay:200ms"
hx-target="#user-list"
hx-swap="innerHTML"
/>
{/* Server returns: <option value="Ada Lovelace"> ... */}{{ combobox(id="user", placeholder="Search users…",
hx_get="/api/search",
hx_trigger="input changed delay:200ms",
hx_target="#user-list", hx_swap="innerHTML") }}{{template "combobox" (dict "ID" "user" "Placeholder" "Search users…"
"HxGet" "/api/search" "HxTrigger" "input changed delay:200ms"
"HxTarget" "#user-list" "HxSwap" "innerHTML")}}<.combobox id="user" placeholder="Search users…"
hx-get={~p"/api/search"} hx-trigger="input changed delay:200ms"
hx-target="#user-list" hx-swap="innerHTML" /><div class="grid w-full max-w-md gap-2">
<label for="ex-combo-server" class="flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50" data-slot="label">Language</label>
<span data-slot="combobox" class="inline-block w-full">
<input type="text" id="ex-combo-server" name="lang" list="ex-combo-server-list" placeholder="Start typing… (try "ja")" autocomplete="off" class="flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base outline-none transition-[color,box-shadow] placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm" hx-get="/combobox/search" hx-trigger="input changed delay:200ms" hx-target="#ex-combo-server-list" hx-swap="innerHTML"/>
<datalist id="ex-combo-server-list" data-slot="combobox-list">
</datalist>
</span>
</div>Further reading
API Reference
<Combobox> — native <input list> + <datalist>
Props you pass to the JSX component. Anything matching hx-* is forwarded onto the underlying <input>. The browser handles dropdown UI, filter, click + keyboard selection, focus management. No custom JS.
| Prop | Type | Default | Description |
|---|---|---|---|
type | "text"|"search"|"url"|"tel"|"email"|"number"|"date"|"datetime-local"|"month"|"week"|"time"|"range"|"color" | "text" | Native input type. list is valid on these 13 types, so you can build a url/email/search combobox or a number/date/time/range/color picker with suggested values.MDN<input list> valid types |
minlength / maxlength | number | — | Character bounds for the free-typed value (text-like types). |
pattern | string | — | Regex the typed value must match — datalist suggestions are not requirements, so the free-typed value still needs validation.MDNpattern |
title | string | — | Explains the pattern constraint to assistive tech and on validation failure. |
readonly | boolean | false | Focusable + copy-selectable but not editable. Not supported on range/color types. |
disabled (on options) | boolean | false | Per-option flag on ComboboxOption — marks a suggestion non-checkable (greyed out, no click/focus).MDN<option disabled> |
id* | string | — | Pairs the input with the <datalist> via list="{id}-list". |
options* | Array<{ value: string; label?: string }> | — | Choices the browser will render in the native dropdown.MDN<datalist> |
name | string | — | Form field name. |
placeholder | string | — | Placeholder text shown when empty. |
value | string | — | Initial value. |
required | boolean | false | Native HTML required. |
disabled | boolean | false | Disable the input. |
ariaLabel | string | — | Accessible name when no visible <label>. |
ariaLabelledby | string | — | Id of a visible label. |
class | string | — | Tailwind classes appended to the wrapper. |
* required