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

# Tree Grid

Tree Grid is a component for displaying hierarchical tabular data grouped into expandable nodes.

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

```typescript
async dataProvider(
  params: GridDataProviderParams<Person>,
  callback: GridDataProviderCallback<Person>
) {
  // The requested page and the full length of the corresponding
  // hierarchy level is requested from the data service
  const { people, hierarchyLevelSize } = await getPeople({
    count: params.pageSize,
    startIndex: params.page * params.pageSize,
    managerId: params.parentItem ? params.parentItem.id : null,
  });

  callback(people, hierarchyLevelSize);
}

protected override render() {
  return html`
    <vaadin-grid .itemHasChildrenPath="${'manager'}" .dataProvider="${this.dataProvider}">
      <vaadin-grid-tree-column path="firstName"></vaadin-grid-tree-column>
      <vaadin-grid-column path="lastName"></vaadin-grid-column>
      <vaadin-grid-column path="email"></vaadin-grid-column>
    </vaadin-grid>
  `;
}
```

**Flow** — `TreeGridBasic.java`

```java
TreeGrid<Person> treeGrid = new TreeGrid<>();
treeGrid.setItems(managers, this::getStaff);
treeGrid.addHierarchyColumn(Person::getFirstName)
        .setHeader("First name");
treeGrid.addColumn(Person::getLastName).setHeader("Last name");
treeGrid.addColumn(Person::getEmail).setHeader("Email");
```

**Flow** — `Person.java`

```java
public class Person {

    @Nonnull
    private String firstName;

    @Nonnull
    private String lastName;

    @Nonnull
    private String email;

    @Nonnull
    private Date birthday;

    @Nonnull
    private Integer id;

    @Nonnull
    private Boolean subscriber;

    @Nonnull
    private String membership;

    @Nonnull
    private String pictureUrl;

    @Nonnull
    private String profession;

    @Nonnull
    private Address address;

    private Integer managerId;

    @Nonnull
    private Boolean manager;

    @Nonnull
    private String status;

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getFullName() {
        return firstName + " " + lastName;
    }

    public String getEmail() {
        return email;
    }

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

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public boolean isSubscriber() {
        return subscriber;
    }

    public void setSubscriber(boolean subscriber) {
        this.subscriber = subscriber;
    }

    public String getMembership() {
        return membership;
    }

    public void setMembership(String membership) {
        this.membership = membership;
    }

    public String getPictureUrl() {
        return pictureUrl;
    }

    public void setPictureUrl(String pictureUrl) {
        this.pictureUrl = pictureUrl;
    }

    public String getProfession() {
        return profession;
    }

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

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    @Override
    public int hashCode() {
        return id;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof Person)) {
            return false;
        }
        Person other = (Person) obj;
        return id == other.id;
    }

    public Integer getManagerId() {
        return managerId;
    }

    public void setManagerId(Integer managerId) {
        this.managerId = managerId;
    }

    public boolean isManager() {
        return manager;
    }

    public void setManager(boolean manager) {
        this.manager = manager;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }
}
```

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

```tsx
async function dataProvider(
  params: GridDataProviderParams<Person>,
  callback: GridDataProviderCallback<Person>
) {
  // The requested page and the full length of the corresponding
  // hierarchy level is requested from the data service
  const { people, hierarchyLevelSize } = await getPeople({
    count: params.pageSize,
    startIndex: params.page * params.pageSize,
    managerId: params.parentItem ? params.parentItem.id : null,
  });

  callback(people, hierarchyLevelSize);
}

function Example() {
  return (
    <Grid itemHasChildrenPath="manager" dataProvider={dataProvider}>
      <GridTreeColumn path="firstName" />
      <GridColumn path="lastName" />
      <GridColumn path="email" />
    </Grid>
  );
}
```

> **Note:** Tree Grid is an extension of the Grid component. Therefore, all of Grid’s features are available in Tree Grid. However, Tree Grid isn’t meant to be used as a navigation menu.

## <a id="tree-column"></a>Tree Column

The tree column contains the toggles for expanding and collapsing nodes. Nodes are opened and closed by clicking a tree column’s cell. They can also be toggled programmatically.

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

