Documentation

Documentation versions (currently viewingVaadin 24)

Grid

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

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

Some of the more complex features of this component are described on separate tabs:

Open in a
new tab
@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>
  `;
}
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);

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.

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"
    ${columnBodyRenderer(this.manageRenderer, [])}
  ></vaadin-grid-column>
</vaadin-grid>

Selection

Grid supports single and multi-select modes. Neither is enabled by default.

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[] = [];

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

protected override 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 more than one row — not necessarily contiguous rows. Or the user can select all rows by clicking on the checkbox in the header row — and then un-check the ones they don’t want to be selected, rather than check many, individually.

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

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

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

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

In addition to selecting rows individually, a range of rows can be selected by dragging from one selection checkbox to another, if enabled:

<vaadin-grid .items="${this.items}">
  <vaadin-grid-selection-column drag-select></vaadin-grid-selection-column>
        ...
</vaadin-grid>

Selection Modes in Flow

Each selection mode is represented by a GridSelectionModel, accessible through the getSelectionModel() method, which can be cast that to the specific selection model type, SingleSelectionModel or MultiSelectionModel. These interfaces provide selection mode specific APIs for configuration and selection events.

To use Grid with Binder in Flow, you can use asSingleSelect() or asMultiSelect(), depending on the currently defined selection mode. Both methods return interfaces that implement the HasValue interface for use with Binder.

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.

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>

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.

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

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.

Open in a
new tab
@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`
  <vaadin-horizontal-layout style="align-items: center;" theme="spacing">
    <vaadin-avatar
      img="${person.pictureUrl}"
      name="${person.firstName} ${person.lastName}"
    ></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>
`;

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

private birthdayRenderer: GridColumnBodyLitRenderer<Person> = (person) => {
  const birthday = parseISO(person.birthday);
  return 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>
  `;
};

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.

Filtering

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.

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

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.

Open in a
new tab
@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>
  `;
}

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.

Open in a
new tab
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;
  result = result.slice(page * pageSize, 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>
  `;
}

To learn more about data providers in Flow, see the Binding Items to Components documentation page.

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.

Open in a
new tab
<vaadin-grid .items="${this.items}" column-rendering="lazy">

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.

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

  @state()
  private items: 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>
    `;
  }
}

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

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

  @state()
  private items: 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 path="displayName" header="Name"></vaadin-grid-column>
        <vaadin-grid-column path="profession"></vaadin-grid-column>
        <vaadin-grid-column
          ${columnBodyRenderer<Person>(
            (person) => 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>
            `,
            []
          )}
        ></vaadin-grid-column>
      </vaadin-grid>
    `;
  }
}

Tooltips can be used as a lightweight alternative to the item details panel.

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 for more information.

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

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();
  }
}

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

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

See the Tooltips documentation page for details on tooltip configuration.

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.

Open in a
new tab
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>
    <div>
      <vaadin-text-area
        label="Cell focus event information"
        readonly
        .value="${this.eventSummary}"
      ></vaadin-text-area>
    </div>
  `;
}
Component Usage Recommendation

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.

AC63AABF-4102-4C3E-9776-A09DDC04EF37