Tree data (hierarchy)

treeData nests rows by parent id into an expandable hierarchy. Tree rows stay real data rows - own cells, formatting, editing - and just gain an expander plus indent. Takes flat parent-id data directly, or nested children arrays via flattenTreeData. Full treegrid a11y with arrow-key expand.

A live, editable Svelte 5 data grid example from the SvGrid gallery (Sorting & Grouping). See the SvGrid documentation for the full API.

About this example

Client-side tree data in the Svelte 5 data grid. treeData={{ parentField: 'managerId', column: 'name' }} nests rows into an expandable hierarchy by parent id; tree rows stay real data rows with their own cells, formatting, editing and selection, and only gain an expander plus indent in the tree column. Flat parent-id data is used directly and nested children arrays go through flattenTreeData, with full treegrid accessibility and arrow-key expand.

treeData nests rows into an expandable hierarchy by parent id:

treeData={{ parentField: 'managerId', column: 'name' }}

Tree rows stay REAL data rows - own cells, formatting, editing, selection - and only gain an expander plus indent in the tree column. (Row grouping is the other shape: there the parent is a synthetic full-width banner.)

Two input shapes, one model: FLAT - rows already carry a parent id. Use it directly. NESTED - objects hold a children array. flattenTreeData stamps __parentId on each child and returns one flat list.

Imports, features and API used

Imports: @svgrid/grid

Table features registered: rowSortingFeature

SvGridApi methods called: api.collapseAllGroups(), api.expandAllGroups()

Frequently asked questions

How do I turn flat rows with a parent id into a tree?

Pass treeData with parentField naming the id column of the parent and column naming the column that shows the expander and indent. Rows whose parent is missing are roots.

My data has nested children arrays. Do I have to flatten it?

Call flattenTreeData(rows) from @svgrid/grid. It stamps __parentId on each child and returns one flat list, which is then the same shape as the parent-id case.

How is this different from row grouping?

In grouping the parent is a synthetic banner created by the grid. In tree data the parent is a real row from your data with its own values, and children can be edited and selected like any other row.

Related documentation

Related articles

Source code (426-tree-data.svelte)