```typescript
@state()
private expandedItems: unknown[] = [];

protected override render() {
  return html`
    <vaadin-horizontal-layout
      style="align-items: center; height: var(--lumo-size-xl);"
      theme="spacing"
    >
      <h3 style="flex-grow: 1; margin: 0;">Employee</h3>
      <vaadin-button @click="${this.expandAll}">Expand All</vaadin-button>
      <vaadin-button @click="${this.collapseAll}">Collapse All</vaadin-button>
    </vaadin-horizontal-layout>

    <vaadin-grid
      .dataProvider="${this.dataProvider}"
      .itemIdPath="${'id'}"
      .itemHasChildrenPath="${'manager'}"
      .expandedItems="${this.expandedItems}"
    >
      <vaadin-grid-tree-column path="firstName"></vaadin-grid-tree-column>
      <vaadin-grid-column path="lastName"></vaadin-grid-column>
      <vaadin-grid-column path="email"></vaadin-grid-column>
    </vaadin-grid>
  `;
}

private expandAll() {
  this.expandedItems = [...this.managers];
}

private collapseAll() {
  this.expandedItems = [];
}
```

**Flow** — `TreeGridColumn.java`

```java
Button expand = new Button("Expand All");
expand.addClickListener(event -> treeGrid.expand(managers));

Button collapse = new Button("Collapse All");
collapse.addClickListener(event -> treeGrid.collapse(managers));
```

**Flow** — `Person.java`

```java
public class Person {

    @Nonnull
    private String firstName;

    @Nonnull
    private String lastName;

    @Nonnull
    private String email;

    @Nonnull
    private Date birthday;

    @Nonnull
    private Integer id;

    @Nonnull
    private Boolean subscriber;

    @Nonnull
    private String membership;

    @Nonnull
    private String pictureUrl;

    @Nonnull
    private String profession;

    @Nonnull
    private Address address;

    private Integer managerId;

    @Nonnull
    private Boolean manager;

    @Nonnull
    private String status;

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getFullName() {
        return firstName + " " + lastName;
    }

    public String getEmail() {
        return email;
    }

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

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public boolean isSubscriber() {
        return subscriber;
    }

    public void setSubscriber(boolean subscriber) {
        this.subscriber = subscriber;
    }

    public String getMembership() {
        return membership;
    }

    public void setMembership(String membership) {
        this.membership = membership;
    }

    public String getPictureUrl() {
        return pictureUrl;
    }

    public void setPictureUrl(String pictureUrl) {
        this.pictureUrl = pictureUrl;
    }

    public String getProfession() {
        return profession;
    }

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

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    @Override
    public int hashCode() {
        return id;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof Person)) {
            return false;
        }
        Person other = (Person) obj;
        return id == other.id;
    }

    public Integer getManagerId() {
        return managerId;
    }

    public void setManagerId(Integer managerId) {
        this.managerId = managerId;
    }

    public boolean isManager() {
        return manager;
    }

    public void setManager(boolean manager) {
        this.manager = manager;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }
}
```

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

```tsx
<HorizontalLayout
  style={{ alignItems: 'center', height: 'var(--lumo-size-xl)' }}
  theme="spacing"
>
  <h3 style={{ flexGrow: 1, margin: 0 }}>Employee</h3>
  <Button onClick={expandAll}>Expand All</Button>
  <Button onClick={collapseAll}>Collapse All</Button>
</HorizontalLayout>

<Grid
  dataProvider={dataProvider}
  itemIdPath="id"
  itemHasChildrenPath="manager"
  expandedItems={expandedItems.value}
>
  <GridTreeColumn path="firstName" />
  <GridColumn path="lastName" />
  <GridColumn path="email" />
</Grid>
```

## <a id="rich-content"></a>Rich Content

Like Grid, Tree Grid supports rich content.

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

```typescript
private employeeRenderer: GridColumnBodyLitRenderer<Person> = (person, model) => html`
  <vaadin-grid-tree-toggle
    .leaf="${!person.manager}"
    .level="${model.level ?? 0}"
    @expanded-changed="${(e: GridTreeToggleExpandedChangedEvent) => {
      if (e.detail.value) {
        this.expandedItems = [...this.expandedItems, person];
      } else {
        this.expandedItems = this.expandedItems.filter((p) => p.id !== person.id);
      }
    }}"
    .expanded="${!!model.expanded}"
  >
    <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.profession}
        </span>
      </vaadin-vertical-layout>
    </vaadin-horizontal-layout>
  </vaadin-grid-tree-toggle>
