Skip to content

Select

The select lets the user pick from a list of options. Create one with Select::make() and pass the options with ->options(). Each option is a label/value pair built with Select::option().

Select::make('country', 'Country')
->placeholder('Pick a country')
->options([
Select::option('Germany', 'de'),
Select::option('France', 'fr'),
Select::option('Spain', 'es'),
Select::option('Italy', 'it'),
])

->placeholder() sets the text shown before a value is chosen. It is not selectable as a value. ->emptyLabel() sets the message shown in the dropdown when no options match (default “No options”), and ->searchPlaceholder() sets the search box placeholder (default “Search…”).

->enum() builds the options from a backed enum. Pass the enum class for every case, or an array of cases for a subset. Labels come from the enum’s HasLabel contract when it implements one, otherwise the case name is humanized.

Select::make('status', 'Status')->enum(OrderStatus::class);

->multiple() lets the user choose more than one option. The field submits an array of values.

Select::make('languages', 'Languages')
->multiple()
->placeholder('Choose languages')
->options([
Select::option('PHP', 'php'),
Select::option('JavaScript', 'js'),
Select::option('Go', 'go'),
Select::option('Rust', 'rust'),
])

A static ->options() select has no search box — the dropdown shows the full list. Call ->searchable() with a resolver to add a search box and resolve options on the server (->searchPlaceholder() then sets its placeholder). There is no client-side filter for static options — a list long enough to need filtering belongs behind a resolver.

->searchable() turns the select into a server-driven search. The resolver receives $search (the query string) and returns the matching options; it may also inject $get/$state/$component and any container type such as Request through closure evaluation. The option value is the entity identifier and is fully controlled by the resolver, so it scales to large datasets without shipping every option to the client.

Select::make('author_id', 'Author')
->searchable(fn ($search) => User::query()
->where('name', 'like', "%{$search}%")
->limit(10)
->get()
->map(fn (User $user) => Select::option($user->name, (string) $user->id))
->all());

On an edit form the stored value needs a label to display. ->resolveSelectedUsing() receives the current value(s) — always as an array — and returns their options:

Select::make('author_id', 'Author')
->searchable(/* … */)
->resolveSelectedUsing(fn (array $values) => User::query()
->whereIn('id', $values)
->get()
->map(fn (User $user) => Select::option($user->name, (string) $user->id))
->all());

Backing a select with an Eloquent model is the common case, so ->optionsFrom() takes an OptionSource and wires both the search and the selected-label resolution from one declaration. EloquentOptions is the shipped source:

use Lattice\EloquentOptions;
Select::make('author_id', 'Author')
->optionsFrom(EloquentOptions::make(User::class)->label('name'));

->label() and ->value() pick the display and value columns (value defaults to the key). Add ->searchColumns(['name', 'email']) to match more than the label, ->scope(fn ($query) => $query->where('active', true)) to constrain the query, and ->limit(50) to cap results.

optionsFrom only resolves the options. The selected value (e.g. author_id) is a normal field value, so it is validated and returned in the submitted array like any other — your handler persists it. Lattice forms stay decoupled from the model.

->optionSchema() renders each option through a schema of bound components instead of the plain label — a second line, a badge, an avatar. Components bind option fields with ->dataKey($prop, $key), exactly like the stack column. The bindings resolve against the option’s data record plus its label and value (label and value are reserved and always resolve to the option’s own fields). The schema is declared once and ships once; each option only carries its data, passed as the third argument to Select::option(). For static options, the dropdown filter matches the option label; text rendered from data stays display-only.

Select::make('customer', 'Customer')
->placeholder('Pick a customer')
->options([
Select::option('Acme GmbH', '1', ['email' => '[email protected]', 'number' => 'K-10021']),
Select::option('Globex AG', '2', ['email' => '[email protected]', 'number' => 'K-10022']),
])
->optionSchema([
Stack::make()->schema([
Text::make('')->dataKey('text', 'label'),
Text::make('')->dataKey('text', 'email')->size(Size::Sm)->color(Color::muted()),
]),
Badge::make('')->dataKey('label', 'number'),
])

Searchable selects work the same way: a resolver returns options with data (Select::option($user->name, (string) $user->id, ['email' => $user->email])), and EloquentOptions attaches it with ->data() — pass the columns to include, or a closure receiving the model for computed values:

Select::make('customer_id', 'Customer')
->optionsFrom(
EloquentOptions::make(Customer::class)
->searchColumns(['name', 'email', 'customer_number'])
->data(['email', 'customer_number']),
)
->optionSchema([
Stack::make()->schema([
Text::make('')->dataKey('text', 'label'),
Text::make('')->dataKey('text', 'email')->size(Size::Sm)->color(Color::muted()),
]),
Badge::make('')->dataKey('label', 'customer_number'),
]);

The selected value renders as the plain option label in the trigger and in multi-select chips.

->creatable() turns the select into a tag input: typing a value that isn’t in the list and pressing Enter (or a comma, or pasting a comma-separated list) adds it as a new option. Pair it with ->multiple() for a field that holds a set of tags.

Select::make('keywords', 'Keywords')
->multiple()
->creatable()
->placeholder('Add a keyword')

The typed label becomes the value — free-text tags submit as a plain array of strings, and the consumer creates matching records in handle().

Pair ->creatable() with ->optionsFrom() to back the tags with real records instead of plain strings, and give an option a color in its data to color its chip:

Select::make('tags', 'Tags')
->multiple()
->optionsFrom(EloquentOptions::make(Tag::class)->label('name')->data(['color']))
->creatable();

A typed tag that matches no existing option still submits as its raw label, so the submitted array mixes real ids (picked options) with new labels (typed tags) — the consumer resolves the array in handle(), keeping the ids and creating records for the remaining labels.

->itemRules() validates each tag instead of the field as a whole:

Select::make('keywords', 'Keywords')
->multiple()
->creatable()
->itemRules(['string', 'max:40']);

Select shares label, default value, required, disabled, read-only, and visibility options with every field — see Fields. For validation and conditional behavior, see Validation and Conditional fields.