My Design System

Grid

Grid is a component for showing tabular data.

Open in a
new tab
@state()
private items: Person[] = [];

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

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

Content

A basic Grid uses plain text to display information in rows and columns. Rich content can be used to provide additional information in a more legible fashion. Components such as input fields and Button are also supported.

Open in a
new tab
@state()
private items?: Person[];

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

render() {
  return html`
    <vaadin-grid .items="${this.items}">
      <vaadin-grid-selection-column></vaadin-grid-selection-column>
      <vaadin-grid-column
        header="Employee"
        .renderer="${this.employeeRenderer}"
        flex-grow="0"
        auto-width
      ></vaadin-grid-column>
      <vaadin-grid-column path="profession" auto-width></vaadin-grid-column>
      <vaadin-grid-column
        header="Status"
        .renderer="${this.statusRenderer}"
        auto-width
      ></vaadin-grid-column>
    </vaadin-grid>
  `;
}

private employeeRenderer = (root: HTMLElement, _: HTMLElement, model: GridItemModel<Person>) => {
  const person = model.item;
  render(
    html`
      <vaadin-horizontal-layout style="align-items: center;" theme="spacing">
        <vaadin-avatar
          img="${person.pictureUrl}"
          name="${person.firstName} ${person.lastName}"
          alt="User avatar"
        ></vaadin-avatar>
        <vaadin-vertical-layout style="line-height: var(--lumo-line-height-m);">
          <span> ${person.firstName} ${person.lastName} </span>
          <span
            style="font-size: var(--lumo-font-size-s); color: var(--lumo-secondary-text-color);"
          >
            ${person.email}
          </span>
        </vaadin-vertical-layout>
      </vaadin-horizontal-layout>
    `,
    root
  );
};

private statusRenderer = (root: HTMLElement, _: HTMLElement, model: GridItemModel<Person>) => {
  const person = model.item;
  render(
    html`
      <span theme="badge ${person.status === 'Available' ? 'success' : 'error'}"
        >${person.status}</span
      >
    `,
    root
  );
};

Component Renderer vs Lit Renderer (Flow only)

As demonstrated in the example above, custom content can be rendered using component renderers or Lit renderers.

Component Renderer

Component renderers are easy to build but slow to render. For a given column, they generate a component for each item in the dataset. The rendered components are fully controllable on the server side.

For each rendered cell, Grid creates a corresponding component instance on the server side. A dataset of 100 items and 10 columns using component renderer adds up to 1000 components that need to be managed. The more components used in a component renderer, the more it will negatively affect performance.

Component renderers are very flexible and easy to use, but should be used with caution. They are better suited as editors since only a single row can be edited at a time. They can also be used for detail rows.

Lit Renderer

Lit renderers render quickly but require writing HTML. To use components with Lit renderers, you must use their HTML format. Lit templates are immutable, meaning the state of the components cannot be managed on the server side. The template can however have different representations depending on the state of the item.

The only data sent from the server, other than the template itself (which is only sent once), is the extra name property of each item.

Lit templates still enable event handling on the server side but you cannot, for example, disable or change the text of a button from the event handler. For those types of situations, use editors instead.

With Lit renderers, the server does not keep track of the components in each cell. It only manages the state of the item in each row. The client side does not need to wait for the server to send missing information about what needs to be rendered: it can use the template and stamp away all the information it needs.

See more in-depth documentation on using Lit renderers with Grid

Dynamic Height

Grid has a default height of 400 pixels. It becomes scrollable when its items 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, meaning that the grid will grow and shrink based on the row count.

Please note that this disables scrolling and should not be used for large data sets to avoid performance issues.

Open in a
new tab
<vaadin-grid .items="${this.invitedPeople}" all-rows-visible>
  <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" .renderer="${this.manageRenderer}"></vaadin-grid-column>
</vaadin-grid>

Selection

Selection is not enabled by default. Grid supports single and multi-select. The former allows the user to select exactly one item while the latter enables multiple items to be selected.

Single Selection Mode

In single selection mode, the user can select and deselect rows by clicking anywhere on the row.

Open in a
new tab
@state()
private items: Person[] = [];

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

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

render() {
  return html`
    <vaadin-grid
      .items="${this.items}"
      .selectedItems="${this.selectedItems}"
      @active-item-changed="${(e: GridActiveItemChangedEvent<Person>) => {
        const item = e.detail.value;
        this.selectedItems = item ? [item] : [];
      }}"
    >
      <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>
  `;
}

Multi-Select Mode

In multi-select mode, the user can use a checkbox column to select and deselect rows.

Open in a
new tab
@customElement('grid-multi-select-mode')
export class Example extends LitElement {
  protected createRenderRoot() {
    const root = super.createRenderRoot();
    // Apply custom theme (only supported if your app uses one)
    applyTheme(root);
    return root;
  }

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

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

  render() {
    return html`
      <vaadin-grid .items="${this.items}">
        <vaadin-grid-selection-column></vaadin-grid-selection-column>
        <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>
    `;
  }
}

Columns

Column alignment, freezing (fixed position), grouping, headers & footers, visibility, and width can be configured. Users can be allowed to resize and reorder columns.

Column Alignment

Three different column alignments are supported: left (default), center and right.

Right align is useful when comparing numeric values as it helps with readability and scannability. Tabular numbers (if the font offers them) or a monospace font could be used to further improve digit alignment.