`;

private contactRenderer: GridColumnBodyLitRenderer<Person> = (person) => html`
  <vaadin-vertical-layout
    style="font-size: var(--lumo-font-size-s); line-height: var(--lumo-line-height-m);"
  >
    <a href="mailto:${person.email}" style="align-items: center; display: flex;">
      <vaadin-icon
        icon="vaadin:envelope"
        style="height: var(--lumo-icon-size-s); margin-inline-end: var(--lumo-space-s); width: var(--lumo-icon-size-s);"
      ></vaadin-icon>
      <span>${person.email}</span>
    </a>
    <a href="tel:${person.address.phone}" style="align-items: center; display: flex;">
      <vaadin-icon
        icon="vaadin:phone"
        style="height: var(--lumo-icon-size-s); margin-inline-end: var(--lumo-space-s); width: var(--lumo-icon-size-s);"
      ></vaadin-icon>
      <span>${person.address.phone}</span>
    </a>
  </vaadin-vertical-layout>
`;

protected override render() {
  return html`
    <vaadin-grid .dataProvider="${this.dataProvider}" .expandedItems="${this.expandedItems}">
      <vaadin-grid-column
        auto-width
        header="Employee"
        ${columnBodyRenderer(this.employeeRenderer, [])}
      ></vaadin-grid-column>
      <vaadin-grid-column
        auto-width
        header="Contact"
        ${columnBodyRenderer(this.contactRenderer, [])}
      ></vaadin-grid-column>
    </vaadin-grid>
  `;
}
```

**Flow** — `TreeGridRichContent.java`

```java
treeGrid.addComponentHierarchyColumn(person -> {
    Avatar avatar = new Avatar();
    avatar.setName(person.getFullName());
    avatar.setImage(person.getPictureUrl());

    Span fullName = new Span(person.getFullName());

    Span profession = new Span(person.getProfession());
    profession.getStyle()
            .set("color", "var(--lumo-secondary-text-color)")
            .set("font-size", "var(--lumo-font-size-s)");

    VerticalLayout column = new VerticalLayout(fullName, profession);
    column.getStyle().set("line-height", "var(--lumo-line-height-m)");
    column.setPadding(false);
    column.setSpacing(false);

    HorizontalLayout row = new HorizontalLayout(avatar, column);
    row.setAlignItems(FlexComponent.Alignment.CENTER);
    row.setSpacing(true);
    return row;
}).setHeader("Employee");

treeGrid.addComponentColumn(person -> {
    Icon emailIcon = createIcon(VaadinIcon.ENVELOPE);
    Span email = new Span(person.getEmail());

    Anchor emailLink = new Anchor();
    emailLink.add(emailIcon, email);
    emailLink.setHref("mailto:" + person.getEmail());
    emailLink.getStyle().set("align-items", "center").set("display",
            "flex");

    Icon phoneIcon = createIcon(VaadinIcon.PHONE);
    Span phone = new Span(person.getAddress().getPhone());

    Anchor phoneLink = new Anchor();
    phoneLink.add(phoneIcon, phone);
    phoneLink.setHref("tel:" + person.getAddress().getPhone());
    phoneLink.getStyle().set("align-items", "center").set("display",
            "flex");

    VerticalLayout column = new VerticalLayout(emailLink, phoneLink);
    column.getStyle().set("font-size", "var(--lumo-font-size-s)")
            .set("line-height", "var(--lumo-line-height-m)");
    column.setPadding(false);
    column.setSpacing(false);
    return column;
}).setHeader("Contact");
```

**Flow** — `Person.java`

