Combobox
Basic Usage
The Combobox field renders a selection control for known values. Add options() for finite choices. A single non-searchable choice uses native select markup by default. Use the Tokens and Records sections for free-form string tokens or record-backed selections.
use InertiaUI\Forms\Fields\Combobox;
Combobox::make('region')
->options([
'north' => 'North region',
'central' => 'Central region',
'remote' => 'Remote team',
])
->default('central')
->placeholder('Choose a region');Search, multiple selection, custom values, tokens, records, rich rows, and selected media use the non-native combobox control automatically. There is no public native() method. Short visible lists are often better as Radio::buttons() or CheckboxGroup::buttons(), especially when all available choices should stay visible.
Option Value Types
Finite choices may use string, numeric, or boolean values. Keyed option arrays are the shortest form for string and numeric values. Boolean values should use explicit value / label rows so the submitted value stays a boolean.
Native <select> elements expose changed values as strings in the DOM, so Combobox resolves the selected DOM value to the normalized option before submitting. The original PHP option value type is preserved in form data:
Combobox::make('priority')
->options([
1 => 'Low',
2 => 'Normal',
3 => 'High',
])
->default(2);
Combobox::make('notifications_enabled')
->options([
['value' => true, 'label' => 'Enabled'],
['value' => false, 'label' => 'Disabled'],
])
->default(false);Non-native combobox controls select the normalized option value directly.
Grouped Native Choices
Grouped option arrays keep related values together and still stay in the finite choice workflow.
Combobox::make('workflow_status')
->label('Status')
->options([
'Workflow' => [
'draft' => 'Draft',
'review' => 'In review',
],
'Release' => [
'approved' => 'Approved',
'published' => 'Published',
],
])
->groupBy()
->default('review');Model And Query Paging
Model and query-backed options() use Inertia Infinite Scroll automatically once the form serializes the field, provided the source is a model class, Eloquent builder, query builder, or Closure returning one. There is no row-count threshold.
Combobox::make('assignee_id')
->options(User::class, label: 'name', value: 'id');Resolve the builder to a collection when you want to use Eloquent but keep the field as a finite option list:
Combobox::make('assignee_id')
->options(User::query()->orderBy('name')->get(), label: 'name', value: 'id');A bare model class orders by the label column automatically when the label is a simple column name. Passing an Eloquent or query builder preserves the builder's order, so add orderBy(...) yourself. Dotted relation paths such as team.name are eager loaded automatically for Eloquent sources.
Each Infinite Scroll request loads 25 options by default. Use perPage() to change that page size:
Combobox::make('assignee_id')
->options(User::class, 'name', 'id')
->perPage(10);Searchable Choices
Add searchable() for local option search. Search is case and accent insensitive. It matches the option label, description, disabled reason, selected suffix, and submitted value.
Combobox::make('country')
->options([
'nl' => 'Netherlands',
'be' => 'Belgium',
'de' => 'Germany',
'fr' => 'France',
])
->searchable()
->clearable()
->default('nl')
->placeholder('Search countries');autofocus() focuses choice-mode controls on page load. Simple finite choices focus the native select. Searchable, multiple, and choices that allow custom values focus the custom input. Tokens and records do not autofocus through Combobox:
Combobox::make('country')
->options(Country::pluck('name', 'code'))
->autofocus();Multiple Choices
Use multiple() when the saved value should be an array of selected option values. This remains choice mode. It is not tokens and it is not records.
Combobox::make('reviewers')
->options([
'copy' => 'Copy review',
'accessibility' => 'Accessibility review',
'release' => 'Release review',
])
->multiple()
->maxSelected(3)
->placeholder('Search reviewers')
->default(['copy', 'accessibility']);Custom Values
Use allowCustomValues() when a choice list should accept a one-off value in addition to existing options. A local value created by typing into a custom-value choice is submitted as the typed string.
Combobox::make('topics')
->options([
'planning' => 'Planning',
'research' => 'Research',
'review' => 'Review',
'launch' => 'Launch',
])
->multiple()
->allowCustomValues()
->clearable()
->placeholder('Add a topic')
->customValueText('Add "%s" as a topic')
->default(['planning']);Choice-mode list arrays use integer array keys as submitted values. Use domain keys or explicit value / label rows when saved values matter.
Tokens
Use tokens() for typed string tokens where the submitted value is an array of strings and each value is shown as a tag. This is the better fit for lists with token constraints, delimiters, or token-specific validation. Suggestions come from options(), and custom tokens are allowed by default.
Combobox::make('topics')
->tokens()
->options(['Planning', 'Research', 'Review', 'Launch'])
->default(['Research'])
->placeholder('Type a topic');Database-backed suggestions use options() too. For tokens, use the same column for label and value so selecting a suggestion submits the displayed token text:
Combobox::make('topics')
->tokens()
->options(Topic::query()->orderBy('name'), label: 'name', value: 'name')
->placeholder('Type a topic');Bound or default strings do not need to appear in options(). Existing saved tokens remain visible and submit as strings while custom tokens are allowed:
Combobox::make('topics')
->tokens()
->options(['Planning', 'Research'])
->default(['Research', 'Quarterly review']);Suggestions Only
Restrict tokens to the suggestion list with allowCustomValues(false). This adds array validation on the field and Rule::in(...) validation for each submitted token.
Combobox::make('departments')
->tokens()
->options(['Engineering', 'Design', 'Marketing', 'Sales'])
->allowCustomValues(false);Limits And Rejection Rules
minSelected() and maxSelected() validate selected token count. maxLength() and pattern() reject invalid entries before they are added to the selected token list.
Combobox::make('slugs')
->tokens()
->label('Slugs')
->placeholder('Try qa-review')
->options(['release-plan', 'qa-review', 'launch-notes'])
->maxSelected(3)
->maxLength(16)
->pattern('^[a-z0-9-]+$')
->clearable()
->default(['release-plan']);Delimiters, Duplicates, And Blur
Enter and the configured delimiter commit the current text as a token. The default delimiter is a comma. Duplicate detection is case-insensitive by default:
Combobox::make('emails')
->tokens()
->delimiter(';')
->allowDuplicates()
->caseSensitive()
->createOnBlur(false);createOnBlur() defaults to true, so pending input becomes a token as focus leaves the field. Pressing Tab commits the pending token and advances focus. The frontend announces rejected tokens when duplicates, length, pattern, or suggestion rules prevent a token from being added. Suggested tokens use the same listbox keyboard navigation and no-results state as choice Combobox fields.
Add tokens() to fields that use token-only helpers such as delimiter(), allowDuplicates(), caseSensitive(), pattern(), maxLength(), and createOnBlur(). allowCustomValues(), minSelected(), and maxSelected() are shared by multiple choices, tokens, and records.
Reorderable, Clearable, And Styled Tokens
Combobox::make('steps')
->tokens()
->label('Workflow steps')
->placeholder('Add a step')
->options(['Draft', 'Reviewed', 'Ready', 'Final review', 'Published'])
->reorderable()
->clearable()
->default(['Draft', 'Reviewed']);reorderable() changes the order of selected tokens and preserves that order in the submitted name[] values. clearable() shows a clear-all control for selected tokens. Use tagClass() for tag badge styling:
Combobox::make('priorities')
->tokens()
->tagClass('bg-blue-50 text-blue-700 ring-1 ring-blue-200');Advanced Mapping And Rich Rows
options() accepts keyed arrays, arrays of records, Collections, model classes, Eloquent builders, query builders, closures, and Arrayable values. Keyed arrays map keys to submitted values and values to labels:
Combobox::make('country')
->options(['us' => 'United States', 'ca' => 'Canada']);Record arrays use value and label by default. Pass a label key, value key, description key, or callbacks for richer records:
Combobox::make('plan_id')
->options(
Plan::query()->active()->orderBy('name'),
label: 'name',
value: 'id',
description: fn (Plan $plan) => $plan->summary,
);Flat record arrays may be grouped by a field key:
Combobox::make('assignee_id')
->options([
['id' => 1, 'name' => 'Avery Stone', 'role' => 'Admins'],
['id' => 2, 'name' => 'Morgan Vale', 'role' => 'Editors'],
['id' => 3, 'name' => 'Jordan Example', 'role' => 'Admins'],
], label: 'name', value: 'id')
->groupBy('role');Use Option::make() when building rich static options in PHP:
use InertiaUI\Forms\Fields\Support\Option;
use InertiaUI\Forms\Fields\Support\OptionImage;
Combobox::make('workspace_tool')
->options([
Option::make('mara-finch', 'Mara Finch')
->description('Shared tokens and review notes.')
->icon('PaletteIcon')
->avatar(OptionImage::make('/avatars/mara-finch.jpeg')->alt('Mara Finch')->small())
->badge('Pinned')
->url('/team/mara-finch')
->metadata(['stack' => 'design'])
->selectedSuffix('Current'),
]);The frontend renders leading media in this order: image, then avatar, then icon. image and avatar accept a plain URL or an OptionImage payload. JSON endpoints may send these payload keys:
| Key | Use |
|---|---|
url / src | Image URL. One is required for object payloads. |
alt | Image alt text. |
title | Optional image title. |
size | Preset size: xs, sm, md, lg, or xl. |
rounded | Boolean roundness override. |
width / height | Explicit image dimensions. |
class | Extra image classes. |
OptionImage builder methods include url(), to(), alt(), title(), size(), small(), medium(), large(), extraLarge(), rounded(), square(), width(), height(), dimensions(), and class().
For explicitly grouped static options, group the array shape and call groupBy(). The options do not need an extra group key:
Combobox::make('workspace_tool')
->options([
'Design' => [
Option::make('design-kit', 'Design kit'),
Option::make('style-guide', 'Style guide'),
],
'Delivery' => [
Option::make('release-board', 'Release board'),
],
])
->groupBy();Eloquent model and query options may be grouped by a model accessor, relation path, or callback. In these examples, Project and User are Eloquent models:
Combobox::make('project_id')
->options(Project::query()->orderBy('name'), label: 'name')
->groupBy('team.name');
Combobox::make('assignee_id')
->options(User::query()->orderBy('name'), label: 'name')
->groupBy(fn (User $user) => $user->team?->name ?? 'Unassigned');Use mapGroupAs() to translate group values into labels. An array maps the raw group value. A callback receives the source row and optional source key, so it may read sibling attributes:
Combobox::make('status')
->options([
['code' => 'pending', 'name' => 'draft', 'role_id' => 10],
['code' => 'published', 'name' => 'live', 'role_id' => 20],
], label: 'name', value: 'code')
->groupBy('role_id')
->mapGroupAs([
10 => 'Internal',
20 => 'Public',
]);
Combobox::make('assignee_id')
->options([
['id' => 1, 'name' => 'Avery Stone', 'role_id' => 10, 'role_name' => 'admin'],
['id' => 2, 'name' => 'Morgan Vale', 'role_id' => 20, 'role_name' => 'editor'],
], label: 'name', value: 'id')
->groupBy('role_id')
->mapGroupAs(fn (array $user) => str($user['role_name'])->title()->plural()->toString());groupBy() needs the complete option list, so grouped model/query-backed options resolve as finite options and do not use Inertia scroll. A Closure passed to optionValue() or mapValueAs() uses the same finite fallback. Keep large model/query sources ungrouped and map values with a string key when they should page through Inertia Infinite Scroll.
Use the option mapping helpers for icons, badges, URLs, metadata, disabled states, and selected-item suffix text. Icon keys require a frontend resolver. See Icons for iconResolver setup:
Combobox::make('workspace_tool')
->options([
'Design stack' => [
Option::make('design-kit', 'Design kit')
->description('Shared tokens, preview states, and review notes.')
->icon('PaletteIcon')
->selectedSuffix('Pinned'),
Option::make('illustration-brief', 'Illustration brief')
->description('Requests diagrams or small UI artwork.')
->icon('PenToolIcon'),
],
'Delivery stack' => [
Option::make('release-board', 'Release board')
->description('Tracks approvals, owners, and blocked tasks.')
->icon('PanelsTopLeftIcon'),
Option::make('deploy-runner', 'Deploy runner')
->description('Requires release captain approval.')
->icon('ServerIcon')
->disabled(reason: 'Locked during the release freeze.'),
],
'People shortcuts' => [
Option::make('team-duty', 'Team duty contact')
->description('Routes urgent questions to the current owner.')
->icon('UserRoundIcon'),
Option::make('ops-support', 'Operations support')
->icon('ShieldCheckIcon'),
],
])
->groupBy()
->searchable()
->clearable()
->placeholder('Search tools')
->emptyText('No tools available')
->noResultsText('No tools found')
->withSelectedOptionMedia()
->default('design-kit');Query-backed options use the same helpers against model attributes:
Combobox::make('workspace_tool')
->options(Tool::query()->orderBy('name'), label: 'name')
->groupBy('category')
->optionIcon('icon')
->optionBadge('badge')
->optionDisabled('locked')
->optionDisabledReason('locked_reason')
->optionSelectedSuffix('status');Resolving Labels
Use optionLabel() when the option label should come from a source record key, relation path, or callback. Passing label: to options() is the same shortcut for the common case:
Combobox::make('assignee_id')
->options(User::query()->orderBy('name'), value: 'id')
->optionLabel('name');Remapping Labels
Use mapAs() when the source value is already resolved and should be translated to a display label:
Combobox::make('status')
->options([
['code' => 'draft', 'name' => 'draft'],
['code' => 'review', 'name' => 'review'],
], label: 'name', value: 'code')
->mapAs([
'draft' => 'Draft',
'review' => 'In review',
]);Adjusting Built Options
Use mapOptionAs() when the final option needs custom logic after the base mappings run. It receives the normalized Option, the source record, and the source key:
Combobox::make('feature_id')
->options(Feature::query()->orderBy('name'))
->mapOptionAs(fn (Option $option, Feature $feature) => $option
->label($feature->name)
->description($feature->summary)
->metadata(['owner' => $feature->owner_id]));Mapping Reference
| Method | Use |
|---|---|
optionValue() | Resolve the submitted option value from a key, relation path, or callback. |
mapValueAs() | Remap the resolved submitted value. |
optionDescription() / mapDescriptionAs() | Resolve or remap secondary row text. |
optionImage() / mapImageAs() | Resolve or remap larger row or selected media. |
optionAvatar() / mapAvatarAs() | Resolve or remap avatar media. |
optionIcon() / mapIconAs() | Resolve or remap a frontend icon key. |
optionBadge() / mapBadgeAs() | Resolve or remap trailing badge text. |
optionUrl() / mapUrlAs() | Resolve or remap an associated URL for custom slots. |
optionMetadata() / mapMetadataAs() | Resolve or remap structured application metadata. |
optionDisabled() / mapDisabledAs() | Resolve or remap disabled rows. |
optionDisabledReason() / mapDisabledReasonAs() | Resolve or remap disabled row explanations. |
optionSelectedSuffix() / mapSelectedSuffixAs() | Add compact selected-value suffix text. |
groupBy() / mapGroupAs() | Group flat arrays, query rows, or relation paths. |
Selected Media
Use withSelectedOptionMedia() when selected choice values should show mapped media from the option row. Record selections use withSelectedRecordMedia() in the record examples below.
Combobox::make('workspace_tool')
->options(Tool::query(), label: 'name', value: 'id')
->optionIcon('icon')
->withSelectedOptionMedia();Records
Use records() when the submitted value belongs to an application record and the selected record should keep display metadata around that value. Tokens submit the text they show. Records submit the configured value and render from a selected record payload, such as a label, email, avatar, disabled state, or metadata. A multiple record field may also become a sortable row list with reorderable(), shown later in this section.
$options = [
['id' => 1, 'name' => 'Mara Finch', 'email' => 'mara@example.com', 'avatar' => '/avatars/mara-finch.jpeg'],
['id' => 2, 'name' => 'Noel Park', 'email' => 'noel@example.com', 'avatar' => '/avatars/noel-park.jpeg'],
['id' => 3, 'name' => 'Iris Vale', 'email' => 'iris@example.com', 'avatar' => '/avatars/iris-vale.jpeg'],
['id' => 4, 'name' => 'Avery Stone', 'email' => 'avery@example.com', 'avatar' => '/avatars/avery-stone.jpeg'],
];
Combobox::make('author_id')
->records()
->label('Author')
->placeholder('Search users')
->options($options, label: 'name', value: 'id', description: 'email')
->withSelectedRecordMedia()
->clearable()
->default(2);Model and query-backed options() use the same Inertia Infinite Scroll path described above. records() changes the selected-value contract, not whether the option source may page through Inertia:
Combobox::make('assignee_id')
->records()
->options(User::query()->orderBy('name'), label: 'name', value: 'id')
->perPage(10);Record rows may include disabled and disabledReason. Disabled rows remain visible in the dropdown and are not selectable. The shared disabled() field API locks current selected records and keeps their selected display visible:
Combobox::make('reviewer_ids')
->records()
->options([
[
'id' => 1,
'name' => 'Avery Stone',
'disabled' => true,
'disabledReason' => 'Already assigned',
],
['id' => 2, 'name' => 'Morgan Vale'],
], label: 'name', value: 'id')
->selected([['value' => 1, 'label' => 'Avery Stone']])
->multiple()
->disabled();Multiple record lists submit an ordered array of selected IDs. Add reorderable() inside the records workflow only when that selected order matters.
Combobox::make('reviewer_ids')
->records()
->label('Reviewers')
->options([
['value' => 'content', 'label' => 'Content desk'],
['value' => 'design', 'label' => 'Design desk'],
['value' => 'support', 'label' => 'Support desk'],
['value' => 'ops', 'label' => 'Operations desk'],
])
->selected([
['value' => 'content', 'label' => 'Content desk'],
['value' => 'design', 'label' => 'Design desk'],
])
->multiple()
->reorderable()
->maxSelected(3);maxSelected() caps the number of records the user may select. minSelected(), maxSelected(), exists(), and distinct() are reflected in generated validation rules for record IDs. In records mode, maxItemsText() replaces the message shown after maxSelected() is reached.
Combobox::make('approval_reviewer_ids')
->records()
->label('Approval reviewers')
->placeholder('Add reviewers')
->options([
[
'value' => 'copy',
'label' => 'Copy review',
'description' => 'Checks wording and tone.',
],
[
'value' => 'legal',
'label' => 'Legal review',
'description' => 'Already assigned to this launch.',
'disabled' => true,
'disabledReason' => 'Already assigned to this launch.',
],
[
'value' => 'security',
'label' => 'Security review',
'description' => 'Checks access and sensitive changes.',
],
[
'value' => 'release',
'label' => 'Release review',
'description' => 'Final approval before publishing.',
],
])
->selected([
[
'value' => 'copy',
'label' => 'Copy review',
'description' => 'Checks wording and tone.',
],
])
->multiple()
->maxSelected(2)
->maxItemsText('Two reviewers maximum');Remote Endpoints
Endpoint-backed records use source() for search and pagination. A separate selectedSource() endpoint may resolve selected labels during edit forms. Without selectedSource(), the frontend falls back to the source endpoint, sends the configured valuesParam(), and includes selected=1.
Combobox::make('author_id')
->records()
->source('/api/users')
->selectedSource('/api/users/selected')
->withSelectedRecordMedia()
->exists(User::class, 'id')
->placeholder('Search users');Use searchOptionsUsing() for search-only option endpoints. It sends the current query as q and search, expects option rows back, and bypasses Inertia scroll. Use source() for records, selected lookup, request context, and paged sources.
Use selected() for selected rows already known during server rendering. It seeds display metadata for the current value; it does not set the submitted value. Use default() or bound form data for the value:
Combobox::make('author_id')
->records()
->source('/api/users')
->selected(fn (mixed $value) => User::query()
->whereKey($value)
->get()
->map(fn (User $user) => [
'value' => $user->id,
'label' => $user->name,
'description' => $user->email,
]));Source Lifecycle And Paging
Record sources include request context, search, preload, paging, and state text controls. Endpoint-backed results use the configured pageParam() and perPage() for infinite-scroll style paging. preload() fetches the first page when the dropdown opens. Passing an integer requests that many items only for page 1 with an empty query. Later pages and searched requests use perPage():
Combobox::make('reviewer_ids')
->records()
->source('/api/users', ['team' => 7])
->params(fn () => ['tenant' => tenant('id')])
->filters(fn () => ['active' => true])
->scopes(['reviewable', 'verified'])
->searchParam('term')
->valuesParam('ids')
->pageParam('p')
->perPageParam('size')
->perPage(50)
->minSearchLength(2)
->debounce(150)
->preload(10) // first empty-query page only
->emptyText('Type a user name')
->noResultsText('No users found')
->loadingText('Loading users')
->errorText('Could not load users');params() sends fixed request context as top-level request data. filters() sends nested filters data. scopes() sends a list of named source hints. Search requests, selected-value lookups, Inertia option reloads, and quick-create requests receive the same context. Scopes are hints, not executable input, so endpoint code should allow-list accepted scope names before applying query scopes.
minSearchLength() waits for enough typed characters before a remote search. debounce() delays remote search after typing. records() makes inline record options searchable by default; use searchable(false) for inline or preloaded records that should not filter locally. Endpoint-backed records remain searchable because the source URL owns search.
Quick create persists a new record and selects the returned item. It stays in record mode because the field calls records() explicitly:
Combobox::make('assignee_id')
->records()
->source('/api/users')
->createRecordUsing('/api/users', method: 'post', param: 'name')
->createRecordText('Add "%s"')
->canCreateRecord(auth()->user()->can('create', User::class));The param argument controls the primary request body key. The frontend also sends q as a query mirror when the primary key is not q. The body always includes params, filters, and scopes:
{
"name": "Avery Stone",
"q": "Avery Stone",
"params": { "tenant": "acme" },
"filters": { "active": true },
"scopes": ["reviewable"]
}createRecordText() accepts %s or {query} placeholders for the typed query. canCreateRecord(false) hides the create affordance, but the endpoint should still authorize writes and validate the request. Successful create endpoints may return either a bare item or an item wrapper:
{ "value": 5, "label": "Avery Stone" }{ "item": { "value": 5, "label": "Avery Stone" } }Endpoint Response Shape
Return the raw array from a Laravel paginator's toArray() result whenever the endpoint is pageable. This is the preferred server response because it matches Laravel's default JSON pagination shape. The frontend reads data, current_page, per_page, total, last_page, and next_page_url:
Route::get('/api/users', function (Request $request) {
return User::query()
->when($request->query('q'), fn ($query, string $search) => $query
->where('name', 'like', "%{$search}%"))
->paginate($request->integer('per_page', 25))
->through(fn (User $user) => [
'value' => $user->id,
'label' => $user->name,
'description' => $user->email,
])
->toArray();
});next_page_url drives infinite loading. Length-aware paginator responses use the next numeric page, and cursor paginator responses may use next_cursor:
{
"current_page": 1,
"data": [
{
"value": 1,
"label": "Mara Finch",
"description": "Senior editor",
"avatar": "https://example.com/mara.jpg"
}
],
"per_page": 25,
"total": 128,
"last_page": 6,
"next_page_url": "https://app.test/api/users?page=2"
}{
"data": [
{
"value": 1,
"label": "Mara Finch"
}
],
"per_page": 25,
"next_cursor": "eyJpZCI6NDJ9",
"next_page_url": "https://app.test/api/users?cursor=eyJpZCI6NDJ9"
}Existing endpoints may return an alternate pagination shape with data plus meta.next_page and meta.has_more:
{
"data": [
{
"value": 1,
"label": "Mara Finch"
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 128,
"next_page": 2,
"has_more": true
}
}A bare array of items is accepted for non-paginated endpoints, but it disables infinite loading because no pagination metadata is available.
Selected lookup endpoints receive the configured valuesParam() key and should return the currently selected rows:
Route::get('/api/users/selected', function (Request $request) {
return User::query()
->whereIn('id', Arr::wrap($request->input('values', [])))
->get()
->map(fn (User $user) => [
'value' => $user->id,
'label' => $user->name,
'description' => $user->email,
]);
});Item Fields
| Key | Description |
|---|---|
value | Submitted value. Required. |
label | Visible label. Required. |
description | Secondary row text. |
disabled | Prevents selecting the item. |
disabledReason | Explains why a disabled item is unavailable. |
image | First-choice media shown in option rows or selected values. |
avatar | Fallback media used when image is not present. |
icon | Final media fallback. Requires a frontend icon resolver. |
badge | Trailing badge text. |
selectedSuffix | Compact suffix beside a selected value. |
metadata | Extra application data for custom slots. |
group | Group label for grouped custom option lists. |
Validation
Finite options() automatically add Rule::in() validation. Multiple choices and tokens add array validation and validate each item against the finite list unless custom values or remote sources make the complete list unknown.
Combobox::make('country')
->options(['us' => 'United States', 'ca' => 'Canada']);
// Adds Rule::in(['us', 'ca'])
Combobox::make('topics')
->tokens()
->options(['Planning', 'Research'])
->allowCustomValues(false);
// Adds 'array' on topics and Rule::in([...]) on topics.*
Combobox::make('author_id')
->records()
->source('/api/users')
->exists(User::class, 'id');
// Adds an exists rule for the submitted record id.minSelected() and maxSelected() validate selected count for multiple choice, tokens, and records. Multiple record fields add distinct to each submitted ID by default. Use distinct(false) to allow duplicate record IDs:
Combobox::make('reviewer_ids')
->records()
->source('/api/users')
->multiple()
->distinct(false);Eloquent model or builder option sources use EloquentOptionValuesRule for automatic membership validation when the submitted value maps to a real column. Use ruleIn(false) to disable automatic finite or Eloquent option membership validation for values broader than the configured options. Add explicit rules for the accepted value shape:
Combobox::make('status')
->options([
'draft' => 'Draft',
'published' => 'Published',
])
->ruleIn(false)
->rules(['string']);Shared APIs
Use Combobox for finite choices, typed tokens, and record picking. The PHP API selects the frontend intent with tokens() and records(), while ordinary choices keep the default intent.
Combobox also supports shared field APIs documented in Form Class, Model Binding, Validation, Styling, Conditional Visibility, and Authorization.
Vue Display Slots
Custom option UIs may replace Vue listbox rows, selected token chips, and status rows through slots:
<Combobox v-bind="form.getField('workspace_tool')">
<template #option="{ option, selected, disabled }">
<div :class="{ 'opacity-50': disabled }">
<strong>{{ option.label }}</strong>
<span v-if="option.description">{{ option.description }}</span>
<span v-if="selected">Selected</span>
</div>
</template>
<template #tag="{ option, remove, disabled }">
<span>
{{ option.label }}
<button v-if="!disabled" type="button" @click="remove(option.value)">
Remove
</button>
</span>
</template>
<template #empty="{ message }">
<span>{{ message }}</span>
</template>
<template #loading="{ message }">
<span>{{ message }}</span>
</template>
<template #create-option="{ message, create }">
<button type="button" @click="create">{{ message }}</button>
</template>
</Combobox>The option slot receives { option, selected, disabled }. The tag slot receives { option, remove, disabled }. The empty slot receives { query, message }, the loading slot receives { message }, and the create-option slot receives { query, message, create }.
Using The Component Directly
Combobox is exported from each stack's components subpath. Direct component usage accepts the same intent model as serialized PHP fields. Standalone token mode also accepts the direct-only delimiters and transformTag props. Native attributes and listeners are forwarded to the native select in simple mode and to the text input in custom mode. See Native Attributes and Events.
<script setup lang="ts">
import { Combobox } from '@inertiaui/form-vue/components'
import { ref } from 'vue'
const status = ref('review')
const topics = ref(['Research'])
const authorId = ref<number | null>(2)
const statusOptions = [
{ value: 'draft', label: 'Draft' },
{ value: 'review', label: 'In review' },
]
const topicOptions = [
{ value: 'Planning', label: 'Planning' },
{ value: 'Research', label: 'Research' },
]
const authors = [
{ value: 1, label: 'Mara Finch', avatar: '/avatars/mara-finch.jpeg' },
{ value: 2, label: 'Noel Park', avatar: '/avatars/noel-park.jpeg' },
{ value: 3, label: 'Iris Vale', avatar: '/avatars/iris-vale.jpeg' },
{ value: 4, label: 'Avery Stone', avatar: '/avatars/avery-stone.jpeg' },
]
</script>
<template>
<Combobox v-model="status" name="status" label="Status" :options="statusOptions" />
<Combobox
v-model="topics"
intent="tokens"
name="topics"
label="Topics"
:options="topicOptions"
:delimiters="[',', ';']"
transform-tag="uppercase"
/>
<Combobox
v-model="authorId"
intent="records"
name="author_id"
label="Author"
:options="authors"
:selected="[authors[1]]"
selected-record-media
/>
</template>import { Combobox } from '@inertiaui/form-react/components'
import { useState } from 'react'
const statusOptions = [
{ value: 'draft', label: 'Draft' },
{ value: 'review', label: 'In review' },
]
const topicOptions = [
{ value: 'Planning', label: 'Planning' },
{ value: 'Research', label: 'Research' },
]
const authors = [
{ value: 1, label: 'Mara Finch', avatar: '/avatars/mara-finch.jpeg' },
{ value: 2, label: 'Noel Park', avatar: '/avatars/noel-park.jpeg' },
{ value: 3, label: 'Iris Vale', avatar: '/avatars/iris-vale.jpeg' },
{ value: 4, label: 'Avery Stone', avatar: '/avatars/avery-stone.jpeg' },
]
export function DirectComboboxes() {
const [status, setStatus] = useState('review')
const [topics, setTopics] = useState(['Research'])
const [authorId, setAuthorId] = useState<number | null>(2)
return (
<>
<Combobox name="status" label="Status" options={statusOptions} value={status} onValueChange={setStatus} />
<Combobox
intent="tokens"
name="topics"
label="Topics"
options={topicOptions}
delimiters={[',', ';']}
transformTag="uppercase"
value={topics}
onValueChange={setTopics}
/>
<Combobox
intent="records"
name="author_id"
label="Author"
options={authors}
selected={[authors[1]]}
selectedRecordMedia
value={authorId}
onValueChange={setAuthorId}
/>
</>
)
}