Open in a
new tab
The inline `.renderer` function is encapsulated by the `guard` directive for performance reasons.
See the https://lit.dev/docs/templates/directives#guard[official Lit documentation] for more details.

<vaadin-grid .items="${this.items}">
  <vaadin-grid-column path="displayName" header="Name"></vaadin-grid-column>
  <vaadin-grid-column
    header="Due"
    .renderer="${guard(
      [],
      () => (root: HTMLElement) => render(html`<span>${this.randomDate()}</span>`, root)
    )}"
  ></vaadin-grid-column>
  <vaadin-grid-column
    header="Amount"
    text-align="end"
    .renderer="${guard([], () => (root: HTMLElement) => {
      render(
        html`<span style="font-variant-numeric: tabular-nums">${this.randomAmount()}</span>`,
        root
      );
    })}"
  ></vaadin-grid-column>
</vaadin-grid>

Column Freezing

Columns and column groups can be frozen to exclude them from scrolling a grid horizontally. This can be useful for keeping the most important columns always visible in a grid with a large number of columns.

Open in a
new tab
<vaadin-grid-column
  frozen
  header="Name"
  .renderer="${this.nameRenderer}"
  auto-width
  flex-grow="0"
></vaadin-grid-column>
Note
Freeze leftmost columns only

While it’s technically possible to freeze any column, this feature should primarily be used to freeze a certain number of columns starting with the leftmost one, leaving all remaining columns to the right unfrozen.

Column Grouping

It is possible to group columns together. Grouped columns share a common header and footer. Use this feature to better visualise and organise related or hierarchical data.

Open in a
new tab
@customElement('grid-column-grouping')
export class Example extends LitElement {
  protected createRenderRoot() {
    const root = super.createRenderRoot();
    // Apply custom theme (only supported if your app uses one)
    applyTheme(root);
    return root;
  }

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

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

  render() {
    return html`
      <vaadin-grid .items="${this.items}">
        <vaadin-grid-column-group header="Name">
          <vaadin-grid-column path="firstName"></vaadin-grid-column>
          <vaadin-grid-column path="lastName"></vaadin-grid-column>
        </vaadin-grid-column-group>
        <vaadin-grid-column-group header="Address">
          <vaadin-grid-column path="address.street"></vaadin-grid-column>
          <vaadin-grid-column path="address.city"></vaadin-grid-column>
          <vaadin-grid-column path="address.zip"></vaadin-grid-column>
          <vaadin-grid-column path="address.state"></vaadin-grid-column>
        </vaadin-grid-column-group>
      </vaadin-grid>
    `;
  }
}

Column Headers & Footers

Each column has its own customizable header and footer. A basic column header shows the name in plain text. Footers are empty and thus hidden by default. Both can contain rich content and components.

Open in a
new tab
@state()
private items: Person[] = [];

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

render() {
  return html`
    <vaadin-grid .items="${this.items}">
      <vaadin-grid-column
        header="Name"
        path="displayName"
        .footerRenderer="${this.displayNameFooterRenderer}"
      ></vaadin-grid-column>
      <vaadin-grid-column
        .renderer="${this.subscriberRenderer}"
        .headerRenderer="${this.subscriberHeaderRenderer}"
        .footerRenderer="${this.subscriberFooterRenderer}"
      ></vaadin-grid-column>
      <vaadin-grid-column
        path="membership"
        .headerRenderer="${this.membershipHeaderRenderer}"
        .footerRenderer="${this.membershipFooterRenderer}"
      ></vaadin-grid-column>
    </vaadin-grid>
  `;
}

displayNameFooterRenderer = (root: HTMLElement) => {
  render(html`<span>200 total members</span>`, root);
};

subscriberHeaderRenderer = (root: HTMLElement) => {
  render(
    html`
      <vaadin-horizontal-layout style="align-items: center;">
        <span>Subscriber</span>
        <vaadin-icon
          icon="vaadin:info-circle"
          title="Subscribers are paying customers"
          style="height: var(--lumo-font-size-m); color: var(--lumo-contrast-70pct);"
        ></vaadin-icon>
      </vaadin-horizontal-layout>
    `,
    root
  );
};

subscriberRenderer = (root: HTMLElement, _: HTMLElement, model: GridItemModel<Person>) => {
  const person = model.item;
  render(html`<span>${person.subscriber ? 'Yes' : 'No'}</span>`, root);
};

subscriberFooterRenderer = (root: HTMLElement) => {
  render(html`<span>102 subscribers</span>`, root);
};

membershipHeaderRenderer = (root: HTMLElement) => {
  render(
    html`
      <vaadin-horizontal-layout style="align-items: center;">
        <span>Membership</span>
        <vaadin-icon
          icon="vaadin:info-circle"
          title="Membership levels determines which features a client has access to"
          style="height: var(--lumo-font-size-m); color: var(--lumo-contrast-70pct);"
        ></vaadin-icon>
      </vaadin-horizontal-layout>
    `,
    root
  );
};

membershipFooterRenderer = (root: HTMLElement) => {
  render(html`<span>103 regular, 71 premium , 66 VIP</span>`, root);
};

Column Visibility

Columns and column groups can be hidden. You can provide the user with a menu for toggling column visibilities, for example, using Menu Bar.

Allowing the user to hide columns is useful when only a subset of the columns are relevant to their task, and if there is a large number of columns.

Open in a
new tab
@state()
private items: Person[] = [];

