Skip to content

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.

Example
php
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:

php
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.

Example
php
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.

php
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:

php
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:

php
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.

Example
php
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:

php
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.

Example
Copy reviewAccessibility review
php
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.

Example
Planning
php
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.

Example
Research

php
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:

php
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:

php
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.

php
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.

Example
release-plan

php
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:

php
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

Example
DraftReviewed

php
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:

php
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:

php
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:

php
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:

php
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:

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:

KeyUse
url / srcImage URL. One is required for object payloads.
altImage alt text.
titleOptional image title.
sizePreset size: xs, sm, md, lg, or xl.
roundedBoolean roundness override.
width / heightExplicit image dimensions.
classExtra 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:

php
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:

php
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:

php
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:

Example
php
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:

php
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:

php
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:

php
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:

php
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

MethodUse
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.

php
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.

Example
Noel Park

php
$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:

php
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:

php
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.

Example
  • Content desk
  • Design desk

php
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.

Example
  • Copy reviewChecks wording and tone.

php
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.

php
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:

php
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():

php
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:

php
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:

json
{
  "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:

json
{ "value": 5, "label": "Avery Stone" }
json
{ "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:

php
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:

json
{
  "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"
}
json
{
  "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:

json
{
  "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:

php
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

KeyDescription
valueSubmitted value. Required.
labelVisible label. Required.
descriptionSecondary row text.
disabledPrevents selecting the item.
disabledReasonExplains why a disabled item is unavailable.
imageFirst-choice media shown in option rows or selected values.
avatarFallback media used when image is not present.
iconFinal media fallback. Requires a frontend icon resolver.
badgeTrailing badge text.
selectedSuffixCompact suffix beside a selected value.
metadataExtra application data for custom slots.
groupGroup 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.

php
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:

php
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:

php
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:

vue
<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.

vue
<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>
tsx
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}
            />
        </>
    )
}