<!-- Documented in: docs/help/rows/tree-data.md -->
<script lang="ts">
  /**
   * 426. Tree data
   * --------------
   * `treeData` nests rows into an expandable hierarchy by parent id:
   *
   *   treeData={{ parentField: 'managerId', column: 'name' }}
   *
   * Tree rows stay REAL data rows - own cells, formatting, editing, selection -
   * and only gain an expander plus indent in the tree column. (Row grouping is
   * the other shape: there the parent is a synthetic full-width banner.)
   *
   * Two input shapes, one model:
   *   FLAT   - rows already carry a parent id. Use it directly.
   *   NESTED - objects hold a `children` array. `flattenTreeData` stamps
   *            `__parentId` on each child and returns one flat list.
   */
  import {
    SvGrid,
    flattenTreeData,
    tableFeatures,
    rowSortingFeature,
    type GridColumns,
    type SvGridApi,
  } from '@svgrid/grid'

  const features = tableFeatures({ rowSortingFeature })

  // ---- Flat source: an org chart, each row naming its manager -------------
  type Person = {
    id: number
    managerId: number | null
    name: string
    title: string
    location: string
    reports: number
    budget: number
  }

  const people: Person[] = [
    { id: 1,  managerId: null, name: 'Ada Lovelace',      title: 'Chief Executive',    location: 'London',    reports: 10, budget: 4_200_000 },
    { id: 2,  managerId: 1,    name: 'Grace Hopper',      title: 'VP Engineering',     location: 'New York',  reports: 5,  budget: 1_850_000 },
    { id: 3,  managerId: 2,    name: 'Alan Turing',       title: 'Principal Engineer', location: 'Cambridge', reports: 2,  budget: 540_000 },
    { id: 4,  managerId: 3,    name: 'Barbara Liskov',    title: 'Staff Engineer',     location: 'Boston',    reports: 0,  budget: 210_000 },
    { id: 5,  managerId: 3,    name: 'Ken Thompson',      title: 'Staff Engineer',     location: 'Remote',    reports: 0,  budget: 205_000 },
    { id: 6,  managerId: 2,    name: 'Dennis Ritchie',    title: 'Engineering Manager',location: 'New York',  reports: 1,  budget: 420_000 },
    { id: 7,  managerId: 6,    name: 'Brian Kernighan',   title: 'Senior Engineer',    location: 'Princeton', reports: 0,  budget: 190_000 },
    { id: 8,  managerId: 1,    name: 'Margaret Hamilton', title: 'VP Operations',      location: 'Houston',   reports: 2,  budget: 1_100_000 },
    { id: 9,  managerId: 8,    name: 'Katherine Johnson', title: 'Operations Lead',    location: 'Houston',   reports: 1,  budget: 380_000 },
    { id: 10, managerId: 9,    name: 'Dorothy Vaughan',   title: 'Operations Engineer',location: 'Houston',   reports: 0,  budget: 175_000 },
    { id: 11, managerId: 1,    name: 'Linus Torvalds',    title: 'VP Infrastructure',  location: 'Portland',  reports: 0,  budget: 950_000 },
  ]

  // ---- Nested source: a file tree with children arrays --------------------
  type Node = {
    id: number
    name: string
    title: string
    location: string
    reports: number
    budget: number
    children?: Node[]
  }

  const fileTree: Node[] = [
    {
      id: 100, name: 'src', title: 'folder', location: '—', reports: 5, budget: 0,
      children: [
        {
          id: 101, name: 'components', title: 'folder', location: '—', reports: 2, budget: 0,
          children: [
            { id: 102, name: 'Grid.svelte',    title: 'component', location: '18.4 KB', reports: 0, budget: 0 },
            { id: 103, name: 'Toolbar.svelte', title: 'component', location: '6.1 KB',  reports: 0, budget: 0 },
          ],
        },
        {
          id: 104, name: 'lib', title: 'folder', location: '—', reports: 2, budget: 0,
          children: [
            { id: 105, name: 'utils.ts',   title: 'module', location: '3.2 KB', reports: 0, budget: 0 },
            { id: 106, name: 'format.ts',  title: 'module', location: '1.8 KB', reports: 0, budget: 0 },
          ],
        },
        { id: 107, name: 'main.ts', title: 'entry', location: '0.9 KB', reports: 0, budget: 0 },
      ],
    },
    {
      id: 200, name: 'docs', title: 'folder', location: '—', reports: 2, budget: 0,
      children: [
        { id: 201, name: 'readme.md',  title: 'markdown', location: '2.4 KB', reports: 0, budget: 0 },
        { id: 202, name: 'changelog.md', title: 'markdown', location: '5.7 KB', reports: 0, budget: 0 },
      ],
    },
  ]

  // One call converts the nested shape into the flat parent-id shape the model
  // consumes. `__parentId` is the link field it stamps on by default.
  const flattenedFiles = flattenTreeData(fileTree, { childrenField: 'children' })

  type Source = 'org' | 'files'
  let source = $state<Source>('org')

  const orgColumns: GridColumns<Person> = [
    { field: 'name',     header: 'Name',     width: 240 },
    { field: 'title',    header: 'Title',    width: 190 },
    { field: 'location', header: 'Location', width: 130 },
    { field: 'reports',  header: 'Reports',  width: 100, align: 'right' },
    {
      field: 'budget', header: 'Budget', width: 140, align: 'right',
      format: { type: 'currency', currency: 'USD', options: { maximumFractionDigits: 0 } },
    },
  ]

  const fileColumns: GridColumns<Node> = [
    { field: 'name',     header: 'Name',    width: 280 },
    { field: 'title',    header: 'Kind',    width: 140 },
    { field: 'location', header: 'Size',    width: 120, align: 'right' },
  ]

  // `expandAllGroups` / `collapseAllGroups` walk the row model's `subRows`,
  // which is exactly what the tree model builds - so they drive a tree too.
  let api = $state<SvGridApi<typeof features, Person | Node> | null>(null)
</script>