@state()
private contextMenuItems: (ContextMenuItem & { key: string })[] = [
  { text: 'First name', checked: true, key: 'firstName' },
  { text: 'Last name', checked: true, key: 'lastName' },
  { text: 'Email', checked: true, key: 'email' },
  { text: 'Phone', checked: true, key: 'phone' },
  { text: 'Profession', checked: true, key: 'profession' },
];

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

render() {
  return html`
    <vaadin-horizontal-layout style="align-items: baseline">
      <strong style="flex: 1;">Employees</strong>
      <vaadin-context-menu
        open-on="click"
        .items="${this.contextMenuItems}"
        @item-selected="${(e: ContextMenuItemSelectedEvent) => {
          const value = e.detail.value as ContextMenuItem & { key: string };
          this.contextMenuItems = this.contextMenuItems.map((item) =>
            item.key === value.key ? { ...item, checked: !value.checked } : item
          );
        }}"
      >
        <vaadin-button theme="tertiary">Show/Hide Columns</vaadin-button>
      </vaadin-context-menu>
    </vaadin-horizontal-layout>

    <vaadin-grid .items="${this.items}">
      <vaadin-grid-column
        path="firstName"
        .hidden="${!this.contextMenuItems[0].checked}"
      ></vaadin-grid-column>
      <vaadin-grid-column
        path="lastName"
        .hidden="${!this.contextMenuItems[1].checked}"
      ></vaadin-grid-column>
      <vaadin-grid-column
        path="email"
        .hidden="${!this.contextMenuItems[2].checked}"
      ></vaadin-grid-column>
      <vaadin-grid-column
        path="address.phone"
        .hidden="${!this.contextMenuItems[3].checked}"
      ></vaadin-grid-column>
      <vaadin-grid-column
        path="profession"
        .hidden="${!this.contextMenuItems[4].checked}"
      ></vaadin-grid-column>
    </vaadin-grid>
  `;
}

Column Reordering & Resizing

Enabling the user to reorder columns is useful when they wish to compare data that is not adjacent by default. Grouped columns can only be reordered within their group.

Resizing is helpful when a column’s content does not fit and gets cut off or varies in length.

Open in a
new tab
@customElement('grid-column-reordering-resizing')
export class Example extends LitElement {
  protected createRenderRoot() {
    const root = super.createRenderRoot();
    // Apply custom theme (only supported if your app uses one)
    applyTheme(root);
    return root;
  }

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

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

  render() {
    return html`
      <vaadin-grid .items="${this.items}" column-reordering-allowed>
        <vaadin-grid-column-group header="Name">
          <vaadin-grid-column path="firstName" resizable></vaadin-grid-column>
          <vaadin-grid-column path="lastName" resizable></vaadin-grid-column>
        </vaadin-grid-column-group>
        <vaadin-grid-column-group header="Address">
          <vaadin-grid-column path="address.street" resizable></vaadin-grid-column>
          <vaadin-grid-column path="address.city" resizable></vaadin-grid-column>
          <vaadin-grid-column path="address.zip" resizable></vaadin-grid-column>
          <vaadin-grid-column path="address.state" resizable></vaadin-grid-column>
        </vaadin-grid-column-group>
      </vaadin-grid>
    `;
  }
}

Column Width

All columns are the same width by default. You can set a specific width for any column, or allow the Grid to automatically set the width based on the contents.

Column widths can be fixed or non-fixed (default). Fixed width columns do not grow or shrink as the available space changes, while non-fixed width columns do.

In the following example, the first and last columns have fixed widths. The second column’s width is to set to be based on the content, while the third column takes up the remaining space.

Open in a
new tab
@customElement('grid-column-width')
export class Example extends LitElement {
  protected createRenderRoot() {
    const root = super.createRenderRoot();
    // Apply custom theme (only supported if your app uses one)
    applyTheme(root);
    return root;
  }

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

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

  render() {
    return html`
      <vaadin-split-layout>
        <vaadin-grid .items="${this.items}" style="width: 100%;">
          <vaadin-grid-selection-column></vaadin-grid-selection-column>
          <vaadin-grid-column path="firstName" width="7em" flex-grow="0"></vaadin-grid-column>
          <vaadin-grid-column path="profession" auto-width flex-grow="0"></vaadin-grid-column>
          <vaadin-grid-column path="email"></vaadin-grid-column>
          <vaadin-grid-column
            width="6em"
            flex-grow="0"
            header="Has Sub"
            .renderer="${this.subscriptionRenderer}"
          ></vaadin-grid-column>
        </vaadin-grid>
        <div></div>
      </vaadin-split-layout>
    `;
  }

  private subscriptionRenderer = (
    root: HTMLElement,
    _: HTMLElement,
    model: GridItemModel<Person>
  ) => {
    root.textContent = model.item.subscriber ? 'Yes' : 'No';
  };
}

Sorting

Any column can be made sortable. Enable sorting to allow the user to sort items alphabetically, numerically, by date, etc.

Open in a
new tab
<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>

You can also sort columns that contain rich and/or custom content by defining which property to sort by. For example, you can have a column containing a person’s profile picture, name and email sorted by the person’s last name.

Open in a
new tab
@state()
private items: Person[] = [];

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

render() {
  return html`
    <vaadin-grid .items="${this.items}">
      <vaadin-grid-sort-column
        header="Employee"
        path="lastName"
        .renderer="${this.employeeRenderer}"
      ></vaadin-grid-sort-column>
      <vaadin-grid-column
        .renderer="${this.birthdayRenderer}"
        .headerRenderer="${this.birthdayHeaderRenderer}"
      ></vaadin-grid-column>
    </vaadin-grid>
  `;
}

