Grid
- Usage
- Flow Usage
- Content
- Dynamic Height
- Selection
- Columns
- Sorting
- Filtering
- Lazy Loading
- Item Details
- Context Menu
- Tooltips
- Drag and Drop
- Inline Editing (Java Only)
- Styling Rows & Columns
- Theme Variants
- Cell Focus
- Related Components
Grid is a component for showing tabular data.
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>
`;
}
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.
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-selection-column></vaadin-grid-selection-column>
<vaadin-grid-column
header="Employee"
flex-grow="0"
auto-width
${columnBodyRenderer(this.employeeRenderer, [])}
></vaadin-grid-column>
<vaadin-grid-column path="profession" auto-width></vaadin-grid-column>
<vaadin-grid-column
header="Status"
auto-width
${columnBodyRenderer(this.statusRenderer, [])}
></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}"
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>
`;
private statusRenderer: GridColumnBodyLitRenderer<Person> = ({ status }) => html`
<span theme="badge ${status === 'Available' ? 'success' : 'error'}">${status}</span>
`;
Component Renderer vs. Lit Renderer (Flow Only)
As demonstrated in the previous example, 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 produce up to 1,000 components that need to be managed. The more components you use in a component renderer, the greater the impact on performance.
Component renderers are very flexible and easy to use, but should be used with caution. They’re 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 will render quickly, but require you to write HTML. To use components with Lit renderers, you need to use their HTML format. Lit templates are immutable, meaning the state of the components can’t be managed on the server side. However, the template can have different representations, depending on the state of the item.
The only data sent from the server, other than the template itself — which is sent only once — is the extra name property of each item.
Lit templates still enable event handling on the server side. However, you can’t, for example, disable or change the text of a button from the event handler. For such situations, use editors instead.
With Lit renderers, the server doesn’t keep track of the components in each cell. It only manages the state of the item in each row. The client side doesn’t have to wait for the server to send missing information about what needs to be rendered. It can use the template to render all of the cells it needs.
For more in-depth information, see Using Lit Renderers with Grid.
Wrap Cell Content
Overflowing cell content is clipped or truncated by default. This variant makes the content wrap instead.
new tab
@customElement('grid-wrap-cell-content')
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}" theme="wrap-cell-content">
<vaadin-grid-column
header="Image"
flex-grow="0"
auto-width
${columnBodyRenderer(this.avatarRenderer, [])}
></vaadin-grid-column>
<vaadin-grid-column path="firstName"></vaadin-grid-column>
<vaadin-grid-column path="lastName"></vaadin-grid-column>
<vaadin-grid-column
header="Address"
${columnBodyRenderer(this.addressRenderer, [])}
></vaadin-grid-column>
</vaadin-grid>
`;
}
private avatarRenderer: GridColumnBodyLitRenderer<Person> = (person) => html`
<vaadin-avatar
img="${person.pictureUrl}"
name="${person.firstName} ${person.lastName}"
alt="User avatar"
></vaadin-avatar>
`;
private addressRenderer: GridColumnBodyLitRenderer<Person> = ({ address }) => html`
<span>${address.street} ${address.city} ${address.zip} ${address.state}</span>
`;
}
Tooltips can also be used to display content that doesn’t fit into the cell.
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. The grid grows and shrinks based on the row count. This disables scrolling and shouldn’t be used for large data sets to avoid performance issues.
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
Selection isn’t enabled by default. Grid supports single- and multi-select. Single-select allows the user to select only one item, while multi-select permits 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.
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 rows.
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>
`;
}
}
Columns
Column alignment, freezing (i.e., fixed positioning), grouping, headers and footers, visibility, and width can be configured. Users can be permitted to resize and reorder columns.
Column Alignment
Three different column alignments are supported: left (default), center, and right.
Right alignment 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.
new tab
<vaadin-grid .items="${this.items}">
<vaadin-grid-column path="displayName" header="Name"></vaadin-grid-column>
<vaadin-grid-column
header="Due"
${columnBodyRenderer(() => html`<span>${this.randomDate()}</span>`, [])}
></vaadin-grid-column>
<vaadin-grid-column
header="Amount"
text-align="end"
${columnBodyRenderer(
() => html`
<span style="font-variant-numeric: tabular-nums">${this.randomAmount()}</span>
`,
[]
)}
></vaadin-grid-column>
</vaadin-grid>
Column Freezing
Columns and column groups can be frozen — made sticky — to exclude them from scrolling a grid, horizontally. This can be useful for keeping the most important columns always visible in a grid with many columns. Freezing columns at the end of the grid is useful, for example, for keeping row actions always visible.
new tab
<vaadin-grid-column
frozen
header="Name"
auto-width
flex-grow="0"
${columnBodyRenderer<Person>(
(person) => html`${person.firstName} ${person.lastName}`,
[]
)}
></vaadin-grid-column>
<vaadin-grid-column
frozen-to-end
auto-width
flex-grow="0"
${columnBodyRenderer(
() => html`<vaadin-button theme="tertiary-inline">Edit</vaadin-button>`,
[]
)}
></vaadin-grid-column>
Although it’s technically possible to freeze any column, it should be used primarily to freeze columns at the start or end of the grid, leaving the remaining columns unfrozen.
Column Grouping
It’s possible to group columns together. Grouped columns share a common header and footer. Use this feature to better visualize and organize related or hierarchical data.
new tab
@customElement('grid-column-grouping')
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-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.
new tab
@state()
private items: 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 .items="${this.items}">
<vaadin-grid-column
header="Name"
path="displayName"
${columnFooterRenderer(() => html`<span>200 total members</span>`, [])}
></vaadin-grid-column>
<vaadin-grid-column
${columnHeaderRenderer(this.subscriberHeaderRenderer, [])}
${columnBodyRenderer(this.subscriberRenderer, [])}
${columnFooterRenderer(() => html`<span>102 subscribers</span>`, [])}
></vaadin-grid-column>
<vaadin-grid-column
path="membership"
${columnHeaderRenderer(this.membershipHeaderRenderer, [])}
${columnFooterRenderer(() => html`<span>103 regular, 71 premium , 66 VIP</span>`, [])}
></vaadin-grid-column>
</vaadin-grid>
`;
}
private subscriberHeaderRenderer = () => 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>
`;
private subscriberRenderer: GridColumnBodyLitRenderer<Person> = (person) =>
html`<span>${person.subscriber ? 'Yes' : 'No'}</span>`;
private membershipHeaderRenderer = () => 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>
`;
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 is relevant to their task, and if there are plenty of columns.
new tab
@state()
private items: Person[] = [];
@state()
private contextMenuItems: Array<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' },
];
protected override async firstUpdated() {
const { people } = await getPeople();
this.items = people;
}
protected override 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