Docs

Documentation versions (currently viewingVaadin 24)

Grid

Column alignment, freezing (i.e. fixed positioning), grouping, headers and 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, which is the default; center; and right.

Right alignment is useful when comparing numeric values, as it helps with readability and scannability. Notice how the Amount column in the example here aligns the euro values to the right. Those values would be difficult to scan visually if they were left aligned or centered. Tabular numbers — if the font offers them — or a monospace font could be used to further improve digit alignment.

Open in a
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 horizontally scrolling a grid. This can be useful for keeping the most important columns always visible in a grid that contains so many columns they might otherwise scroll out of view. Freezing columns at the end of the grid is useful, for example, for keeping row actions always visible.

In the example here, try scrolling the data to the right and back left. Notice that the name of each person is stationary so that you can see easily which data relates to which person as you scroll. The Edit button at the end also remains in place so that it’s available at any point while scrolling.

Open in a
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. They share a common header and footer. Use this feature to better visualize and organize related or hierarchical data.

In the example below, the first and last names are grouped under Name. Whereas, the street address, the city, and so forth are grouped under Address.

Open in a
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 a customizable header and footer. A basic column header shows the name in plain text. Footers are empty by default and therefore hidden. However, if you add content for the footer, it becomes visible. Incidentally, both the header and footer can contain rich content and components — notice the header and footer in the example here.

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

When you want, 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.

In the example here, notice that the email addresses and telephone numbers are not fully visible because the data doesn’t wrap and won’t fit in the width provided. Now click on the Show/Hide Columns button to see a menu of choices to reduce the number of columns. Notice that all columns are checked. De-select the First Name and then the Profession column. That should allow the email addresses and telephone numbers to be fully visible. Incidentally, if you don’t deselect any columns, you can still right-click on an email address to copy it: You’ll still get the whole address, even if it’s not visible.

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

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

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 item = e.detail.value;
          item.checked = !item.checked;
          this.contextMenuItems = [...this.contextMenuItems];
        }}"
      >
        <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 want to compare data that isn’t adjacent by default. Grouped columns can only be reordered within their group. Resizing is helpful when a column’s content doesn’t fit and is cut off or varies in length.

Instead of hiding columns as in the earlier example, in the example here the user can resize a truncated column to be able to read it, fully. Try doing that. Hover your mouse pointer over the space on the header row between the Street and City columns, until the mouse pointer changes to a resizing tool. Then, while holding the left mouse button, pull on the right edge of the Street column until you can see all of the street addresses.

Open in a
new tab
@customElement('grid-column-reordering-resizing')
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}" 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

Unless specified, all columns are the same width. You can set a specific width for any column, though, or allow the Grid to set the width based on the content. Column widths can be fixed or non-fixed, which is the default. Fixed-width columns don’t grow or shrink as the available space changes, while non-fixed-width columns do.

Note
The em length unit is a relative setting for font sizes. Don’t use it for column widths. If the font sizes of the header, body or footer cells differ, may cause column widths to be inconsistent, leading to misaligned content. When length relative to the font size is needed, use instead the rem length unit — or make sure that the font size is consistent across the cells.

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

Open in a
new tab
@customElement('grid-column-width')
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-split-layout>
        <vaadin-grid .items="${this.items}" style="width: 100%;">
          <vaadin-grid-selection-column></vaadin-grid-selection-column>
          <vaadin-grid-column path="firstName" width="7rem" 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="6rem"
            flex-grow="0"
            header="Has Sub"
            ${columnBodyRenderer<Person>((item) => html`${item.subscriber ? 'Yes' : 'No'}`, [])}
          ></vaadin-grid-column>
        </vaadin-grid>
        <div></div>
      </vaadin-split-layout>
    `;
  }
}