private employeeRenderer = (root: HTMLElement, _: HTMLElement, model: GridItemModel<Person>) => {
  const person = model.item;
  render(
    html`
      <vaadin-horizontal-layout style="align-items: center;" theme="spacing">
        <vaadin-avatar
          img="${person.pictureUrl}"
          name="${person.firstName} ${person.lastName}"
          alt="User avatar"
        ></vaadin-avatar>
        <vaadin-vertical-layout style="line-height: var(--lumo-line-height-m);">
          <span> ${person.firstName} ${person.lastName} </span>
          <span
            style="font-size: var(--lumo-font-size-s); color: var(--lumo-secondary-text-color);"
          >
            ${person.email}
          </span>
        </vaadin-vertical-layout>
      </vaadin-horizontal-layout>
    `,
    root
  );
};

private birthdayHeaderRenderer = (root: HTMLElement) => {
  render(html`<vaadin-grid-sorter path="birthday">Birthdate</vaadin-grid-sorter>`, root);
};

private birthdayRenderer = (root: HTMLElement, _: HTMLElement, model: GridItemModel<Person>) => {
  const person = model.item;
  const birthday = parseISO(person.birthday);
  render(
    html`
      <vaadin-vertical-layout style="line-height: var(--lumo-line-height-m);">
        <span> ${format(birthday, 'P')} </span>
        <span
          style="font-size: var(--lumo-font-size-s); color: var(--lumo-secondary-text-color);"
        >
          Age: ${differenceInYears(Date.now(), birthday)}
        </span>
      </vaadin-vertical-layout>
    `,
    root
  );
};

Sorting helps users find and analyze the data, so it’s generally recommended to enable it for all applicable columns, except in cases where the order of items is an essential part of the data itself (such as prioritized lists).

Filtering

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

Open in a
new tab
<vaadin-grid .items="${this.items}">
  <vaadin-grid-filter-column
    header="Name"
    path="displayName"
    .renderer="${this.nameRenderer}"
    flex-grow="0"
    width="230px"
  ></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>

Place filters outside the grid when:

  • The filter is based on multiple columns.

  • A bigger field or more complex filter UI is needed, which wouldn’t comfortably fit in a column.

Open in a
new tab
@state()
private filteredItems: PersonEnhanced[] = [];

private items: PersonEnhanced[] = [];

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

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 as string) || '').trim();
          const matchesTerm = (value: string) => {
            return value.toLowerCase().indexOf(searchTerm.toLowerCase()) >= 0;
          };

          this.filteredItems = this.items.filter(({ displayName, email, profession }) => {
            return (
              !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"
          .renderer="${this.nameRenderer}"
          flex-grow="0"
          width="230px"
        ></vaadin-grid-column>
        <vaadin-grid-column path="email"></vaadin-grid-column>
        <vaadin-grid-column path="profession"></vaadin-grid-column>
      </vaadin-grid>
    </vaadin-vertical-layout>
  `;
}

Item Details

Item details are expandable content areas that can be displayed below the regular content of a row, used to display more information about an item. By default, an item’s details are toggled by clicking on the item’s row.

Open in a
new tab

The inline .renderer function is encapsulated by the guard directive for performance reasons. See the official Lit documentation for more details.

@customElement('grid-item-details')
export class Example extends LitElement {
  protected createRenderRoot() {
    const root = super.createRenderRoot();
    // Apply custom theme (only supported if your app uses one)
    applyTheme(root);
    return root;
  }

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

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

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

  render() {
    return html`
      <vaadin-grid
        theme="row-stripes"
        .items="${this.items}"
        .detailsOpenedItems="${this.detailsOpenedItem as any}"
        @active-item-changed="${(e: GridActiveItemChangedEvent<Person>) =>
          (this.detailsOpenedItem = [e.detail.value])}"
        .rowDetailsRenderer="${guard(
          [],
          () => (root: HTMLElement, _: GridElement, model: GridItemModel<Person>) => {
            const person = model.item;

            render(
              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>`,
              root
            );
          }
        )}"
      >
        <vaadin-grid-column path="displayName" header="Name"></vaadin-grid-column>
        <vaadin-grid-column path="profession"></vaadin-grid-column>
      </vaadin-grid>
    `;
  }
}

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

Open in a
new tab

The inline .renderer function is encapsulated by the guard directive for performance reasons. See the official Lit documentation for more details.

@customElement('grid-item-details-toggle')
export class Example extends LitElement {
  protected createRenderRoot() {
    const root = super.createRenderRoot();
    // Apply custom theme (only supported if your app uses one)
    applyTheme(root);
    return root;
  }

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

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

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

  render() {
    return html`
      <vaadin-grid
        theme="row-stripes"
        .items="${this.items}"
        .detailsOpenedItems="${this.detailsOpenedItems as any}"
        .rowDetailsRenderer="${guard(
          [],
          () => (root: HTMLElement, _: GridElement, model: GridItemModel<Person>) => {
            const person = model.item;

            render(
              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>`,
              root
            );
          }
        )}"
      >
        <vaadin-grid-column path="displayName" header="Name"></vaadin-grid-column>
        <vaadin-grid-column path="profession"></vaadin-grid-column>
        <vaadin-grid-column
          .renderer="${guard(
            [],
            () => (root: HTMLElement, _: GridColumnElement, model: GridItemModel<Person>) => {
              const person = model.item;
              render(
                html`<vaadin-button
                  theme="tertiary"
                  @click="${() => {
                    const isOpened = this.detailsOpenedItems.includes(person);
                    this.detailsOpenedItems = isOpened
                      ? this.detailsOpenedItems.filter((p) => p != person)
                      : [...this.detailsOpenedItems, person];
                  }}"
                  >Toggle details
                </vaadin-button>`,
                root
              );
            }
          )}"
        ></vaadin-grid-column>
      </vaadin-grid>
    `;
  }
}

Context Menu

You can use Context Menu to provide shortcuts to the user. It appears on right (default) or left click. In a mobile browser, a long press opens the menu.

Please note that using a context menu should not be the only way of accomplishing a task. The same functionality needs to be accessible elsewhere in the UI as well.

See Context Menu for more information.

Open in a
new tab
The inline `.renderer` function is encapsulated by the `guard` directive for performance reasons.
See the https://lit.dev/docs/templates/directives#guard[official Lit documentation] for more details.

private contextMenuRenderer =
  () => (root: HTMLElement, elem: ContextMenuElement, context: ContextMenuRendererContext) => {
    const { sourceEvent } = context.detail as { sourceEvent: Event };
    const grid = elem.firstElementChild as GridElement<Person>;

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

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

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

render() {
  return html`
    <vaadin-context-menu .renderer="${guard([], this.contextMenuRenderer)}">
      <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 GridElement).getEventContext(e) as GridEventContext<Person>).section !==
    'body'
  ) {
    e.stopPropagation();
  }
}

