> Markdown version of [Grid](https://vaadin.com/docs/latest/components/grid). Section index: [llms.txt](https://vaadin.com/docs/latest/components/llms.txt)

# Grid

Vaadin Grid is a component for displaying tabular data, including various enhancements to grid renderings.

Some of the more complex topics are described on separate pages:

- [Columns](https://vaadin.com/docs/latest/components/grid/columns.md)

- [Renderers](https://vaadin.com/docs/latest/components/grid/renderers.md)

- [Styling](https://vaadin.com/docs/latest/components/grid/styling.md)

- [Selection](https://vaadin.com/docs/latest/components/grid/selection.md)

- [Drag & Drop](https://vaadin.com/docs/latest/components/grid/drag-drop.md)

- [Data Binding Flow](https://vaadin.com/docs/latest/components/grid/data-binding.md)

- [Inline Editing Flow](https://vaadin.com/docs/latest/components/grid/inline-editing.md)

- [AI-Powered Grid Flow](https://vaadin.com/docs/latest/flow/ai-support/ai-powered-grid.md) — let users populate the grid from your database via natural language.

<!-- vale Vaadin.Will = NO -->

**Lit** — `grid-basic.ts`

```typescript
@state()
private items: Person[] = [];

protected override async firstUpdated() {
  const { people } = await getPeople();
  this.items = people;
}

protected override render() {
  return html`
    <vaadin-grid .items="${this.items}">
      <vaadin-grid-column path="firstName"></vaadin-grid-column>
      <vaadin-grid-column path="lastName"></vaadin-grid-column>
      <vaadin-grid-column path="email"></vaadin-grid-column>
      <vaadin-grid-column path="profession"></vaadin-grid-column>
    </vaadin-grid>
  `;
}
```

**Flow** — `GridBasic.java`

```java
Grid<Person> grid = new Grid<>(Person.class, false);
grid.addColumn(Person::getFirstName).setHeader("First name");
grid.addColumn(Person::getLastName).setHeader("Last name");
grid.addColumn(Person::getEmail).setHeader("Email");
grid.addColumn(Person::getProfession).setHeader("Profession");

List<Person> people = DataService.getPeople();
grid.setItems(people);
```

**Flow** — `Person.java`

```java
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;
    }
}
```

**React** — `grid-basic.tsx`

```typescript
const items = useSignal<Person[]>([]);
useEffect(() => {
  getPeople().then(({ people }) => {
    items.value = people;
  });
}, []);

return (
  <Grid items={items.value}>
    <GridColumn path="firstName" />
    <GridColumn path="lastName" />
    <GridColumn path="email" />
    <GridColumn path="profession" />
  </Grid>
);
```

> **Note: Auto-generated columns in Flow Grid**
>
> Although most code examples define columns explicitly, the Flow component can generated them automatically based root-level properties on the bean class if you pass it as an argument to the constructor, e.g. `new Grid<>(Person.class);`

## <a id="dynamic-height"></a>Dynamic Height

Grid has a default height of 400 pixels. It becomes scrollable when items contained in it overflow the allocated space.

In addition to setting any fixed or relative value, the height of a grid can be set by the number of items in the dataset. The grid expands and retracts based on the row count. This feature disables scrolling. It shouldn’t be used with large data sets since it might cause performance issues.

Notice how the height of the rows in the earlier example adjusts because of the text in the *Address* cells wrapping. With that in mind, click the gray icon at the top right corner of the example below to open it in a new browser tab. Try resizing it, making it narrower and then wider. Notice how the rows are always the same height and that the text doesn’t wrap. Instead, the text is truncated with ellipses.

**Lit** — `grid-dynamic-height.ts`

```typescript
<vaadin-grid
  .items="${this.invitedPeople}"
  all-rows-visible
  style="margin-top: var(--vaadin-gap-s)"
>
  <vaadin-grid-column header="Name" path="displayName" auto-width></vaadin-grid-column>
  <vaadin-grid-column path="email"></vaadin-grid-column>
  <vaadin-grid-column path="address.phone"></vaadin-grid-column>
  <vaadin-grid-column
    header="Manage"
    ${columnBodyRenderer(this.manageRenderer, [])}
  ></vaadin-grid-column>
</vaadin-grid>
```

**Flow** — `GridDynamicHeight.java`

```java
grid = new Grid<>(Person.class, false);
grid.setAllRowsVisible(true);
```

**React** — `grid-dynamic-height.tsx`

```tsx
<Grid
  items={invitedPeople.value}
  allRowsVisible
  style={{ marginTop: 'var(--vaadin-gap-s)' }}
>
  <GridColumn header="Name" path="displayName" autoWidth />
  <GridColumn path="email" />
  <GridColumn path="address.phone" />
  <GridColumn header="Manage" renderer={removePersonRenderer} />
</Grid>
```

## <a id="sorting"></a>Sorting

Any column can be used for sorting the data displayed. Enable sorting to allow the user to sort items alphabetically, numerically, by date, or by some other method.

The arrowhead symbols in the column header indicate the current sorting direction. When toggled, the direction will cycle between ascending, descending and none.

**Lit** — `grid-sorting.ts`

```typescript
<vaadin-grid .items="${this.items}">
  <vaadin-grid-sort-column path="id"></vaadin-grid-sort-column>
  <vaadin-grid-sort-column path="displayName" header="Name"></vaadin-grid-sort-column>
  <vaadin-grid-sort-column path="email"></vaadin-grid-sort-column>
  <vaadin-grid-sort-column path="profession"></vaadin-grid-sort-column>
  <vaadin-grid-sort-column path="birthday"></vaadin-grid-sort-column>
</vaadin-grid>
```

**Flow** — `GridSorting.java`

```java
Grid<Person> grid = new Grid<>(Person.class, false);
grid.addColumn(Person::getId).setHeader("Id").setSortable(true);
grid.addColumn(Person::getFullName).setHeader("Name").setSortable(true);
grid.addColumn(Person::getEmail).setHeader("Email").setSortable(true);
grid.addColumn(Person::getProfession).setHeader("Profession")
        .setSortable(true);
grid.addColumn(new LocalDateRenderer<>(GridSorting::getPersonBirthday,
        "yyyy-MM-dd")).setHeader("Birthday").setSortable(true)
        .setComparator(Person::getBirthday);
```

**React** — `grid-sorting.tsx`

```tsx
<Grid items={items.value}>
  <GridSortColumn path="id" />
  <GridSortColumn header="Name" path="displayName" />
  <GridSortColumn path="email" />
  <GridSortColumn path="profession" />
  <GridSortColumn path="birthday" />
</Grid>
```

### <a id="sorting-by-multiple-columns"></a>Sorting by Multiple Columns

Multi-sort mode allows the Grid to be sorted by multiple columns simultaneously.

In normal multi-sort mode, additional sorting columns are applied simply by clicking their headers.

A separate multi-sort on shift-click mode combines single and multi-column sorting by adding more sorting columns only when the user holds the `Shift` key while clicking their headers.

The order in which multi-sort columns (known as sorting criteria) are evaluated is determined by the multi-sort priority setting.

**Lit** — `grid-multisort.ts`

```typescript
<vaadin-grid .items="${this.items}" multi-sort multi-sort-priority="append">
  <vaadin-grid-sort-column path="id"></vaadin-grid-sort-column>
  <vaadin-grid-sort-column path="displayName" header="Name"></vaadin-grid-sort-column>
  <vaadin-grid-sort-column path="email"></vaadin-grid-sort-column>
  <vaadin-grid-sort-column path="profession"></vaadin-grid-sort-column>
  <vaadin-grid-sort-column path="birthday"></vaadin-grid-sort-column>
</vaadin-grid>
```

**Flow** — `GridMultiSort.java`

```java
Grid<Person> grid = new Grid<>(Person.class, false);
grid.addColumn(Person::getId).setHeader("Id").setSortable(true);
grid.addColumn(Person::getFullName).setHeader("Name").setSortable(true);
grid.addColumn(Person::getEmail).setHeader("Email").setSortable(true);
grid.addColumn(Person::getProfession).setHeader("Profession")
        .setSortable(true);
grid.addColumn(new LocalDateRenderer<>(GridMultiSort::getPersonBirthday,
        "yyyy-MM-dd")).setHeader("Birthday").setSortable(true)
        .setComparator(Person::getBirthday);
grid.setMultiSort(true, MultiSortPriority.APPEND);
```

**React** — `grid-multisort.tsx`

```tsx
<Grid items={items.value} multiSort multiSortPriority="append">
  <GridSortColumn path="id" />
  <GridSortColumn path="displayName" header="Name" />
  <GridSortColumn path="email" />
  <GridSortColumn path="profession" />
  <GridSortColumn path="birthday" />
</Grid>
```

> **Note: Shift-Click Multi-Sorting Accessibility Issues**
>
> The multi-sort on shift-click mode is not recommended for applications for which accessibility is important. This feature is unlikely to work well with assistive technologies, and the lack of visual affordance makes it difficult to discover for sighted users.

### <a id="specifying-sort-property"></a>Specifying Sort Property

Columns with rich or custom content can be sorted by defining the property by which to sort. For example, in the table here there’s a column containing the employees' first and last names, avatar images, and email addresses. By clicking on the heading for that column, it’ll sort the data by their last names.

**Lit** — `grid-rich-content-sorting.ts`

```typescript
@state()
private items: Person[] = [];

protected override async firstUpdated() {
  const { people } = await getPeople();
  this.items = people;
}

protected override render() {
  return html`
    <vaadin-grid .items="${this.items}">
      <vaadin-grid-sort-column
        header="Employee"
        path="lastName"
        ${columnBodyRenderer(this.employeeRenderer, [])}
      ></vaadin-grid-sort-column>
      <vaadin-grid-column
        ${columnHeaderRenderer(this.birthdayHeaderRenderer, [])}
        ${columnBodyRenderer(this.birthdayRenderer, [])}
      ></vaadin-grid-column>
    </vaadin-grid>
  `;
}

private employeeRenderer: GridColumnBodyLitRenderer<Person> = (person) => html`
  <div class="person-item">
    <vaadin-avatar
      img="${person.pictureUrl}"
      name="${person.firstName} ${person.lastName}"
      style="--vaadin-avatar-size: 2.25rem"
    ></vaadin-avatar>
    <span>${person.firstName} ${person.lastName}</span>
    <span>${person.email}</span>
  </div>
`;

private birthdayHeaderRenderer = () => html`
  <vaadin-grid-sorter path="birthday">Birthdate</vaadin-grid-sorter>
`;

private birthdayRenderer: GridColumnBodyLitRenderer<Person> = (person) => {
  const birthday = parseISO(person.birthday);
  return html`
    <div>${format(birthday, 'MM/dd/yyyy')}</div>
    <div style="font-size: .875rem; color: var(--vaadin-text-color-secondary);">
      Age: ${differenceInYears(Date.now(), birthday)}
    </div>
  `;
};
```

**Flow** — `GridRichContentSorting.java`

```java
Grid<Person> grid = new Grid<>(Person.class, false);
grid.addColumn(createEmployeeRenderer()).setHeader("Employee")
        .setAutoWidth(true).setFlexGrow(0)
        .setComparator(Person::getLastName);
grid.addColumn(createBirthdayRenderer()).setHeader("Birthdate")
        .setComparator(Person::getBirthday);
```

**React** — `grid-rich-content-sorting.tsx`

```tsx
function employeeRenderer({ item: person }: { item: Person }) {
  return (
    <div className="person-item">
      <Avatar
        img={person.pictureUrl}
        name={`${person.firstName} ${person.lastName}`}
        style={{ '--vaadin-avatar-size': '2.25rem' }}
      />
      <span>
        {person.firstName} {person.lastName}
      </span>
      <span>{person.email}</span>
    </div>
  );
}

function birthdayRenderer({ item: person }: { item: Person }) {
  const birthday = parseISO(person.birthday);
  return (
    <>
      <div>{format(birthday, 'MM/dd/yyyy')}</div>
      <div style={{ fontSize: '.875rem', color: 'var(--vaadin-text-color-secondary)' }}>
        Age: {Math.floor((Date.now() - birthday.getTime()) / (1000 * 60 * 60 * 24 * 365.25))}
      </div>
    </>
  );
}

function Example() {
  const items = useSignal<Person[]>([]);

  useEffect(() => {
    getPeople().then(({ people }) => {
      items.value = people;
    });
  }, []);

  return (
    <Grid items={items.value}>
      <GridSortColumn header="Employee" path="lastName" renderer={employeeRenderer} />

      <GridSortColumn header="Birthdate" path="birthday" renderer={birthdayRenderer} />
    </Grid>
  );
}
```

`person-item.css`

```css
.person-item {
    display: grid;
    grid-template-columns: min-content auto;
    grid-template-rows: auto auto;
    grid-template-areas:
        "avatar name"
        "avatar title";
    gap: 0 var(--vaadin-gap-s);
    align-items: center;
    line-height: 1.2;

    & > :is(vaadin-avatar, img) {
        grid-area: avatar;
    }

    & > span:first-of-type {
        grid-area: name;
    }

    & > span:last-of-type {
        grid-area: title;
        font-size: 0.875rem;
        font-weight: 400;
        color: var(--vaadin-text-color-secondary);
    }
}
```

Sorting helps users find and examine data. Therefore, it’s recommended to enable sorting for all applicable columns. An exception, though, would be when the order is an essential part of the data itself, such as with prioritized lists.

### <a id="programmatic-sorting"></a>Programmatic Sorting

You can sort a Grid programmatically using the `sort()` method with `GridSortOrder`. This is useful when you want to set an initial sort order or change the sorting based on application logic:

```java
Grid.Column<Person> nameColumn = grid.addColumn(Person::getLastName)
        .setHeader("Last Name")
        .setSortable(true);

// Sort ascending by the name column
grid.sort(GridSortOrder.asc(nameColumn).build());
```

To sort by multiple columns, use `GridSortOrder.asc()` or `GridSortOrder.desc()` and chain with `thenAsc()` or `thenDesc()`:

```java
grid.sort(GridSortOrder.asc(lastNameColumn)
        .thenDesc(ageColumn)
        .build());
```

> **Note: GridSortOrder vs. QuerySortOrder**
>
> `GridSortOrder` is used with `grid.sort()` and references Grid columns. Don’t confuse it with `QuerySortOrder`, which is used in data provider callbacks and references property name strings. The Grid automatically translates between the two when fetching data.

## <a id="filtering"></a>Filtering

<!-- vale Vale.Spelling = NO -->

Filtering allows the user to find a specific item or subset of items. You can add filters to Grid columns or use external filter fields.

For instance, try typing `anna` in the input box for *Name* below. When you’re finished, the data shown is only people who have *anna* in their name. That includes some with the names Anna and Annabelle, as well as some with Arianna and Brianna.

<!-- vale Vaadin.Spelling = YES -->

**Lit** — `grid-column-filtering.ts`

```typescript
<vaadin-grid .items="${this.items}">
  <vaadin-grid-filter-column
    header="Name"
    path="displayName"
    flex-grow="0"
    width="230px"
    ${columnBodyRenderer(this.nameRenderer, [])}
  ></vaadin-grid-filter-column>
  <vaadin-grid-filter-column path="email"></vaadin-grid-filter-column>
  <vaadin-grid-filter-column path="profession"></vaadin-grid-filter-column>
</vaadin-grid>
```

**Flow** — `GridColumnFiltering.java`

```java
Grid<Person> grid = new Grid<>(Person.class, false);
Grid.Column<Person> nameColumn = grid.addColumn(createPersonRenderer())
        .setWidth("230px").setFlexGrow(0);
Grid.Column<Person> emailColumn = grid.addColumn(Person::getEmail);
Grid.Column<Person> professionColumn = grid
        .addColumn(Person::getProfession);

List<Person> people = DataService.getPeople();
GridListDataView<Person> dataView = grid.setItems(people);
PersonFilter personFilter = new PersonFilter(dataView);

grid.getHeaderRows().clear();
HeaderRow headerRow = grid.appendHeaderRow();

headerRow.getCell(nameColumn).setComponent(
        createFilterHeader("Name", personFilter::setFullName));
headerRow.getCell(emailColumn).setComponent(
        createFilterHeader("Email", personFilter::setEmail));
headerRow.getCell(professionColumn).setComponent(
        createFilterHeader("Profession", personFilter::setProfession));

...

private static Component createFilterHeader(String labelText,
        Consumer<String> filterChangeConsumer) {
    TextField textField = new TextField(labelText);
    textField.setValueChangeMode(ValueChangeMode.EAGER);
    textField.setClearButtonVisible(true);
    textField.addThemeVariants(TextFieldVariant.SMALL);
    textField.addValueChangeListener(
            e -> filterChangeConsumer.accept(e.getValue()));
    return textField;
}

private static class PersonFilter {
    private final GridListDataView<Person> dataView;

    private String fullName;
    private String email;
    private String profession;

    public PersonFilter(GridListDataView<Person> dataView) {
        this.dataView = dataView;
        this.dataView.addFilter(this::test);
    }

    public void setFullName(String fullName) {
        this.fullName = fullName;
        this.dataView.refreshAll();
    }

    public void setEmail(String email) {
        this.email = email;
        this.dataView.refreshAll();
    }

    public void setProfession(String profession) {
        this.profession = profession;
        this.dataView.refreshAll();
    }

    public boolean test(Person person) {
        boolean matchesFullName = matches(person.getFullName(), fullName);
        boolean matchesEmail = matches(person.getEmail(), email);
        boolean matchesProfession = matches(person.getProfession(),
                profession);

        return matchesFullName && matchesEmail && matchesProfession;
    }

    private boolean matches(String value, String searchTerm) {
        return searchTerm == null || searchTerm.isEmpty()
                || value.toLowerCase().contains(searchTerm.toLowerCase());
    }
}
```

**React** — `grid-column-filtering.tsx`

```tsx
function Example() {
  const items = useSignal<PersonEnhanced[]>([]);

  useEffect(() => {
    getPeople().then(({ people }) => {
      items.value = people.map((person) => ({
        ...person,
        displayName: `${person.firstName} ${person.lastName}`,
      }));
    });
  }, []);

  return (
    <Grid items={items.value}>
      <GridFilterColumn
        header="Name"
        path="displayName"
        flexGrow={0}
        width="230px"
        renderer={nameRenderer}
      />

      <GridFilterColumn path="email" />
      <GridFilterColumn path="profession" />
    </Grid>
  );
}
```

Place filters outside the grid when the filter is based on multiple columns, or when a bigger field or more complex filter UI is needed, one which wouldn’t fit well in a column. In the example here, whatever you type in the search box can be matched against all of the columns. Type `Rheumatologist` in the search box. The results show only the rows with that profession.

**Lit** — `grid-external-filtering.ts`

```typescript
@state()
private filteredItems: PersonEnhanced[] = [];

private items: PersonEnhanced[] = [];

protected override async firstUpdated() {
  const { people } = await getPeople();
  const items = people.map((person) => ({
    ...person,
    displayName: `${person.firstName} ${person.lastName}`,
  }));
  this.items = items;
  this.filteredItems = items;
}

protected override render() {
  return html`
    <vaadin-vertical-layout theme="spacing">
      <vaadin-text-field
        placeholder="Search"
        style="width: 50%;"
        @value-changed="${(e: TextFieldValueChangedEvent) => {
          const searchTerm = (e.detail.value || '').trim();
          const matchesTerm = (value: string) =>
            value.toLowerCase().includes(searchTerm.toLowerCase());

          this.filteredItems = this.items.filter(
            ({ displayName, email, profession }) =>
              !searchTerm ||
              matchesTerm(displayName) ||
              matchesTerm(email) ||
              matchesTerm(profession)
          );
        }}"
      >
        <vaadin-icon slot="prefix" icon="vaadin:search"></vaadin-icon>
      </vaadin-text-field>
      <vaadin-grid .items="${this.filteredItems}">
        <vaadin-grid-column
          header="Name"
          flex-grow="0"
          width="230px"
          ${columnBodyRenderer(this.nameRenderer, [])}
        ></vaadin-grid-column>
        <vaadin-grid-column path="email"></vaadin-grid-column>
        <vaadin-grid-column path="profession"></vaadin-grid-column>
      </vaadin-grid>
    </vaadin-vertical-layout>
  `;
}
```

**Flow** — `GridExternalFiltering.java`

```java
Grid<Person> grid = new Grid<>(Person.class, false);
grid.addColumn(createPersonRenderer()).setHeader("Name").setFlexGrow(0)
        .setWidth("230px");
grid.addColumn(Person::getEmail).setHeader("Email");
grid.addColumn(Person::getProfession).setHeader("Profession");

List<Person> people = DataService.getPeople();
GridListDataView<Person> dataView = grid.setItems(people);

TextField searchField = new TextField();
searchField.setWidth("50%");
searchField.setPlaceholder("Search");
searchField.setPrefixComponent(new Icon(VaadinIcon.SEARCH));
searchField.setValueChangeMode(ValueChangeMode.EAGER);
searchField.addValueChangeListener(e -> dataView.refreshAll());

dataView.addFilter(person -> {
    String searchTerm = searchField.getValue().trim();

    if (searchTerm.isEmpty())
        return true;

    boolean matchesFullName = matchesTerm(person.getFullName(),
            searchTerm);
    boolean matchesEmail = matchesTerm(person.getEmail(), searchTerm);
    boolean matchesProfession = matchesTerm(person.getProfession(),
            searchTerm);

    return matchesFullName || matchesEmail || matchesProfession;
});
```

**React** — `grid-external-filtering.tsx`

```tsx
function nameRenderer({ item: person }: { item: PersonEnhanced }) {
  return (
    <HorizontalLayout style={{ alignItems: 'center' }} theme="spacing">
      <Avatar img={person.pictureUrl} name={person.displayName} />
      <span> {person.displayName} </span>
    </HorizontalLayout>
  );
}

function Example() {
  const filteredItems = useSignal<PersonEnhanced[]>([]);
  const items = useSignal<PersonEnhanced[]>([]);

  useEffect(() => {
    getPeople().then(({ people }) => {
      const newItems = people.map((person) => ({
        ...person,
        displayName: `${person.firstName} ${person.lastName}`,
      }));
      items.value = newItems;
      filteredItems.value = newItems;
    });
  }, []);

  return (
    <VerticalLayout theme="spacing">
      <TextField
        placeholder="Search"
        style={{ width: '50%' }}
        onValueChanged={(e) => {
          const searchTerm = (e.detail.value || '').trim().toLowerCase();
          filteredItems.value = items.value.filter(
            ({ displayName, email, profession }) =>
              !searchTerm ||
              displayName.toLowerCase().includes(searchTerm) ||
              email.toLowerCase().includes(searchTerm) ||
              profession.toLowerCase().includes(searchTerm)
          );
        }}
      >
        <Icon slot="prefix" icon="vaadin:search" />
      </TextField>

      <Grid items={filteredItems.value}>
        <GridColumn header="Name" flexGrow={0} width="230px" renderer={nameRenderer} />

        <GridColumn path="email" />
        <GridColumn path="profession" />
      </Grid>
    </VerticalLayout>
  );
}
```

## <a id="lazy-loading"></a>Lazy Loading

When you want to display a list of items that would be quite large to load entirely into memory, or you want to load items from a database, data providers can be used to provide lazy loading through pagination.

The following example works like the earlier example, but it uses a data provider for lazy loading, sorting, and filtering items.

**Lit** — `grid-data-provider.ts`

```typescript
async function fetchPeople(params: {
  page: number;
  pageSize: number;
  searchTerm: string;
  sortOrders: GridSorterDefinition[];
}) {
  const { page, pageSize, searchTerm, sortOrders } = params;
  const { people } = await getPeople();

  let result = people.map((person) => ({
    ...person,
    fullName: `${person.firstName} ${person.lastName}`,
  }));

  // Filtering
  if (searchTerm) {
    result = result.filter(
      (p) => matchesTerm(p.fullName, searchTerm) || matchesTerm(p.profession, searchTerm)
    );
  }

  // Sorting
  const sortBy = Object.fromEntries(sortOrders.map(({ path, direction }) => [path, direction]));
  if (sortBy.fullName) {
    result = result.sort((p1, p2) => compare(p1.fullName, p2.fullName, sortBy.fullName));
  } else if (sortBy.profession) {
    result = result.sort((p1, p2) => compare(p1.profession, p2.profession, sortBy.profession));
  }

  // Pagination
  const count = result.length;
  const offset = page * pageSize;
  result = result.slice(offset, offset + pageSize);

  return { people: result, count };
}

...

@state()
private searchTerm = '';

@query('#grid')
private grid!: Grid;

private dataProvider = async (
  params: GridDataProviderParams<Person>,
  callback: GridDataProviderCallback<Person>
) => {
  const { page, pageSize, sortOrders } = params;

  const { people, count } = await fetchPeople({
    page,
    pageSize,
    sortOrders,
    searchTerm: this.searchTerm,
  });

  callback(people, count);
};

protected override render() {
  return html`
    <vaadin-vertical-layout theme="spacing">
      <vaadin-text-field
        placeholder="Search"
        style="width: 50%;"
        @value-changed="${(e: TextFieldValueChangedEvent) => {
          this.searchTerm = (e.detail.value || '').trim();
          this.grid.clearCache();
        }}"
      >
        <vaadin-icon slot="prefix" icon="vaadin:search"></vaadin-icon>
      </vaadin-text-field>
      <vaadin-grid id="grid" .dataProvider="${this.dataProvider}">
        <vaadin-grid-sort-column path="fullName" header="Name"></vaadin-grid-sort-column>
        <vaadin-grid-sort-column path="profession"></vaadin-grid-sort-column>
      </vaadin-grid>
    </vaadin-vertical-layout>
  `;
}
```

**Flow** — `GridDataProvider.java`

```java
Grid<Person> grid = new Grid<>();
grid.addColumn(Person::getFullName, "name").setSortable(true)
        .setHeader("Name");
grid.addColumn(Person::getProfession, "profession").setSortable(true)
        .setHeader("Profession");

// Create a data provider instance with a configurable filter, allowing
// the filter value to be set programmatically via setFilter().
ConfigurableFilterDataProvider<Person, Void, String> dataProvider =
        new PersonDataProvider().withConfigurableFilter();
grid.setItems(dataProvider);

TextField searchField = new TextField();
searchField.setWidth("50%");
searchField.setPlaceholder("Search");
searchField.setPrefixComponent(new Icon(VaadinIcon.SEARCH));
searchField.setValueChangeMode(ValueChangeMode.EAGER);
searchField.addValueChangeListener(event -> {
    dataProvider.setFilter(event.getValue());
});

add(searchField, grid);
```

**Flow** — `PersonDataProvider.java`

```java
public class PersonDataProvider
        extends AbstractBackEndDataProvider<Person, String> {
    private static final List<Person> DATABASE = new ArrayList<>(
            DataService.getPeople());

    @Override
    protected Stream<Person> fetchFromBackEnd(Query<Person, String> query) {
        // A real app should use a real database or a service to fetch,
        // filter and sort data.

        // SQL equivalent: SELECT ... FROM ...
        Stream<Person> stream = DATABASE.stream();

        // SQL equivalent: WHERE ...
        stream = stream.filter(createPredicate(query.getFilter()));

        // SQL equivalent: ORDER BY ...
        stream = stream.sorted(createComparator(query.getSortOrders()));

        // SQL equivalent: OFFSET ... LIMIT ...
        stream = stream.skip(query.getOffset()).limit(query.getLimit());

        return stream;
    }

    @Override
    protected int sizeInBackEnd(Query<Person, String> query) {
        // SQL equivalent: SELECT COUNT(*) FROM ...
        Stream<Person> stream = DATABASE.stream();

        // SQL equivalent: WHERE ...
        stream = stream.filter(createPredicate(query.getFilter()));

        return (int) stream.count();
    }

    private Predicate<Person> createPredicate(Optional<String> filter) {
        return (person) -> filter.map(searchTerm -> {
            if (person.getFullName().toLowerCase()
                    .contains(searchTerm.toLowerCase())) {
                return true;
            }
            if (person.getProfession().toLowerCase()
                    .contains(searchTerm.toLowerCase())) {
                return true;
            }

            return false;
        }).orElse(true);
    }

    private Comparator<Person> createComparator(
            List<QuerySortOrder> sortOrders) {
        return sortOrders.stream().map(sortOrder -> {
            Comparator<Person> comparator = switch (sortOrder.getSorted()) {
            case "name" -> Comparator.comparing(Person::getFullName);
            case "profession" -> Comparator.comparing(Person::getProfession);
            default -> (p0, p1) -> 0;
            };

            return sortOrder.getDirection().equals(SortDirection.ASCENDING)
                    ? comparator
                    : comparator.reversed();
        }).reduce((p0, p1) -> 0, Comparator::thenComparing);
    }
}
```

**React** — `grid-data-provider.tsx`

```tsx
function Example() {
  const searchTerm = useSignal('');

  // Create a data provider that calls a backend service with a
  // Spring Data pageable and the search term
  const dataProvider = useGridDataProvider(
    async (pageable) => await GridPersonService.list(pageable, searchTerm.value),
    // Providing the search term as a dependency will automatically
    // refresh the data provider when the search term changes
    [searchTerm.value]
  );

  return (
    <VerticalLayout theme="spacing">
      <TextField
        placeholder="Search"
        style={{ width: '50%' }}
        onValueChanged={(e) => {
          searchTerm.value = e.detail.value.trim();
        }}
      >
        <Icon slot="prefix" icon="vaadin:search" />
      </TextField>

      <Grid dataProvider={dataProvider}>
        <GridSortColumn path="fullName" header="Name" />
        <GridSortColumn path="profession" />
      </Grid>
    </VerticalLayout>
  );
```

**React** — `GridPersonService.java`

```java
@BrowserCallable
@AnonymousAllowed
public class GridPersonService {
    private final GridPersonRepository personRepository;

    public GridPersonService(GridPersonRepository personRepository) {
        this.personRepository = personRepository;
    }

    public @NonNull List<@NonNull Person> list(Pageable pageable,
            String filter) {
        // Implement your data fetching logic here
        // For this example, we're using a Spring Data repository
        return personRepository
                .findByFullNameContainingIgnoreCaseOrProfessionContainingIgnoreCase(
                        filter, filter, pageable);
    }
}
```

**React** — `GridPersonRepository.java`

```java
public interface GridPersonRepository extends JpaRepository<Person, Long> {
    List<Person> findByFullNameContainingIgnoreCaseOrProfessionContainingIgnoreCase(
            String fullName, String profession, Pageable pageable);
}
```

See the [Data Binding](https://vaadin.com/docs/latest/components/grid/data-binding.md#custom-data-providers) page for more information about data providers in Flow.

### <a id="lazy-column-rendering"></a>Lazy Column Rendering

Grids containing a large number of columns can sometimes exhibit performance issues. If many of the columns are typically outside the visible viewport, rendering performance can be optimized by using "lazy column rendering" mode.

This mode enables virtual scrolling horizontally. It renders body cells only when their corresponding columns are inside the visible viewport.

Lazy rendering should be used only with a large number of columns and performance is a high priority. For most use cases, though, the default "eager" mode is recommended.

When considering whether to use the "lazy" mode, keep the following factors in mind:

- Row Height

  When only a number of columns are visible at once, the height of a row can only be that of the highest cell currently visible on that row. Make sure each cell on a single row has the same height as all of the other cells on the row. Otherwise, users may notice jumpiness when horizontally scrolling the grid as lazily rendered cells with different heights are scrolled into view.

- Auto-Width Columns

  For columns that are initially outside the visible viewport, but still use auto-width, only the header content is taken into account when calculating the column width. This is because the body cells of the columns outside the viewport are not rendered initially.

- Screen Reader Compatibility

  Screen readers may not be able to associate the focused cells with the correct headers when only a subset of the body cells on a row is rendered.

- Keyboard Navigation

  Tabbing through focusable elements inside the grid body may not work as expected. This is because some of the columns that would include focusable elements in the body cells may be outside the visible viewport and thus not rendered.

- No Improvement If All Columns Visible

  The lazy column rendering mode can only improve the rendering performance when a significant portion of the columns are outside of the Grid’s visible viewport. It has no effect on Grids in which all columns are visible without horizontal scrolling.

**Lit** — `grid-lazy-column-rendering.ts`

```typescript
<vaadin-grid .items="${this.items}" column-rendering="lazy">
```

**Flow** — `GridLazyColumnRendering.java`

```java
grid.setColumnRendering(ColumnRendering.LAZY);
```

**React** — `grid-lazy-column-rendering.tsx`

```tsx
<Grid items={items.value} columnRendering="lazy">
  <GridColumn frozen renderer={indexColumnRenderer}></GridColumn>

  {[...Array(100).keys()].map((index) => (
    // Generate 100 columns
    <GridColumn key={index} header={`Col ${index}`} renderer={createColumnRenderer(index)} />
  ))}
</Grid>
```

## <a id="pagination"></a>Pagination

In general, there are two approaches to presenting large data sets in grids:

- **Infinite Scrolling** loads data incrementally as the user scrolls, providing a seamless browsing experience. It works well for exploratory browsing but can make locating specific items or returning to previous positions difficult.

- **Pagination** divides data into discrete pages with navigation controls, making it easier to track position within a dataset and locate specific items. It provides better user orientation but requires additional navigation steps to view more content. It can also cause confusion about whether the grid’s "Select All" action means a selection across *all pages* or only on *this particular page*.

By default, Vaadin Grid uses infinite scrolling with lazy loading, as demonstrated in the [Lazy Loading](#lazy-loading) section.

While Vaadin Grid doesn’t include built-in pagination controls, you can still implement manual pagination using other Vaadin components. The example below shows a custom pagination implementation that includes page size selection, navigation buttons, and page indicators.

The implementation uses a grid with `allRowsVisible=true` (to disable scrolling) and a data provider that returns only items relevant to the currently selected page.

**Lit** — `grid-manual-pagination.ts`

```typescript
<vaadin-grid .items="${this.gridItems}" all-rows-visible>
  <vaadin-grid-column
    header="Name"
    flex-grow="0"
    width="230px"
    ${columnBodyRenderer(this.nameRenderer, [])}
  ></vaadin-grid-column>
  <vaadin-grid-column path="email"></vaadin-grid-column>
  <vaadin-grid-column path="profession"></vaadin-grid-column>
</vaadin-grid>
<grid-pagination-controls
  style="width: 100%;"
  .totalItemCount="${this.itemsFilteredByTermCount}"
  @page-changed="${(_: CustomEvent) => {
    this.updateGridItems();
  }}"
></grid-pagination-controls>
```

**Flow** — `GridManualPagination.java`

```java
private final DataProvider<Person, String> pagingDataProvider = DataProvider
        .fromFilteringCallbacks(query -> {
            // We are implementing our own way of data pagination.
            // Unfortunately, the data provider contract requires these
            // two methods to be called during data fetch, otherwise
            // IllegalStateException is thrown
            // -> so we just call them but ignore their return values.
            query.getLimit();
            query.getOffset();

            // determine the offset and limit for the current page.
            var offset = paginationControls.calculateOffset();
            var limit = paginationControls.getPageSize();

            return dataSource.fetch(query.getFilter(), offset, limit);
        }, query -> {
            // Total count of filtered items
            var itemCount = dataSource.count(query.getFilter());

            // Recalculate page count here to avoid calling
            // dataSource.count twice
            paginationControls.recalculatePageCount(itemCount);

            var offset = paginationControls.calculateOffset();
            var limit = paginationControls.getPageSize();

            // Return the number of items for the current page, taking the
            // remaining items on the last page into consideration
            var remainingItemsCount = itemCount - offset;
            return Math.min(remainingItemsCount, limit);
        });

public GridManualPagination() {
    setPadding(false);

    Grid<Person> grid = new Grid<>(Person.class, false);

    grid.addColumn(createPersonRenderer()).setHeader("Name").setFlexGrow(0)
            .setWidth("230px");
    grid.addColumn(Person::getEmail).setHeader("Email");
    grid.addColumn(Person::getProfession).setHeader("Profession");

    grid.setAllRowsVisible(true); // this will prevent scrolling in the grid
    var dataProvider = pagingDataProvider.withConfigurableFilter();
    grid.setDataProvider(dataProvider);

    paginationControls
            .onPageChanged(() -> grid.getDataProvider().refreshAll());

    final TextField searchField = createSearchField();
    searchField.addValueChangeListener(e -> {
        // setFilter will refresh the data provider and trigger data
        // provider fetch / count queries. As a side effect, the pagination
        // controls will be updated.
        dataProvider.setFilter(e.getValue());
    });

    add(searchField, grid, paginationControls);
}
```

**React** — `grid-manual-pagination.tsx`

```tsx
function Example() {
  const allItems = useSignal<PersonEnhanced[]>([]);
  const currentSearchTerm = useSignal('');
  const currentPage = useSignal(1);
  const pageSize = useSignal(10);

  useEffect(() => {
    getPeople().then(({ people }) => {
      allItems.value = people.map((person) => ({
        ...person,
        displayName: `${person.firstName} ${person.lastName}`,
      }));
    });
  }, []);

  const matchesTerm = (value: string, term: string): boolean =>
    value.toLowerCase().includes(term.toLowerCase());

  const itemsFilteredByTerm = useComputed<PersonEnhanced[]>(() =>
    allItems.value.filter(
      ({ displayName, email, profession }) =>
        !currentSearchTerm.value ||
        matchesTerm(displayName, currentSearchTerm.value) ||
        matchesTerm(email, currentSearchTerm.value) ||
        matchesTerm(profession, currentSearchTerm.value)
    )
  );

  const itemsFilteredByTermCount = useComputed<number>(() => itemsFilteredByTerm.value.length);

  const gridItems = useComputed<PersonEnhanced[]>(() => {
    const offset = (currentPage.value - 1) * pageSize.value;
    return itemsFilteredByTerm.value.slice(offset, offset + pageSize.value);
  });

  const handleCurrentPageChanged = (newCurrentPage: number) => {
    currentPage.value = newCurrentPage;
  };
  const handlePageSizeChanged = (newPageSize: number) => {
    pageSize.value = newPageSize;
  };

  return (
    <VerticalLayout theme="spacing">
      <TextField
        placeholder="Search"
        style={{ width: '50%' }}
        onValueChanged={(e: TextFieldValueChangedEvent) => {
          currentSearchTerm.value = (e.detail.value || '').trim();
        }}
      >
        <Icon slot="prefix" icon="vaadin:search" />
      </TextField>
      <Grid items={gridItems.value} all-rows-visible>
        <GridColumn header="Name" flexGrow={0} width="230px" renderer={nameRenderer} />
        <GridColumn path="email" />
        <GridColumn path="profession" />
      </Grid>
      <GridPaginationControls
        totalItemCount={itemsFilteredByTermCount}
        currentPage={currentPage}
        onCurrentPageChanged={handleCurrentPageChanged}
        pageSize={pageSize}
        onPageSizeChanged={handlePageSizeChanged}
      />
    </VerticalLayout>
  );
}
```

## <a id="item-details"></a>Item Details

Item details are expandable content areas that can be displayed below the regular content of a row. They can be used to display more information about an item. By default, an item’s details are toggled by clicking on the item’s row. Try clicking on one of the rows in the example here. Notice that when you do, the row is expanded to show the person’s email address, telephone number, and home address. If you click on the row again, it’s collapsed back to a single line.

**Lit** — `grid-item-details.ts`

```typescript
@customElement('grid-item-details')
export class Example extends LitElement {
  protected override createRenderRoot() {
    const root = super.createRenderRoot();
    applyTheme(root);
    return root;
  }

  @state()
  private items: Person[] = [];

  @state()
  private detailsOpenedItem: Person[] = [];

  protected override async firstUpdated() {
    const { people } = await getPeople();
    this.items = people.map((person) => ({
      ...person,
      displayName: `${person.firstName} ${person.lastName}`,
    }));
  }

  protected override render() {
    return html`
      <vaadin-grid
        theme="row-stripes"
        .items="${this.items}"
        .detailsOpenedItems="${this.detailsOpenedItem}"
        @active-item-changed="${(event: GridActiveItemChangedEvent<Person>) => {
          const person = event.detail.value;
          this.detailsOpenedItem = person ? [person] : [];
        }}"
        ${gridRowDetailsRenderer<Person>(
          (person) => html`
            <vaadin-form-layout .responsiveSteps="${[{ minWidth: '0', columns: 3 }]}">
              <vaadin-text-field
                label="Email address"
                .value="${person.email}"
                colspan="3"
                readonly
              ></vaadin-text-field>
              <vaadin-text-field
                label="Phone number"
                .value="${person.address.phone}"
                colspan="3"
                readonly
              ></vaadin-text-field>
              <vaadin-text-field
                label="Street address"
                .value="${person.address.street}"
                colspan="3"
                readonly
              ></vaadin-text-field>
              <vaadin-text-field
                label="ZIP code"
                .value="${person.address.zip}"
                readonly
              ></vaadin-text-field>
              <vaadin-text-field
                label="City"
                .value="${person.address.city}"
                readonly
              ></vaadin-text-field>
              <vaadin-text-field
                label="State"
                .value="${person.address.state}"
                readonly
              ></vaadin-text-field>
            </vaadin-form-layout>
          `,
          []
        )}
      >
        <vaadin-grid-column path="displayName" header="Name"></vaadin-grid-column>
        <vaadin-grid-column path="profession"></vaadin-grid-column>
      </vaadin-grid>
    `;
  }
}
```

**Flow** — `GridItemDetails.java`

```java
Grid<Person> grid = new Grid<>(Person.class, false);
grid.addColumn(Person::getFullName).setHeader("Name");
grid.addColumn(Person::getProfession).setHeader("Profession");

grid.setItemDetailsRenderer(createPersonDetailsRenderer());

...

private static ComponentRenderer<PersonDetailsFormLayout, Person> createPersonDetailsRenderer() {
    return new ComponentRenderer<>(PersonDetailsFormLayout::new,
            PersonDetailsFormLayout::setPerson);
}

private static class PersonDetailsFormLayout extends FormLayout {
    private final TextField emailField = new TextField("Email address");
    private final TextField phoneField = new TextField("Phone number");
    private final TextField streetField = new TextField("Street address");
    private final TextField zipField = new TextField("ZIP code");
    private final TextField cityField = new TextField("City");
    private final TextField stateField = new TextField("State");

    public PersonDetailsFormLayout() {
        Stream.of(emailField, phoneField, streetField, zipField, cityField,
                stateField).forEach(field -> {
                    field.setReadOnly(true);
                    add(field);
                });

        setResponsiveSteps(new ResponsiveStep("0", 3));
        setColspan(emailField, 3);
        setColspan(phoneField, 3);
        setColspan(streetField, 3);
    }

    public void setPerson(Person person) {
        emailField.setValue(person.getEmail());
        phoneField.setValue(person.getAddress().getPhone());
        streetField.setValue(person.getAddress().getStreet());
        zipField.setValue(person.getAddress().getZip());
        cityField.setValue(person.getAddress().getCity());
        stateField.setValue(person.getAddress().getState());
    }
}
```

**React** — `grid-item-details.tsx`

```tsx
function detailsRenderer({ item: person }: { item: Person }) {
  return (
    <FormLayout responsiveSteps={[{ minWidth: '0', columns: 3 }]}>
      <TextField label="Email address" value={person.email} data-colspan="3" readonly />
      <TextField label="Phone number" value={person.address.phone} data-colspan="3" readonly />
      <TextField label="Street address" value={person.address.street} data-colspan="3" readonly />
      <TextField label="ZIP code" value={person.address.zip} readonly />
      <TextField label="City" value={person.address.city} readonly />
      <TextField label="State" value={person.address.state} readonly />
    </FormLayout>
  );
}

function Example() {
  const items = useSignal<Person[]>([]);
  const detailsOpenedItem = useSignal<Person[]>([]);

  useEffect(() => {
    getPeople().then(({ people }) => {
      items.value = people.map((person) => ({
        ...person,
        displayName: `${person.firstName} ${person.lastName}`,
      }));
    });
  }, []);

  return (
    <Grid
      theme="row-stripes"
      items={items.value}
      detailsOpenedItems={detailsOpenedItem.value}
      onActiveItemChanged={(event) => {
        const person = event.detail.value;
        detailsOpenedItem.value = person ? [person] : [];
      }}
      rowDetailsRenderer={detailsRenderer}
    >
      <GridColumn path="displayName" header="Name" />
      <GridColumn path="profession" />
    </Grid>
  );
}
```

The default toggle behavior can be replaced by programmatically toggling the details visibility, such as from a button click.

**Lit** — `grid-item-details-toggle.ts`

```typescript
@customElement('grid-item-details-toggle')
export class Example extends LitElement {
  protected override createRenderRoot() {
    const root = super.createRenderRoot();
    applyTheme(root);
    return root;
  }

  @state()
  private items: Person[] = [];

  @state()
  private detailsOpenedItems: Person[] = [];

  protected override async firstUpdated() {
    const { people } = await getPeople();
    this.items = people.map((person) => ({
      ...person,
      displayName: `${person.firstName} ${person.lastName}`,
    }));
  }

  protected override render() {
    return html`
      <vaadin-grid
        theme="row-stripes"
        .items="${this.items}"
        .detailsOpenedItems="${this.detailsOpenedItems}"
        ${gridRowDetailsRenderer<Person>(
          (person) => html`
            <vaadin-form-layout .responsiveSteps="${[{ minWidth: '0', columns: 3 }]}">
              <vaadin-text-field
                label="Email address"
                .value="${person.email}"
                colspan="3"
                readonly
              ></vaadin-text-field>
              <vaadin-text-field
                label="Phone number"
                .value="${person.address.phone}"
                colspan="3"
                readonly
              ></vaadin-text-field>
              <vaadin-text-field
                label="Street address"
                .value="${person.address.street}"
                colspan="3"
                readonly
              ></vaadin-text-field>
              <vaadin-text-field
                label="ZIP code"
                .value="${person.address.zip}"
                readonly
              ></vaadin-text-field>
              <vaadin-text-field
                label="City"
                .value="${person.address.city}"
                readonly
              ></vaadin-text-field>
              <vaadin-text-field
                label="State"
                .value="${person.address.state}"
                readonly
              ></vaadin-text-field>
            </vaadin-form-layout>
          `,
          []
        )}
      >
        <vaadin-grid-column
          width="80px"
          flex-grow="0"
          frozen
          ${columnBodyRenderer<Person>(
            (person, { detailsOpened }) => html`
              <vaadin-button
                theme="tertiary icon"
                aria-label="Toggle details"
                aria-expanded="${detailsOpened ? 'true' : 'false'}"
                @click="${() => {
                  this.detailsOpenedItems = detailsOpened
                    ? this.detailsOpenedItems.filter((p) => p !== person)
                    : [...this.detailsOpenedItems, person];
                }}"
              >
                <vaadin-icon
                  .icon="${detailsOpened ? 'vaadin:angle-down' : 'vaadin:angle-right'}"
                ></vaadin-icon>
              </vaadin-button>
            `,
            []
          )}
        ></vaadin-grid-column>
        <vaadin-grid-column path="displayName" header="Name"></vaadin-grid-column>
        <vaadin-grid-column path="profession"></vaadin-grid-column>
      </vaadin-grid>
    `;
  }
}
```

**Flow** — `GridItemDetailsToggle.java`

```java
Grid<Person> grid = new Grid<>(Person.class, false);
grid.addColumn(createToggleDetailsRenderer(grid)).setWidth("80px")
        .setFlexGrow(0).setFrozen(true);
grid.addColumn(Person::getFullName).setHeader("Name");
grid.addColumn(Person::getProfession).setHeader("Profession");

grid.setDetailsVisibleOnClick(false);
grid.setItemDetailsRenderer(createPersonDetailsRenderer());

...

private static Renderer<Person> createToggleDetailsRenderer(
        Grid<Person> grid) {

    return LitRenderer
            .<Person> of("""
                <vaadin-button
                    theme="tertiary icon"
                    aria-label="Toggle details"
                    aria-expanded="${model.detailsOpened ? 'true' : 'false'}"
                    @click="${handleClick}"
                >
                    <vaadin-icon
                    .icon="${model.detailsOpened ? 'vaadin:angle-down' : 'vaadin:angle-right'}"
                    ></vaadin-icon>
                </vaadin-button>
            """)
            .withFunction("handleClick",
                    person -> grid.setDetailsVisible(person,
                            !grid.isDetailsVisible(person)));
}
```

**React** — `grid-item-details-toggle.tsx`

```tsx
const items = useSignal<Person[]>([]);
const detailsOpenedItems = useSignal<Person[]>([]);

useEffect(() => {
  getPeople().then(({ people }) => {
    items.value = people.map((person) => ({
      ...person,
      displayName: `${person.firstName} ${person.lastName}`,
    }));
  });
}, []);

const detailsRenderer = useCallback(({ item: person }: { item: Person }) => {
  return (
    <FormLayout responsiveSteps={[{ minWidth: '0', columns: 3 }]}>
      <TextField label="Email address" value={person.email} data-colspan="3" readonly />
      <TextField label="Phone number" value={person.address.phone} data-colspan="3" readonly />
      <TextField label="Street address" value={person.address.street} data-colspan="3" readonly />
      <TextField label="ZIP code" value={person.address.zip} readonly />
      <TextField label="City" value={person.address.city} readonly />
      <TextField label="State" value={person.address.state} readonly />
    </FormLayout>
  );
}, []);

const toggleDetailsRenderer = useCallback(({ item: person }: { item: Person }) => {
  const isExpanded = detailsOpenedItems.value.includes(person);
  return (
    <Button
      theme="tertiary icon"
      aria-label="Toggle details"
      aria-expanded={isExpanded}
      onClick={() => {
        detailsOpenedItems.value = isExpanded
          ? detailsOpenedItems.value.filter((p) => p !== person)
          : [...detailsOpenedItems.value, person];
      }}
    >
      <Icon icon={isExpanded ? 'vaadin:angle-down' : 'vaadin:angle-right'} />
    </Button>
  );
}, []);

return (
  <Grid
    theme="row-stripes"
    items={items.value}
    detailsOpenedItems={detailsOpenedItems.value}
    rowDetailsRenderer={detailsRenderer}
  >
    <GridColumn width="80px" flexGrow={0} frozen renderer={toggleDetailsRenderer} />
    <GridColumn path="displayName" header="Name" />
    <GridColumn path="profession" />
  </Grid>
);
```

[Tooltips](#tooltips) can be used as a lightweight alternative to the item details panel.

## <a id="empty-state"></a>Empty State

When there’s no data available for the grid to display any rows, the space between the header and footer is left blank by default. You can use the empty state feature to provide a message or other content in this area to inform the user that there are no items to show.

**Lit** — `grid-empty-state.ts`

```typescript
<vaadin-grid>
  <vaadin-grid-column path="firstName"></vaadin-grid-column>
  <vaadin-grid-column path="lastName"></vaadin-grid-column>
  <vaadin-grid-column path="email"></vaadin-grid-column>
  <vaadin-grid-column path="profession"></vaadin-grid-column>

  <span slot="empty-state">No employees found.</span>
</vaadin-grid>
```

**Flow** — `GridEmptyState.java`

```java
grid.setEmptyStateText("No employees found.");
```

**React** — `grid-empty-state.tsx`

```tsx
<Grid>
  <GridColumn path="firstName" />
  <GridColumn path="lastName" />
  <GridColumn path="email" />
  <GridColumn path="profession" />

  <span slot="empty-state">No employees found.</span>
</Grid>
```

## <a id="context-menu"></a>Context Menu

You can use Context Menu to provide shortcuts for the user. It appears on a right-click by default. In a mobile browser, a long press opens the menu. In the example here, try right-clicking on one of the rows. You’ll notice a box appears with a list of choices: Edit the row, delete it, email the person, or call them. If this example were fully configured, the latter two would open the related application (i.e., the default email program or a telephone application).

Using a context menu shouldn’t be the only way of accomplishing a task, though. The same functionality needs to be accessible elsewhere in the UI. See the documentation page on [Context Menu](https://vaadin.com/docs/latest/components/context-menu.md) for more information.

**Lit** — `grid-context-menu.ts`

```typescript
private renderMenu: ContextMenuLitRenderer = (context, menu) => {
  const { sourceEvent } = context.detail as { sourceEvent: Event };
  const grid = menu.firstElementChild as Grid<Person>;

  const eventContext = grid.getEventContext(sourceEvent);
  const person = eventContext.item!;

  const clickHandler = (_action: string) => () => {
    // console.log(`${action}: ${person.firstName} ${person.lastName}`);
  };

  return html`
    <vaadin-context-menu-list-box>
      <vaadin-context-menu-item @click="${clickHandler('Edit')}">
        Edit
      </vaadin-context-menu-item>
      <vaadin-context-menu-item @click="${clickHandler('Delete')}">
        Delete
      </vaadin-context-menu-item>
      <hr />
      <vaadin-context-menu-item @click="${clickHandler('Email')}">
        Email (${person.email})
      </vaadin-context-menu-item>
      <vaadin-context-menu-item @click="${clickHandler('Call')}">
        Call (${person.address.phone})
      </vaadin-context-menu-item>
    </vaadin-context-menu-list-box>
  `;
};

protected override render() {
  return html`
    <vaadin-context-menu ${contextMenuRenderer(this.renderMenu, [])}>
      <vaadin-grid .items="${this.items}" @vaadin-contextmenu="${this.onContextMenu}">
        <vaadin-grid-column path="firstName"></vaadin-grid-column>
        <vaadin-grid-column path="lastName"></vaadin-grid-column>
        <vaadin-grid-column path="email"></vaadin-grid-column>
        <vaadin-grid-column path="profession"></vaadin-grid-column>
      </vaadin-grid>
    </vaadin-context-menu>
  `;
}

onContextMenu(e: MouseEvent) {
  // Prevent opening context menu on header row.
  if ((e.currentTarget as Grid).getEventContext(e).section !== 'body') {
    e.stopPropagation();
  }
}
```

**Flow** — `GridContextMenuExample.java`

```java
Grid<Person> grid = new Grid<>(Person.class, false);
grid.addColumn(Person::getFirstName).setHeader("First name");
grid.addColumn(Person::getLastName).setHeader("Last name");
grid.addColumn(Person::getEmail).setHeader("Email");
grid.addColumn(Person::getProfession).setHeader("Profession");

PersonContextMenu contextMenu = new PersonContextMenu(grid);

add(grid);

...

private static class PersonContextMenu extends GridContextMenu<Person> {
    public PersonContextMenu(Grid<Person> target) {
        super(target);

        addItem("Edit", e -> e.getItem().ifPresent(person -> {
            // System.out.printf("Edit: %s%n", person.getFullName());
        }));
        addItem("Delete", e -> e.getItem().ifPresent(person -> {
            // System.out.printf("Delete: %s%n", person.getFullName());
        }));

        addSeparator();

        GridMenuItem<Person> emailItem = addItem("Email",
                e -> e.getItem().ifPresent(person -> {
                    // System.out.printf("Email: %s%n",
                    // person.getFullName());
                }));
        GridMenuItem<Person> phoneItem = addItem("Call",
                e -> e.getItem().ifPresent(person -> {
                    // System.out.printf("Phone: %s%n",
                    // person.getFullName());
                }));

        setDynamicContentHandler(person -> {
            // Do not show context menu when header is clicked
            if (person == null)
                return false;
            emailItem
                    .setText(String.format("Email: %s", person.getEmail()));
            phoneItem.setText(String.format("Call: %s",
                    person.getAddress().getPhone()));
            return true;
        });
    }
}
```

**React** — `grid-context-menu.tsx`

```tsx
const renderMenu = ({
  context,
}: Readonly<{
  context: ContextMenuRendererContext;
  original: ContextMenuElement;
}>) => {
  if (!gridRef.current) {
    return null;
  }

  const { sourceEvent } = context.detail as { sourceEvent: Event };
  const grid = gridRef.current;

  const eventContext = grid.getEventContext(sourceEvent);
  const person = eventContext.item;

  const clickHandler = (action: string) => () => {
    console.log(`${action}: ${person.firstName} ${person.lastName}`);
  };

  return (
    <ContextMenuListBox>
      <ContextMenuItem onClick={clickHandler('Edit')}>Edit</ContextMenuItem>
      <ContextMenuItem onClick={clickHandler('Delete')}>Delete</ContextMenuItem>
      <hr />
      <ContextMenuItem onClick={clickHandler('Email')}>Email ({person.email})</ContextMenuItem>
      <ContextMenuItem onClick={clickHandler('Call')}>
        Call ({person.address.phone})
      </ContextMenuItem>
    </ContextMenuListBox>
  );
};

return (
  <ContextMenu renderer={renderMenu}>
    <Grid items={items.value} ref={gridRef}>
      <GridColumn path="firstName" />
      <GridColumn path="lastName" />
      <GridColumn path="email" />
      <GridColumn path="profession" />
    </Grid>
  </ContextMenu>
);
```

## <a id="tooltips"></a>Tooltips

Tooltips on cells can be useful in many situations: They can be used to give more details on the contents of a cell — if an [item details panel](#item-details) would be overkill or otherwise undesirable. They can show the full text of a cell if it’s too long to fit feasibly into the cell itself — if [wrapping the cell contents](https://vaadin.com/docs/latest/components/grid/styling.md#wrap-cell-content) is insufficient or otherwise undesirable. Or they can give textual explanations for non-text content, such as status icons.

In the example here, hold your mouse pointer over the birthday date for one of the rows. A tooltip should appear indicating the age of the person. Now hover over one of the status icons, an X or a checkmark. It’ll use Tooltips to interpret the meaning of the icons.

**Lit** — `grid-tooltip-generator.ts`

```typescript
private tooltipGenerator = (context: GridEventContext<Person>): string => {
  let text = '';

  const { column, item } = context;
  if (column && item) {
    switch (column.path) {
      case 'birthday':
        text = `Age: ${differenceInYears(Date.now(), parseISO(item.birthday))}`;
        break;
      case 'status':
        text = item.status;
        break;
      default:
        break;
    }
  }

  return text;
};

...

<vaadin-tooltip slot="tooltip" .generator="${this.tooltipGenerator}"></vaadin-tooltip>
```

**Flow** — `GridTooltipGenerator.java`

```java
grid.addColumn(person -> getFormattedPersonBirthday(person))
        .setTooltipGenerator(person -> "Age: " + getPersonAge(person))
        .setHeader("Birthday");
grid.addComponentColumn(person -> createStatusBadge(person.getStatus()))
        .setTooltipGenerator(person -> person.getStatus())
        .setHeader("Status");
```

**React** — `grid-tooltip-generator.tsx`

```tsx
const tooltipGenerator = (context: GridEventContext<Person>): string => {
  let text = '';

  const { column, item } = context;
  if (column && item) {
    switch (column.path) {
      case 'birthday':
        text = `Age: ${differenceInYears(Date.now(), parseISO(item.birthday))}`;
        break;
      case 'status':
        text = item.status;
        break;
      default:
        break;
    }
  }

  return text;
};

return (
  <Grid items={items.value}>
    <GridColumn path="firstName" />
    <GridColumn path="lastName" />
    <GridColumn path="birthday" />
    <GridColumn path="status" renderer={statusRenderer} />
    <Tooltip slot="tooltip" generator={tooltipGenerator} />
  </Grid>
);
```

### <a id="markdown"></a>Markdown (since V25.0)

Cell tooltips can be configured to render their text content as Markdown. This allows using simple formatting such as bold or italic text in the string returned by the tooltip generator. By default, this feature is inactive and the tooltip content is rendered as plain text.

**Flow**

```java
grid.setTooltipMarkdownEnabled(true);
```

**Lit**

```html
<vaadin-tooltip markdown slot="tooltip" .generator="${this.tooltipGenerator}"></vaadin-tooltip>
```

**React**

```tsx
<Tooltip markdown slot="tooltip" generator={tooltipGenerator} />
```

> **Note:** Using Markdown is discouraged if accessibility of the tooltip content is essential, as semantics of the rendered HTML content (headers, lists, …​) won’t be conveyed to assistive technologies.

See the [Tooltips](https://vaadin.com/docs/latest/components/tooltip.md) documentation page for details on tooltip configuration.

## <a id="cell-focus"></a>Cell Focus

Many of the explanations and examples above alluded to giving the focus to rows and cells. Cells can be focused by clicking on a cell, or with the keyboard. The following keyboard shortcuts are available with Grid:

| Keys                                         | Action                                                                    |
| -------------------------------------------- | ------------------------------------------------------------------------- |
| `Tab`                                        | Switches focus between sections of the grid (i.e., header, body, footer). |
| `Left`, `Up`, `Right`, and `Down` Arrow Keys | Moves focus between cells within a section of the grid.                   |
| `Page Up`                                    | Moves cell focus up by one page of visible rows.                          |
| `Page Down`                                  | Moves cell focus down by one page of visible rows.                        |
| `Home`                                       | Moves focus to the first cell in a row.                                   |
| `End`                                        | Moves focus to the last cell in a row.                                    |

The cell focus event can be used to be notified when the user changes focus between cells. By default, the focus outline is only visible when using keyboard navigation. For illustrative purposes, the example below also uses custom styles to show the focus outline when clicking on cells. Try clicking on a cell. Notice how the cell is highlighted and notice the information shown at the bottom, the information provided about the event.

**Lit** — `grid-cell-focus.ts`

```typescript
protected override render() {
  return html`
    <vaadin-grid
      class="force-focus-outline"
      .items="${this.items}"
      @cell-focus="${(e: GridCellFocusEvent<Person>) => {
        const eventContext = this.grid.getEventContext(e);
        const section = eventContext.section ?? 'Not available';
        const row = eventContext.index ?? 'Not available';
        const column = eventContext.column?.path ?? 'Not available';
        const person = eventContext.item;
        const fullName =
          person?.firstName && person?.lastName
            ? `${person.firstName} ${person.lastName}`
            : 'Not available';

        this.eventSummary = `Section: ${section}\nRow: ${row}\nColumn: ${column}\nPerson: ${fullName}`;
      }}"
    >
      <vaadin-grid-column path="firstName"></vaadin-grid-column>
      <vaadin-grid-column path="lastName"></vaadin-grid-column>
      <vaadin-grid-column path="email"></vaadin-grid-column>
      <vaadin-grid-column path="profession"></vaadin-grid-column>
    </vaadin-grid>
    <vaadin-text-area
      style="margin-top: var(--vaadin-gap-l); padding: 0;"
      label="Cell focus event information"
      readonly
      .value="${this.eventSummary}"
    ></vaadin-text-area>
  `;
}
```

**Flow** — `GridCellFocus.java`

```java
grid.addCellFocusListener(event -> {
    CellFocusEvent.GridSection section = event.getSection();
    String column = event.getColumn().map(Grid.Column::getKey)
            .orElse("Not available");
    String row = event.getItem()
            .map(value -> String.valueOf(people.indexOf(value)))
            .orElse("Not available");
    String fullName = event.getItem().map(Person::getFullName)
            .orElse("Not available");

    String eventSummary = String.format(
            "Section: %s%nRow: %s%nColumn: %s%nPerson: %s", section,
            row, column, fullName);

    textArea.setValue(eventSummary);
});
```

`grid-cell-focus.css`

```css
/* Add this to your global CSS, for example in: */
/* src/main/resources/META-INF/resources/styles.css */

vaadin-grid.force-focus-outline::part(focused-cell)::before {
    content: '';
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    pointer-events: none;
    box-shadow: inset 0 0 0 2px deepskyblue;
}

vaadin-grid.force-focus-outline::part(focused-cell)::after {
    outline: none;
}
```

**React** — `grid-cell-focus.tsx`

```tsx
import React, { useEffect, useRef } from 'react';
import { useSignal } from '@vaadin/hilla-react-signals';
import { Grid, type GridCellFocusEvent, type GridElement } from '@vaadin/react-components/Grid.js';
import { GridColumn } from '@vaadin/react-components/GridColumn.js';
import { TextArea } from '@vaadin/react-components/TextArea.js';
import { getPeople } from 'Frontend/demo/domain/DataService';
import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';

function Example() {
  const gridRef = useRef<GridElement>(null);
  const items = useSignal<Person[]>([]);
  const eventSummary = useSignal('');

  useEffect(() => {
    getPeople().then(({ people }) => {
      items.value = people;
    });
  }, []);

  const handleCellFocus = (event: GridCellFocusEvent<Person>) => {
    if (!gridRef.current) {
      return;
    }
    const eventContext = gridRef.current.getEventContext(event);
    const section = eventContext.section ?? 'Not available';
    const row = eventContext.index ?? 'Not available';
    const column = eventContext.column?.path ?? 'Not available';
    const person = eventContext.item;
    const fullName =
      person?.firstName && person?.lastName
        ? `${person.firstName} ${person.lastName}`
        : 'Not available';

    eventSummary.value = `Section: ${section}\nRow: ${row}\nColumn: ${column}\nPerson: ${fullName}`;
  };

  return (
    <>
      <Grid
        className="force-focus-outline"
        items={items.value}
        onCellFocus={handleCellFocus}
        ref={gridRef}
      >
        <GridColumn path="firstName" />
        <GridColumn path="lastName" />
        <GridColumn path="email" />
        <GridColumn path="profession" />
      </Grid>

      <TextArea
        style={{ marginTop: 'var(--vaadin-gap-l)', padding: '0' }}
        label="Cell focus event information"
        readonly
        value={eventSummary.value}
      />
    </>
  );
}
```

## <a id="programmatic-scrolling"></a>Programmatic Scrolling

Grid allows programmatically scrolling to a specific row or column. This can be useful for example to scroll to a new or updated item, or to initialize the scroll position on page load when the grid’s selected item is set through a URL parameter.

Scrolling to a row is done by using the `scrollToIndex` method with the index of the row to scroll to. In addition, the Flow Grid also supports scrolling to a specific item by using the `scrollToItem` method with the item to scroll to.

**Lit**

```typescript
const grid = document.querySelector('vaadin-grid');
grid.scrollToIndex(rowIndex);
```

**Flow**

```java
// Scroll by index
grid.scrollToIndex(rowIndex);
// Scroll by item reference
grid.scrollToItem(item);
```

**React**

```tsx
const gridRef = useRef<Grid>(null);

gridRef.current!.scrollToIndex(rowIndex);

<Grid ref={gridRef} ...>
  ...
</Grid>
```

Scrolling to a column is done by using the `scrollToColumn` method with either the column index, or a reference to a column instance. The column index is based on the visual order of columns in the grid, excluding any hidden columns.

**Lit**

```typescript
// Scroll by column index
const grid = document.querySelector('vaadin-grid');
grid.scrollToColumn(columnIndex);
// Scroll by column reference
const column = grid.querySelector('vaadin-grid-column[path="email"]');
grid.scrollToColumn(column);
```

**Flow**

```java
// Scroll by column index
grid.scrollToColumn(columnIndex);
// Scroll by column reference
var column = grid.addColumn(Person::getEmail).setHeader("Email");
grid.scrollToColumn(column);
```

**React**

```tsx
const gridRef = useRef<Grid>(null);
const columnRef = useRef<GridColumn>(null);

gridRef.current!.scrollToColumn(columnIndex);
gridRef.current!.scrollToColumn(columnRef.current!);

<Grid ref={gridRef} ...>
  <GridColumn ref={columnRef} path="email" header="Email" />
  ...
</Grid>
```

## <a id="internationalization-i18n"></a>Internationalization (i18n) (new in V25.3)

Grid provides an i18n API for customizing and translating the accessible names — the labels announced by screen readers — for its interactive controls: the Select All checkbox in a selection column header, the per-row selection checkboxes, and the column sorters. It also sets the label announced for the selection column header when the Select All checkbox is hidden.

| Key                    | Default                  | Description                                                                                                                                                                                            |
| ---------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `selectAll`            | `Select All`             | Accessible name for the Select All checkbox in the selection column header.                                                                                                                            |
| `selectAllUnavailable` | `Select All unavailable` | Accessible name for the selection column header cell when the Select All checkbox is hidden — for example, when a data provider is used or conditional selection is enabled.                           |
| `selectRow`            | `Select Row {rowHeader}` | Accessible name template for each row’s selection checkbox. The `{rowHeader}` placeholder is replaced with the row’s row-header cell text, or the 1-based row index when there’s no row-header column. |
| `sorter`               | `Sort by {column}`       | Accessible name template for column sorters. The `{column}` placeholder is replaced with the column’s header text.                                                                                     |

**Lit**

```typescript
grid.i18n = {
  selectAll: 'Select all rows',
  selectAllUnavailable: 'Select all unavailable',
  selectRow: 'Select row {rowHeader}',
  sorter: 'Sort by {column}',
};
```

**Flow**

```java
grid.setI18n(new GridI18n()
        .setSelectAll("Select all rows")
        .setSelectAllUnavailable("Select all unavailable")
        .setSelectRow("Select row {rowHeader}")
        .setSorter("Sort by {column}"));
```

**React**

```tsx
<Grid
  i18n={{
    selectAll: 'Select all rows',
    selectAllUnavailable: 'Select all unavailable',
    selectRow: 'Select row {rowHeader}',
    sorter: 'Sort by {column}',
  }}
>
  {/* ... */}
</Grid>
```

## <a id="related-components"></a>Related Components

| Component                                                           | Usage Recommendation                                                     |
| ------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| [CRUD](https://vaadin.com/docs/latest/components/crud.md)           | Component for creating, displaying, updating, and deleting tabular data. |
| [Grid Pro](https://vaadin.com/docs/latest/components/grid-pro.md)   | Component for showing and editing tabular data.                          |
| [Tree Grid](https://vaadin.com/docs/latest/components/tree-grid.md) | Component for showing hierarchical tabular data.                         |
| [List Box](https://vaadin.com/docs/latest/components/list-box.md)   | Lightweight component for lightweight, single-column lists.              |

`AC63AABF-4102-4C3E-9776-A09DDC04EF37`