<section class="flex flex-col flex-1 min-h-0 gap-3">
  <div
    class="shrink-0 rounded-lg border px-4 py-3"
    style="border-color: var(--sg-border); background: var(--sg-header-bg);"
  >
    <p class="text-sm font-semibold" style="color: var(--sg-fg);">
      Hierarchical rows via <code>treeData</code>
    </p>
    <p class="mt-1 text-xs" style="color: var(--sg-muted);">
      Click a chevron, or focus a row and press <kbd>→</kbd> / <kbd>←</kbd>. The
      grid uses the <code>treegrid</code> role with <code>aria-level</code> and
      <code>aria-expanded</code>, so screen readers announce the depth. Rows keep
      their own cells - sort by Budget and the hierarchy still holds.
    </p>

    <div class="mt-3 flex flex-wrap items-center gap-2">
      <div class="inline-flex overflow-hidden rounded-md border" style="border-color: var(--sg-border);">
        <button
          type="button" class="px-3 py-1 text-xs"
          style={source === 'org'
            ? 'background: var(--sg-accent, #2563eb); color: var(--sg-on-accent, #fff);'
            : 'background: transparent; color: var(--sg-fg);'}
          onclick={() => (source = 'org')}
        >Flat (parent id)</button>
        <button
          type="button" class="px-3 py-1 text-xs"
          style={source === 'files'
            ? 'background: var(--sg-accent, #2563eb); color: var(--sg-on-accent, #fff);'
            : 'background: transparent; color: var(--sg-fg);'}
          onclick={() => (source = 'files')}
        >Nested (flattened)</button>
      </div>

      <button
        type="button" class="rounded-md border px-3 py-1 text-xs"
        style="border-color: var(--sg-border); color: var(--sg-fg);"
        onclick={() => api?.expandAllGroups()}
      >Expand all</button>
      <button
        type="button" class="rounded-md border px-3 py-1 text-xs"
        style="border-color: var(--sg-border); color: var(--sg-fg);"
        onclick={() => api?.collapseAllGroups()}
      >Collapse all</button>

      <span class="text-xs" style="color: var(--sg-muted);">
        {#if source === 'org'}
          <code>parentField: 'managerId'</code>
        {:else}
          <code>flattenTreeData(tree, &lbrace; childrenField: 'children' &rbrace;)</code>
        {/if}
      </span>
    </div>
  </div>

  <div class="flex-1 min-h-0">
    {#if source === 'org'}
      <SvGrid
      columnResize
        responsive={true}
        data={people}
        columns={orgColumns}
        {features}
        treeData={{ parentField: 'managerId', column: 'name' }}
        selectionMode="none"
        rowHeight={34}
        containerHeight="100%"
        fitColumns={true}
        onApiReady={(a) => (api = a as never)}
      />
    {:else}
      <SvGrid
      columnResize
        responsive={true}
        data={flattenedFiles}
        columns={fileColumns}
        {features}
        treeData={{ parentField: '__parentId', column: 'name' }}
        selectionMode="none"
        rowHeight={34}
        containerHeight="100%"
        fitColumns={true}
        onApiReady={(a) => (api = a as never)}
      />
    {/if}
  </div>
</section>

View this example on GitHub

More Sorting & Grouping examples

  • Grouping + aggregation - Group by department, sum salaries, average performance, expand/collapse keys.
  • Group aggregators - Declarative per-column rollups for group rows via the aggregate column option: sum, avg, min, max, count, countDistinct, extent, first, or a custom (values, rows) reducer. Each rollup is formatted with the column format and shown in the group header.
  • Group panel (drag & drop) - A Group Panel: drag chips into the panel to group, drag inside to reorder grouping levels, × to ungroup. Drives api.setGroupBy() under the hood.
  • Group display modes + footers - Switch between groupRows banners, a single combined Group column, and one column per grouped field. groupFooters closes each group with a subtotal row under the real columns. Paging counts DATA rows, so pageSize means what it says: a page reprints the banners its rows sit under, and footers never eat the budget.
  • Reporting workspace - Pivot-lite: group-by chips, per-column aggregator picker, saved views with localStorage persistence, live KPI strip + summary cards.