Drag and Drop

Grid supports drag and drop, for example to reorder rows and to drag rows between grids.

Drop Mode

The drop mode of a grid determines where a drop can happen. Vaadin offers 4 different drop modes:

Drop ModeDescription

On Grid

Drops can occur on the grid as a whole, not on top of or between individual rows. Use this option when the order is unimportant.

Between

Drops can happen between rows. Use this mode when the order is important.

On Top

Drops can take place on top of rows. This is useful when creating relationships between items or moving an item into another item, for example placing a file inside a folder.

On Top or Between

Drops can occur on top of or between rows.

Row Reordering

You can use drag and drop to reorder rows.

Open in a
new tab
@customElement('grid-row-reordering')
export class Example extends LitElement {
  protected createRenderRoot() {
    const root = super.createRenderRoot();
    // Apply custom theme (only supported if your app uses one)
    applyTheme(root);
    return root;
  }

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

  @state()
  private draggedItem?: Person;

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

  render() {
    return html`
      <vaadin-grid
        .items="${this.items}"
        ?rows-draggable="${true}"
        drop-mode="between"
        @grid-dragstart="${(event: GridDragStartEvent<Person>) => {
          this.draggedItem = event.detail.draggedItems[0];
        }}"
        @grid-dragend="${() => {
          delete this.draggedItem;
        }}"
        @grid-drop="${(event: GridDropEvent<Person>) => {
          const { dropTargetItem, dropLocation } = event.detail;
          // only act when dropping on another item
          if (this.draggedItem && dropTargetItem !== this.draggedItem) {
            // remove the item from its previous position
            const draggedItemIndex = this.items.indexOf(this.draggedItem);
            this.items.splice(draggedItemIndex, 1);
            // re-insert the item at its new position
            const dropIndex =
              this.items.indexOf(dropTargetItem) + (dropLocation === 'below' ? 1 : 0);
            this.items.splice(dropIndex, 0, this.draggedItem);
            // re-assign the array to refresh the grid
            this.items = [...this.items];
          }
        }}"
      >
        <vaadin-grid-column
          header="Image"
          .renderer="${this.avatarRenderer}"
          flex-grow="0"
          auto-width
        ></vaadin-grid-column>
        <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>
    `;
  }

  private avatarRenderer = (root: HTMLElement, _: HTMLElement, model: GridItemModel<Person>) => {
    render(
      html`
        <vaadin-avatar
          img="${model.item.pictureUrl}"
          name="${model.item.firstName} ${model.item.lastName}"
          alt="User avatar"
        ></vaadin-avatar>
      `,
      root
    );
  };
}

Drag Rows Between Grids

Rows can be dragged from one grid to another, for example to move, copy or link items from different datasets.

Open in a
new tab
@state()
private draggedItem?: Person;

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

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

async firstUpdated() {
  const { people } = await getPeople({ count: 10 });
  this.grid1Items = people.slice(0, 5);
  this.grid2Items = people.slice(5);
}

private startDraggingItem = (event: GridDragStartEvent<Person>) => {
  this.draggedItem = event.detail.draggedItems[0];
};

private clearDraggedItem = () => {
  delete this.draggedItem;
};

