Combo Box allows the user to choose a value from a filterable list of options presented in an overlay.
It supports lazy loading and can be configured to accept custom typed values.
import { html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import '@vaadin/combo-box';
import { getCountries } from 'Frontend/demo/domain/DataService';
import type Country from 'Frontend/generated/com/vaadin/demo/domain/Country';
import { applyTheme } from 'Frontend/generated/theme';
@customElement('combo-box-basic')
export class Example extends LitElement {
protected override createRenderRoot() {
const root = super.createRenderRoot();
// Apply custom theme (only supported if your app uses one)
applyTheme(root);
return root;
}
@state()
private items: Country[] = [];
protected override async firstUpdated() {
this.items = await getCountries();
}
protected override render() {
return html`
<!-- tag::snippet[] -->
<vaadin-combo-box
label="Country"
item-label-path="name"
item-value-path="id"
.items="${this.items}"
></vaadin-combo-box>
<!-- end::snippet[] -->
`;
}
}
Country.ts
/**
* This module is generated from com.vaadin.demo.domain.Country.
* All changes to this file are overridden. Consider editing the corresponding Java file if necessary.
* @see {@link file:///srv/jenkins/workspace/docs/docs-site-v23/docs/src/main/java/com/vaadin/demo/domain/Country.java}
*/
export default interface Country {
name: string;
abbreviation: string;
id: number;
}
Country.tsComboBoxBasic.java
package com.vaadin.demo.component.combobox;
import com.vaadin.flow.component.combobox.ComboBox;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.router.Route;
import com.vaadin.demo.domain.Country;
import com.vaadin.demo.domain.DataService;
@Route("combo-box-basic")
public class ComboBoxBasic extends Div {
public ComboBoxBasic() {
// tag::snippet[]
ComboBox<Country> comboBox = new ComboBox<>("Country");
comboBox.setItems(DataService.getCountries());
comboBox.setItemLabelGenerator(Country::getName);
add(comboBox);
// end::snippet[]
}
}
Country.java
package com.vaadin.demo.domain;
import javax.annotation.Nonnull;
// tag::snippet[]
public class Country {
@Nonnull
private String name;
@Nonnull
private String abbreviation;
@Nonnull
private Integer id;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAbbreviation() {
return abbreviation;
}
public void setAbbreviation(String abbreviation) {
this.abbreviation = abbreviation;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
@Override
public int hashCode() {
return id;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Country)) {
return false;
}
Country other = (Country) obj;
return id == other.id;
}
}
// end::snippet[]
The overlay opens when the user clicks the field using a pointing device.
Using the Up/Down arrow keys or typing a character (found in at least one of the options) when the field is focused also opens the popup.
Common Input Field Features
Combo Box includes all
Text Field and
shared input field features.
import { html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import '@vaadin/combo-box';
import { applyTheme } from 'Frontend/generated/theme';
@customElement('combo-box-custom-entry-1')
export class Example extends LitElement {
protected override createRenderRoot() {
const root = super.createRenderRoot();
// Apply custom theme (only supported if your app uses one)
applyTheme(root);
return root;
}
@state()
private items = ['Chrome', 'Edge', 'Firefox', 'Safari'];
protected override render() {
return html`
<!-- tag::snippet[] -->
<vaadin-combo-box
allow-custom-value
label="Browser"
helper-text="Select or type a browser"
.items="${this.items}"
></vaadin-combo-box>
<!-- end::snippet[] -->
`;
}
}
ComboBoxCustomEntry1.java
package com.vaadin.demo.component.combobox;
import com.vaadin.flow.component.combobox.ComboBox;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.router.Route;
@Route("combo-box-custom-entry-1")
public class ComboBoxCustomEntry1 extends Div {
public ComboBoxCustomEntry1() {
// tag::snippet[]
ComboBox<String> comboBox = new ComboBox<>("Browser");
comboBox.setAllowCustomValue(true);
add(comboBox);
// end::snippet[]
comboBox.setItems("Chrome", "Edge", "Firefox", "Safari");
comboBox.setHelperText("Select or type a browser");
}
}
Allowing custom entry is useful when you need to present the most common choices but still give users the freedom to enter their own options.
Custom values can also be stored and added to the list of options:
import { html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import '@vaadin/combo-box';
import { comboBoxRenderer } from '@vaadin/combo-box/lit.js';
import type { ComboBoxLitRenderer } from '@vaadin/combo-box/lit.js';
import type { ComboBoxFilterChangedEvent } from '@vaadin/combo-box';
import { getPeople } from 'Frontend/demo/domain/DataService';
import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';
import { applyTheme } from 'Frontend/generated/theme';
@customElement('combo-box-presentation')
export class Example extends LitElement {
protected override createRenderRoot() {
const root = super.createRenderRoot();
// Apply custom theme (only supported if your app uses one)
applyTheme(root);
return root;
}
@state()
private allItems: Person[] = [];
@state()
private filteredItems: Person[] = [];
protected override async firstUpdated() {
const { people } = await getPeople();
const items = people.map((person) => ({
...person,
displayName: `${person.firstName} ${person.lastName}`,
}));
this.allItems = items;
this.filteredItems = items;
}
protected override render() {
return html`
<!-- tag::combobox[] -->
<vaadin-combo-box
label="Choose doctor"
item-label-path="displayName"
.filteredItems="${this.filteredItems}"
style="--vaadin-combo-box-overlay-width: 16em"
@filter-changed="${this.filterChanged}"
${comboBoxRenderer(this.renderer, [])}
></vaadin-combo-box>
<!-- end::combobox[] -->
`;
}
private filterChanged(e: ComboBoxFilterChangedEvent) {
const filter = e.detail.value;
this.filteredItems = this.allItems.filter(({ firstName, lastName, profession }) =>
`${firstName} ${lastName} ${profession}`.toLowerCase().includes(filter.toLowerCase())
);
}
// NOTE
// We are using inline styles here to keep the example simple.
// We recommend placing CSS in a separate style sheet and
// encapsulating the styling in a new component.
private renderer: ComboBoxLitRenderer<Person> = (person) => html`
<div style="display: flex;">
<img
style="height: var(--lumo-size-m); margin-right: var(--lumo-space-s);"
src="${person.pictureUrl}"
alt="Portrait of ${person.firstName} ${person.lastName}"
/>
<div>
${person.firstName} ${person.lastName}
<div style="font-size: var(--lumo-font-size-s); color: var(--lumo-secondary-text-color);">
${person.profession}
</div>
</div>
</div>
`;
}
Person.ts
import Address from './Address';
/**
* This module is generated from com.vaadin.demo.domain.Person.
* All changes to this file are overridden. Consider editing the corresponding Java file if necessary.
* @see {@link file:///srv/jenkins/workspace/docs/docs-site-v23/docs/src/main/java/com/vaadin/demo/domain/Person.java}
*/
export default interface Person {
firstName: string;
lastName: string;
email: string;
birthday: string;
id: number;
subscriber: boolean;
membership: string;
pictureUrl: string;
profession: string;
address: Address;
managerId?: number;
manager: boolean;
status: string;
}
Person.tsComboBoxPresentation.java
package com.vaadin.demo.component.combobox;
import com.vaadin.flow.component.combobox.ComboBox;
import com.vaadin.flow.component.combobox.ComboBox.ItemFilter;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.data.renderer.LitRenderer;
import com.vaadin.flow.data.renderer.Renderer;
import com.vaadin.flow.router.Route;
import com.vaadin.demo.domain.DataService;
import com.vaadin.demo.domain.Person;
@Route("combo-box-presentation")
public class ComboBoxPresentation extends Div {
public ComboBoxPresentation() {
ItemFilter<Person> filter = (person,
filterString) -> (person.getFirstName() + " "
+ person.getLastName() + " " + person.getProfession())
.toLowerCase().indexOf(filterString.toLowerCase()) > -1;
// tag::combobox[]
ComboBox<Person> comboBox = new ComboBox<>("Choose doctor");
comboBox.setItems(filter, DataService.getPeople());
comboBox.setItemLabelGenerator(
person -> person.getFirstName() + " " + person.getLastName());
comboBox.setRenderer(createRenderer());
comboBox.getStyle().set("--vaadin-combo-box-overlay-width", "16em");
add(comboBox);
// end::combobox[]
}
// NOTE
// We are using inline styles here to keep the example simple.
// We recommend placing CSS in a separate style sheet and to
// encapsulating the styling in a new component.
private Renderer<Person> createRenderer() {
StringBuilder tpl = new StringBuilder();
tpl.append("<div style=\"display: flex;\">");
tpl.append(
" <img style=\"height: var(--lumo-size-m); margin-right: var(--lumo-space-s);\" src=\"${item.pictureUrl}\" alt=\"Portrait of ${item.firstName} ${item.lastName}\" />");
tpl.append(" <div>");
tpl.append(" ${item.firstName} ${item.lastName}");
tpl.append(
" <div style=\"font-size: var(--lumo-font-size-s); color: var(--lumo-secondary-text-color);\">${item.profession}</div>");
tpl.append(" </div>");
tpl.append("</div>");
return LitRenderer.<Person> of(tpl.toString())
.withProperty("pictureUrl", Person::getPictureUrl)
.withProperty("firstName", Person::getFirstName)
.withProperty("lastName", Person::getLastName)
.withProperty("profession", Person::getProfession);
}
}
Person.java
package com.vaadin.demo.domain;
import java.util.Date;
import javax.annotation.Nonnull;
// tag::snippet[]
public class Person {
@Nonnull
private String firstName;
@Nonnull
private String lastName;
@Nonnull
private String email;
@Nonnull
private Date birthday;
@Nonnull
private Integer id;
@Nonnull
private Boolean subscriber;
@Nonnull
private String membership;
@Nonnull
private String pictureUrl;
@Nonnull
private String profession;
@Nonnull
private Address address;
private Integer managerId;
@Nonnull
private Boolean manager;
@Nonnull
private String status;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getFullName() {
return firstName + " " + lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public boolean isSubscriber() {
return subscriber;
}
public void setSubscriber(boolean subscriber) {
this.subscriber = subscriber;
}
public String getMembership() {
return membership;
}
public void setMembership(String membership) {
this.membership = membership;
}
public String getPictureUrl() {
return pictureUrl;
}
public void setPictureUrl(String pictureUrl) {
this.pictureUrl = pictureUrl;
}
public String getProfession() {
return profession;
}
public void setProfession(String profession) {
this.profession = profession;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
@Override
public int hashCode() {
return id;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Person)) {
return false;
}
Person other = (Person) obj;
return id == other.id;
}
public Integer getManagerId() {
return managerId;
}
public void setManagerId(Integer managerId) {
this.managerId = managerId;
}
public boolean isManager() {
return manager;
}
public void setManager(boolean manager) {
this.manager = manager;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
}
// end::snippet[]
Use a custom filter to allow the user to search by the rendered properties.
It’s recommended to make filtering case insensitive.
Auto Open
The overlay opens automatically when the field is focused using a pointer (mouse or touch), or when the user types in the field.
You can disable that to only open the overlay when the toggle button or Up/Down arrow keys are pressed.
import { html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import '@vaadin/combo-box';
import { getCountries } from 'Frontend/demo/domain/DataService';
import type Country from 'Frontend/generated/com/vaadin/demo/domain/Country';
import { applyTheme } from 'Frontend/generated/theme';
@customElement('combo-box-auto-open')
export class Example extends LitElement {
protected override createRenderRoot() {
const root = super.createRenderRoot();
// Apply custom theme (only supported if your app uses one)
applyTheme(root);
return root;
}
@state()
private items: Country[] = [];
protected override async firstUpdated() {
this.items = await getCountries();
}
protected override render() {
return html`
<!-- tag::snippet[] -->
<vaadin-combo-box
auto-open-disabled
label="Country"
item-label-path="name"
item-value-path="id"
.items="${this.items}"
></vaadin-combo-box>
<!-- end::snippet[] -->
`;
}
}
Country.ts
/**
* This module is generated from com.vaadin.demo.domain.Country.
* All changes to this file are overridden. Consider editing the corresponding Java file if necessary.
* @see {@link file:///srv/jenkins/workspace/docs/docs-site-v23/docs/src/main/java/com/vaadin/demo/domain/Country.java}
*/
export default interface Country {
name: string;
abbreviation: string;
id: number;
}
Country.tsComboBoxAutoOpen.java
package com.vaadin.demo.component.combobox;
import com.vaadin.flow.component.combobox.ComboBox;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.router.Route;
import com.vaadin.demo.domain.Country;
import com.vaadin.demo.domain.DataService;
@Route("combo-box-auto-open")
public class ComboBoxAutoOpen extends Div {
public ComboBoxAutoOpen() {
// tag::snippet[]
ComboBox<Country> comboBox = new ComboBox<>("Country");
comboBox.setAutoOpen(false);
add(comboBox);
// end::snippet[]
comboBox.setItems(DataService.getCountries());
comboBox.setItemLabelGenerator(Country::getName);
}
}
Country.java
package com.vaadin.demo.domain;
import javax.annotation.Nonnull;
// tag::snippet[]
public class Country {
@Nonnull
private String name;
@Nonnull
private String abbreviation;
@Nonnull
private Integer id;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAbbreviation() {
return abbreviation;
}
public void setAbbreviation(String abbreviation) {
this.abbreviation = abbreviation;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
@Override
public int hashCode() {
return id;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Country)) {
return false;
}
Country other = (Country) obj;
return id == other.id;
}
}
// end::snippet[]
Popup Width
The width of the popup is, by default, the same width as the input field.
The popup width can be overridden to any fixed width in cases where the default width is too narrow.
import { html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import '@vaadin/combo-box';
import type { ComboBoxFilterChangedEvent } from '@vaadin/combo-box';
import { getCountries } from 'Frontend/demo/domain/DataService';
import type Country from 'Frontend/generated/com/vaadin/demo/domain/Country';
import { applyTheme } from 'Frontend/generated/theme';
// tag::snippet[]
@customElement('combo-box-filtering-2')
export class Example extends LitElement {
protected override createRenderRoot() {
const root = super.createRenderRoot();
// Apply custom theme (only supported if your app uses one)
applyTheme(root);
return root;
}
@state()
private allItems: Country[] = [];
@state()
private filteredItems: Country[] = [];
protected override async firstUpdated() {
const countries = await getCountries();
this.allItems = countries;
this.filteredItems = countries;
}
protected override render() {
return html`
<vaadin-combo-box
label="Country"
item-label-path="name"
item-value-path="id"
.filteredItems="${this.filteredItems}"
@filter-changed="${this.filterChanged}"
></vaadin-combo-box>
`;
}
private filterChanged(event: ComboBoxFilterChangedEvent) {
const filter = event.detail.value;
this.filteredItems = this.allItems.filter(({ name }) =>
name.toLowerCase().startsWith(filter.toLowerCase())
);
}
}
// end::snippet[]
ComboBoxFiltering2.java
package com.vaadin.demo.component.combobox;
import com.vaadin.flow.component.combobox.ComboBox;
import com.vaadin.flow.component.combobox.ComboBox.ItemFilter;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.router.Route;
import com.vaadin.demo.domain.Country;
import com.vaadin.demo.domain.DataService;
@Route("combo-box-filtering-2")
public class ComboBoxFiltering2 extends Div {
public ComboBoxFiltering2() {
// tag::snippet[]
ComboBox<Country> comboBox = new ComboBox<>("Country");
ItemFilter<Country> filter = (country, filterString) -> country
.getName().toLowerCase().startsWith(filterString.toLowerCase());
comboBox.setItems(filter, DataService.getCountries());
add(comboBox);
// end::snippet[]
comboBox.setItemLabelGenerator(Country::getName);
}
}
Usage as Autocomplete Field
As the user is typing, the Combo Box filters out the options that don’t match.
Once the correct value has been found, the user can use the Up/Down arrow keys to navigate the list and the Enter key to set the value, essentially using the Combo Box as an autocomplete field.
Best Practices
Combo Box supports lazy loading for large datasets.
It reduces the initial load time, consumes less bandwidth and resources.
Note
Don’t use as a menu
Combo Box is an input field component, not a generic menu component.
Use the Menu Bar component to create overlays for actions.