```java
public class Person {

    @Nonnull
    private String firstName;

    @Nonnull
    private String lastName;

    @Nonnull
    private String email;

    @Nonnull
    private Date birthday;

    @Nonnull
    private Integer id;

    @Nonnull
    private Boolean subscriber;

    @Nonnull
    private String membership;

    @Nonnull
    private String pictureUrl;

    @Nonnull
    private String profession;

    @Nonnull
    private Address address;

    private Integer managerId;

    @Nonnull
    private Boolean manager;

    @Nonnull
    private String status;

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getFullName() {
        return firstName + " " + lastName;
    }

    public String getEmail() {
        return email;
    }

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

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public boolean isSubscriber() {
        return subscriber;
    }

    public void setSubscriber(boolean subscriber) {
        this.subscriber = subscriber;
    }

    public String getMembership() {
        return membership;
    }

    public void setMembership(String membership) {
        this.membership = membership;
    }

    public String getPictureUrl() {
        return pictureUrl;
    }

    public void setPictureUrl(String pictureUrl) {
        this.pictureUrl = pictureUrl;
    }

    public String getProfession() {
        return profession;
    }

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

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    @Override
    public int hashCode() {
        return id;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof Person)) {
            return false;
        }
        Person other = (Person) obj;
        return id == other.id;
    }

    public Integer getManagerId() {
        return managerId;
    }

    public void setManagerId(Integer managerId) {
        this.managerId = managerId;
    }

    public boolean isManager() {
        return manager;
    }

    public void setManager(boolean manager) {
        this.manager = manager;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }
}
```

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

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

const toggleRenderer = useCallback(
  ({ item: person, model }: { item: Person; model: { level?: number; expanded?: boolean } }) => (
    <GridTreeToggle
      leaf={!person.manager}
      level={model?.level ?? 0}
      expanded={!!model?.expanded}
      onClick={(e) => {
        if (!e.defaultPrevented) {
          return;
        }
        if (e.currentTarget.expanded) {
          expandedItems.value = [...expandedItems.value, person];
        } else {
          expandedItems.value = expandedItems.value.filter((p) => p.id !== person.id);
        }
      }}
    >
      <HorizontalLayout style={{ alignItems: 'center' }} theme="spacing">
        <Avatar img={person.pictureUrl} name={`${person.firstName} ${person.lastName}`} />
        <VerticalLayout style={{ lineHeight: 'var(--lumo-line-height-m)' }}>
          <span>
            {person.firstName} {person.lastName}
          </span>
          <span
            style={{
              fontSize: 'var(--lumo-font-size-s)',
              color: 'var(--lumo-secondary-text-color)',
            }}
          >
            {person.profession}
          </span>
        </VerticalLayout>
      </HorizontalLayout>
    </GridTreeToggle>
  ),
  []
);

return (
  <Grid dataProvider={dataProvider} expandedItems={expandedItems.value}>
    <GridColumn autoWidth header="Employee" renderer={toggleRenderer} />
    <GridColumn autoWidth header="Contact" renderer={contactRenderer} />
  </Grid>
);
```

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

Grid supports programmatic navigation to a specific row. This is particularly useful when dealing with large data sets. It saves users from having to scroll through potentially hundreds or thousands of rows.

To use this feature, you need to specify the index of the row you want to view. The scroll position of the grid will then be adjusted to bring that row into view.

With multiple levels of hierarchy, you need to specify the row index for each level, separately. For example, to scroll to the second child-row (index 1) of the third root-level row (index 2), you would provide the indexes 2, 1.

**Lit** — `tree-grid-scroll-to-index.ts`

```typescript
this.grid.scrollToIndex(...this.indexesToScrollTo);
```

**Flow** — `TreeGridScrollToIndex.java`

```java
treeGrid.scrollToIndex(indexesToScrollTo);
```

**React** — `tree-grid-scroll-to-index.tsx`

```tsx
grid.scrollToIndex(...indexesToScrollTo.value);
```

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

| Component                                                      | Usage Recommendation                                                     |
| -------------------------------------------------------------- | ------------------------------------------------------------------------ |
| [Grid](https://vaadin.com/docs/v24/components/grid.md)         | Component for showing tabular data.                                      |
| [Grid Pro](https://vaadin.com/docs/v24/components/grid-pro.md) | Component for showing and editing tabular data.                          |
| [CRUD](https://vaadin.com/docs/v24/components/crud.md)         | Component for creating, displaying, updating, and deleting tabular data. |

`9DEF736F-975A-469A-8625-A0A6E086BF50`