render() {
  return html`
    <div class="grids-container">
      <vaadin-grid
        .items="${this.grid1Items}"
        ?rows-draggable="${true}"
        drop-mode="on-grid"
        @grid-dragstart="${this.startDraggingItem}"
        @grid-dragend="${this.clearDraggedItem}"
        @grid-drop="${() => {
          const draggedPerson = this.draggedItem as Person;
          const draggedItemIndex = this.grid2Items.indexOf(draggedPerson);
          if (draggedItemIndex >= 0) {
            // remove the item from its previous position
            this.grid2Items.splice(draggedItemIndex, 1);
            // re-assign the array to refresh the grid
            this.grid2Items = [...this.grid2Items];
            // re-assign the array to refresh the grid
            this.grid1Items = [...this.grid1Items, draggedPerson];
          }
        }}"
      >
        <vaadin-grid-column
          header="Full name"
          .renderer="${this.fullNameRenderer}"
        ></vaadin-grid-column>
        <vaadin-grid-column path="profession"></vaadin-grid-column>
      </vaadin-grid>

      <vaadin-grid
        .items="${this.grid2Items}"
        ?rows-draggable="${true}"
        drop-mode="on-grid"
        @grid-dragstart="${this.startDraggingItem}"
        @grid-dragend="${this.clearDraggedItem}"
        @grid-drop="${() => {
          const draggedPerson = this.draggedItem as Person;
          const draggedItemIndex = this.grid1Items.indexOf(draggedPerson);
          if (draggedItemIndex >= 0) {
            // remove the item from its previous position
            this.grid1Items.splice(draggedItemIndex, 1);
            // re-assign the array to refresh the grid
            this.grid1Items = [...this.grid1Items];
            // re-assign the array to refresh the grid
            this.grid2Items = [...this.grid2Items, draggedPerson];
          }
        }}"
      >
        <vaadin-grid-column
          header="Full name"
          .renderer="${this.fullNameRenderer}"
        ></vaadin-grid-column>
        <vaadin-grid-column path="profession"></vaadin-grid-column>
      </vaadin-grid>
    </div>
  `;
}

Drag and Drop Filters

Drag and drop filters determine which rows are draggable and which rows are valid drop targets, respectively. The filters function on a per row basis.

Open in a
new tab
@customElement('grid-drag-drop-filters')
export class Example extends LitElement {
  protected createRenderRoot() {
    const root = super.createRenderRoot();
    // Apply custom theme (only supported if your app uses one)
    applyTheme(root);
    return root;
  }

  @query('vaadin-grid')
  private grid!: GridElement;

  @state()
  private draggedItem?: EditablePerson;

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

  @state()
  private managers: EditablePerson[] = [];

  @state()
  private expandedItems: EditablePerson[] = [];

  async firstUpdated() {
    const { people } = await getPeople();
    this.items = people;
    this.managers = this.items.filter((item) => item.manager);
    // avoid using this method
    this.grid.clearCache();
  }

  private dataProvider = async (
    params: GridDataProviderParams<EditablePerson>,
    callback: GridDataProviderCallback<EditablePerson>
  ) => {
    const { page, pageSize, parentItem } = params;
    const startIndex = page * pageSize;
    const endIndex = startIndex + pageSize;

    /*
    We cannot change the underlying data in this demo so this dataProvider uses
    a local field to fetch its values. This allows us to keep a reference to the
    modified list instead of loading a new list every time the dataProvider gets
    called. In a real application, you should always access your data source 
    here and avoid using grid.clearCache() whenever possible.
    */
    const result = parentItem
      ? this.items.filter((item) => item.managerId === parentItem.id)
      : this.managers.slice(startIndex, endIndex);

    callback(result, result.length);
  };

  render() {
    return html`
      <vaadin-grid
        .dataProvider="${this.dataProvider}"
        .itemIdPath="${'id'}"
        .expandedItems="${this.expandedItems}"
        @expanded-items-changed="${(event: GridExpandedItemsChangedEvent<EditablePerson>) => {
          this.expandedItems = event.detail.value;
        }}"
        ?rows-draggable="${true}"
        drop-mode="on-top"
        @grid-dragstart="${(event: GridDragStartEvent<EditablePerson>) => {
          this.draggedItem = event.detail.draggedItems[0];
        }}"
        @grid-dragend="${() => {
          delete this.draggedItem;
        }}"
        @grid-drop="${(event: GridDropEvent<EditablePerson>) => {
          const manager = event.detail.dropTargetItem;
          if (this.draggedItem) {
            // in a real aplpication, when using a data provider, you should
            // change the persisted data instead of updating a field
            this.draggedItem.managerId = manager.id;
            // avoid using this method
            this.grid.clearCache();
          }
        }}"
        .dragFilter="${(model: GridItemModel<EditablePerson>) => {
          const item = model.item;
          return !item.manager; // only drag non-managers
        }}"
        .dropFilter="${(model: GridItemModel<EditablePerson>) => {
          const item = model.item;
          return (
            item.manager && // can only drop on a supervisor
            item.id !== this.draggedItem?.managerId // disallow dropping on the same manager
          );
        }}"
      >
        <vaadin-grid-tree-column
          path="firstName"
          item-has-children-path="manager"
        ></vaadin-grid-tree-column>
        <vaadin-grid-column path="lastName"></vaadin-grid-column>
        <vaadin-grid-column path="email"></vaadin-grid-column>
      </vaadin-grid>
    `;
  }
}

Inline Editing (Java Only)

Grid can be configured to allow inline editing. Editing can be either buffered and non-buffered. Buffered means changes must be explicitly committed, while non-buffered automatically commit changes on blur (when a field loses focus).

Buffered

Open in a
new tab
Grid<Person> grid = new Grid<>(Person.class, false);
Editor<Person> editor = grid.getEditor();

Grid.Column<Person> firstNameColumn = grid
        .addColumn(Person::getFirstName).setHeader("First name")
        .setWidth("120px").setFlexGrow(0);
Grid.Column<Person> lastNameColumn = grid.addColumn(Person::getLastName)
        .setHeader("Last name").setWidth("120px").setFlexGrow(0);
Grid.Column<Person> emailColumn = grid.addColumn(Person::getEmail)
        .setHeader("Email");
Grid.Column<Person> editColumn = grid.addComponentColumn(person -> {
    Button editButton = new Button("Edit");
    editButton.addClickListener(e -> {
        if (editor.isOpen())
            editor.cancel();
        grid.getEditor().editItem(person);
    });
    return editButton;
}).setWidth("150px").setFlexGrow(0);

Binder<Person> binder = new Binder<>(Person.class);
editor.setBinder(binder);
editor.setBuffered(true);

TextField firstNameField = new TextField();
firstNameField.setWidthFull();
binder.forField(firstNameField)
        .asRequired("First name must not be empty")
        .withStatusLabel(firstNameValidationMessage)
        .bind(Person::getFirstName, Person::setFirstName);
firstNameColumn.setEditorComponent(firstNameField);

TextField lastNameField = new TextField();
lastNameField.setWidthFull();
binder.forField(lastNameField).asRequired("Last name must not be empty")
        .withStatusLabel(lastNameValidationMessage)
        .bind(Person::getLastName, Person::setLastName);
lastNameColumn.setEditorComponent(lastNameField);

EmailField emailField = new EmailField();
emailField.setWidthFull();
binder.forField(emailField).asRequired("Email must not be empty")
        .withValidator(new EmailValidator(
                "Please enter a valid email address"))
        .withStatusLabel(emailValidationMessage)
        .bind(Person::getEmail, Person::setEmail);
emailColumn.setEditorComponent(emailField);

Button saveButton = new Button("Save", e -> editor.save());
Button cancelButton = new Button(VaadinIcon.CLOSE.create(),
        e -> editor.cancel());
cancelButton.addThemeVariants(ButtonVariant.LUMO_ICON,
        ButtonVariant.LUMO_ERROR);
HorizontalLayout actions = new HorizontalLayout(saveButton,
        cancelButton);
actions.setPadding(false);
editColumn.setEditorComponent(actions);

Non-Buffered

In the example below, double-click a row to start editing. Press Escape, or click on a different row to stop editing.

Open in a
new tab
Grid<Person> grid = new Grid<>(Person.class, false);
Grid.Column<Person> firstNameColumn = grid
        .addColumn(Person::getFirstName).setHeader("First name")
        .setWidth("120px").setFlexGrow(0);
Grid.Column<Person> lastNameColumn = grid.addColumn(Person::getLastName)
        .setHeader("Last name").setWidth("120px").setFlexGrow(0);
Grid.Column<Person> emailColumn = grid.addColumn(Person::getEmail)
        .setHeader("Email");

Binder<Person> binder = new Binder<>(Person.class);
Editor<Person> editor = grid.getEditor();
editor.setBinder(binder);

TextField firstNameField = new TextField();
firstNameField.setWidthFull();
addCloseHandler(firstNameField, editor);
binder.forField(firstNameField)
        .asRequired("First name must not be empty")
        .withStatusLabel(firstNameValidationMessage)
        .bind(Person::getFirstName, Person::setFirstName);
firstNameColumn.setEditorComponent(firstNameField);

TextField lastNameField = new TextField();
lastNameField.setWidthFull();
addCloseHandler(lastNameField, editor);
binder.forField(lastNameField).asRequired("Last name must not be empty")
        .withStatusLabel(lastNameValidationMessage)
        .bind(Person::getLastName, Person::setLastName);
lastNameColumn.setEditorComponent(lastNameField);

EmailField emailField = new EmailField();
emailField.setWidthFull();
addCloseHandler(emailField, editor);
binder.forField(emailField).asRequired("Email must not be empty")
        .withValidator(new EmailValidator(
                "Please enter a valid email address"))
        .withStatusLabel(emailValidationMessage)
        .bind(Person::getEmail, Person::setEmail);
emailColumn.setEditorComponent(emailField);

grid.addItemDoubleClickListener(e -> {
    editor.editItem(e.getItem());
    Component editorComponent = e.getColumn().getEditorComponent();
    if (editorComponent instanceof Focusable) {
        ((Focusable) editorComponent).focus();
    }
});

Alternatively, use Grid Pro for more streamlined inline-editing, or CRUD for editing in a separate side panel or dialog.

Styling Rows and Columns

You can style individual cells based on the data, for example, to highlight changes or important information.

The Java Flow API currently only allows to style the whole row, but not individual cells.

Open in a
new tab
interface PersonWithRating extends Person {
  customerRating: number;
}

@customElement('grid-styling')
export class Example extends LitElement {
  protected createRenderRoot() {
    const root = super.createRenderRoot();
    // Apply custom theme (only supported if your app uses one)
    applyTheme(root);
    return root;
  }

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

  private ratingFormatter = new Intl.NumberFormat('en-US', {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2,
  });

  async firstUpdated() {
    const { people } = await getPeople();
    this.items = people.map((person) => ({ ...person, customerRating: Math.random() * 10 }));
  }

  render() {
    return html`
      <vaadin-grid .items="${this.items}" .cellClassNameGenerator="${this.cellClassNameGenerator}">
        <vaadin-grid-column path="firstName"></vaadin-grid-column>
        <vaadin-grid-column path="lastName"></vaadin-grid-column>
        <vaadin-grid-column path="profession"></vaadin-grid-column>
        <vaadin-grid-column
          header="Customer rating (0-10)"
          .renderer="${this.ratingRenderer}"
        ></vaadin-grid-column>
      </vaadin-grid>
    `;
  }

  private ratingRenderer = (
    root: HTMLElement,
    _column?: GridColumnElement,
    model?: GridItemModel<PersonWithRating>
  ) => {
    const item = model?.item;
    const rating = item ? this.ratingFormatter.format(item.customerRating) : '';
    render(html` <span>${rating}</span> `, root);
  };

  private cellClassNameGenerator(
    column: GridColumnElement,
    model: GridItemModel<PersonWithRating>
  ) {
    const item = model.item;
    let classes = '';
    // make the customer rating column bold
    if (column.header?.startsWith('Customer rating')) {
      classes += ' font-weight-bold';
    }
    // add high-rating class to customer ratings of 8 or higher
    if (item.customerRating >= 8.0) {
      classes += ' high-rating';
      // add low-rating class to customer ratings of 4 or lower
    } else if (item.customerRating <= 4.0) {
      classes += ' low-rating';
    }
    return classes;
  }
}

Theme Variants

Grid variants can reduce the white space inside the grid, adjust border and row highlight visibility, and control cell content overflow behavior.

Variants can be combined together freely.

Compact

The compact theme variant makes a grid more dense by reducing the header and row heights, as well as the spacing between columns.

It is useful for displaying more information on-screen without having to scroll. It can also help improve scannability and comparability between rows.

Open in a
new tab
<vaadin-grid .items="${this.items}" theme="compact">
  <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>

No Border

The no-border theme variant removes the outer border of the grid.

Open in a
new tab
<vaadin-grid .items="${this.items}" theme="no-border">
  <vaadin-grid-column
    header="Image"
    .renderer="${this.avatarRenderer}"
    flex-grow="0"
    auto-width
  ></vaadin-grid-column>
  <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>

No Row Border

This theme variant removes the horizontal row borders. It is best suited for small datasets. Parsing larger sets may be difficult unless paired with the row-stripes theme variant.

Open in a
new tab
<vaadin-grid .items="${this.items}" theme="no-row-borders">
  <vaadin-grid-column
    header="Image"
    .renderer="${this.avatarRenderer}"
    flex-grow="0"
    auto-width
  ></vaadin-grid-column>
  <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 Borders

You can add vertical borders between columns by using the column-borders theme variant. Data sets with a lot of columns packed tightly together, or where content gets truncated, can benefit from the additional separation that vertical borders bring.

Open in a
new tab
<vaadin-grid .items="${this.items}" theme="column-borders">
  <vaadin-grid-column
    header="Image"
    .renderer="${this.avatarRenderer}"
    flex-grow="0"
    auto-width
  ></vaadin-grid-column>
  <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>

Row Stripes

The row-stripes theme produces a background color for every other row. It can have a positive effect on scannability.

Open in a
new tab
<vaadin-grid .items="${this.items}" theme="row-stripes">
  <vaadin-grid-column
    header="Image"
    .renderer="${this.avatarRenderer}"
    flex-grow="0"
    auto-width
  ></vaadin-grid-column>
  <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>

Wrap Cell Content

Overflowing cell content is clipped or truncated by default. This variant makes the content wrap instead.

Open in a
new tab
@customElement('grid-wrap-cell-content')
export class Example extends LitElement {
  protected createRenderRoot() {
    const root = super.createRenderRoot();
    // Apply custom theme (only supported if your app uses one)
    applyTheme(root);
    return root;
  }

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

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

  render() {
    return html`
      <vaadin-grid .items="${this.items}" theme="wrap-cell-content">
        <vaadin-grid-column
          header="Image"
          .renderer="${this.avatarRenderer}"
          flex-grow="0"
          auto-width
        ></vaadin-grid-column>
        <vaadin-grid-column path="firstName"></vaadin-grid-column>
        <vaadin-grid-column path="lastName"></vaadin-grid-column>
        <vaadin-grid-column
          header="Address"
          .renderer="${this.addressRenderer}"
        ></vaadin-grid-column>
      </vaadin-grid>
    `;
  }

  private avatarRenderer = (root: HTMLElement, _: HTMLElement, model: GridItemModel<Person>) => {
    render(
      html`
        <vaadin-avatar
          img="${model.item.pictureUrl}"
          name="${model.item.firstName} ${model.item.lastName}"
          alt="User avatar"
        ></vaadin-avatar>
      `,
      root
    );
  };

  private addressRenderer = (root: HTMLElement, _: HTMLElement, model: GridItemModel<Person>) => {
    const item = model.item;
    render(
      html`
        <span
          >${item.address.street} ${item.address.city} ${item.address.zip}
          ${item.address.state}</span
        >
      `,
      root
    );
  };
}

Cell Focus

Cells can be focused by clicking on a cell or with the keyboard.

The following keyboard shortcuts are available:

Tab

Switches focus between sections of the grid (header, body, footer)

, , ,

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 get notified when the user changes focus between cells.

By default, the focus outline is only visible when using keyboard navigation. For demonstration purposes, the example below uses custom styles to also show the focus outline when clicking on cells.

Open in a
new tab
render() {
  return html`
    <vaadin-grid
      theme="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 != undefined ? 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>
    <div>
      <vaadin-text-area
        label="Cell focus event information"
        readonly
        .value="${this.eventSummary}"
      ></vaadin-text-area>
    </div>
  `;
}
ComponentUsage recommendations

CRUD

Component for creating, displaying, updating and deleting tabular data.

Grid Pro

Component for showing and editing tabular data.

Tree Grid

Component for showing hierarchical tabular data.

List Box

Lightweight component for lightweight, single-column lists.