Tree Grid
Organize and edit nested resources without losing two-dimensional grid navigation.
Usage
Expanded rows
Inspect a project inventory whose nested applications and features remain visibly connected to their parents.
<!-- Tree Grid / Expanded -->
<script setup lang="ts">
import { reactive } from 'vue'
import { TreeGridCell, TreeGridDisclosure, TreeGridEditor, TreeGridRoot, TreeGridRow } from '@sectile/vue/tree-grid'
const rows = [
{ id: 'platform', parentID: null, cells: ['platform-name', 'platform-owner', 'platform-status'] },
{ id: 'storefront', parentID: 'platform', cells: ['storefront-name', 'storefront-owner', 'storefront-status'] },
{ id: 'checkout', parentID: 'storefront', cells: ['checkout-name', 'checkout-owner', 'checkout-status'] },
]
const values = reactive(new Map([
['platform-name', 'Commerce platform'], ['platform-owner', 'Platform team'], ['platform-status', 'Healthy'],
['storefront-name', 'Storefront'], ['storefront-owner', 'Mina Kim'], ['storefront-status', 'Modified'],
['checkout-name', 'Checkout flow'], ['checkout-owner', 'Alex Chen'], ['checkout-status', 'In review'],
]))
const valueFor = (id: string) => values.get(id) ?? ''
const updateValue = (id: string, value: string) => values.set(id, value)
</script>
<template>
<TreeGridRoot
:rows="rows"
:get-cell-value="valueFor"
:set-cell-value="updateValue"
:default-expanded-value="['platform', 'storefront']"
aria-label="Project inventory"
v-slot="{ expandedValue }"
>
<div role="row"><span role="columnheader">Resource</span><span role="columnheader">Owner</span><span role="columnheader">Status</span></div>
<TreeGridRow value="platform" :row-index="1" :expandable="true">
<TreeGridCell value="platform-name" :column-index="1">
<TreeGridDisclosure for="platform">Toggle</TreeGridDisclosure>
{{ valueFor('platform-name') }}
</TreeGridCell>
<TreeGridCell value="platform-owner" :column-index="2" v-slot="{ editing }">
<span v-if="!editing">{{ valueFor('platform-owner') }}</span>
<TreeGridEditor for="platform-owner" label="Commerce platform owner" />
</TreeGridCell>
<TreeGridCell value="platform-status" :column-index="3">{{ valueFor('platform-status') }}</TreeGridCell>
</TreeGridRow>
<TreeGridRow v-if="expandedValue.includes('platform')" value="storefront" :row-index="2" :level="2" :expandable="true">
<TreeGridCell value="storefront-name" :column-index="1">
<TreeGridDisclosure for="storefront">Toggle</TreeGridDisclosure>
{{ valueFor('storefront-name') }}
</TreeGridCell>
<TreeGridCell value="storefront-owner" :column-index="2" v-slot="{ editing }">
<span v-if="!editing">{{ valueFor('storefront-owner') }}</span>
<TreeGridEditor for="storefront-owner" label="Storefront owner" />
</TreeGridCell>
<TreeGridCell value="storefront-status" :column-index="3">{{ valueFor('storefront-status') }}</TreeGridCell>
</TreeGridRow>
<TreeGridRow v-if="expandedValue.includes('storefront')" value="checkout" :row-index="3" :level="3">
<TreeGridCell value="checkout-name" :column-index="1">{{ valueFor('checkout-name') }}</TreeGridCell>
<TreeGridCell value="checkout-owner" :column-index="2" v-slot="{ editing }">
<span v-if="!editing">{{ valueFor('checkout-owner') }}</span>
<TreeGridEditor for="checkout-owner" label="Checkout owner" />
</TreeGridCell>
<TreeGridCell value="checkout-status" :column-index="3">{{ valueFor('checkout-status') }}</TreeGridCell>
</TreeGridRow>
</TreeGridRoot>
</template>Editable cells
Edit the owner of a nested resource without leaving keyboard-based row and column navigation.
<!-- Tree Grid / Editable -->
<script setup lang="ts">
import { reactive } from 'vue'
import { TreeGridCell, TreeGridDisclosure, TreeGridEditor, TreeGridRoot, TreeGridRow } from '@sectile/vue/tree-grid'
const rows = [
{ id: 'platform', parentID: null, cells: ['platform-name', 'platform-owner', 'platform-status'] },
{ id: 'storefront', parentID: 'platform', cells: ['storefront-name', 'storefront-owner', 'storefront-status'] },
{ id: 'checkout', parentID: 'storefront', cells: ['checkout-name', 'checkout-owner', 'checkout-status'] },
]
const values = reactive(new Map([
['platform-name', 'Commerce platform'], ['platform-owner', 'Platform team'], ['platform-status', 'Healthy'],
['storefront-name', 'Storefront'], ['storefront-owner', 'Mina Kim'], ['storefront-status', 'Modified'],
['checkout-name', 'Checkout flow'], ['checkout-owner', 'Alex Chen'], ['checkout-status', 'In review'],
]))
const valueFor = (id: string) => values.get(id) ?? ''
const updateValue = (id: string, value: string) => values.set(id, value)
</script>
<template>
<TreeGridRoot
:rows="rows"
:get-cell-value="valueFor"
:set-cell-value="updateValue"
:default-expanded-value="['platform', 'storefront']"
default-highlighted-value="storefront-owner"
default-edit-mode="editing"
aria-label="Project inventory"
v-slot="{ expandedValue }"
>
<div role="row"><span role="columnheader">Resource</span><span role="columnheader">Owner</span><span role="columnheader">Status</span></div>
<TreeGridRow value="platform" :row-index="1" :expandable="true">
<TreeGridCell value="platform-name" :column-index="1">
<TreeGridDisclosure for="platform">Toggle</TreeGridDisclosure>
{{ valueFor('platform-name') }}
</TreeGridCell>
<TreeGridCell value="platform-owner" :column-index="2" v-slot="{ editing }">
<span v-if="!editing">{{ valueFor('platform-owner') }}</span>
<TreeGridEditor for="platform-owner" label="Commerce platform owner" />
</TreeGridCell>
<TreeGridCell value="platform-status" :column-index="3">{{ valueFor('platform-status') }}</TreeGridCell>
</TreeGridRow>
<TreeGridRow v-if="expandedValue.includes('platform')" value="storefront" :row-index="2" :level="2" :expandable="true">
<TreeGridCell value="storefront-name" :column-index="1">
<TreeGridDisclosure for="storefront">Toggle</TreeGridDisclosure>
{{ valueFor('storefront-name') }}
</TreeGridCell>
<TreeGridCell value="storefront-owner" :column-index="2" v-slot="{ editing }">
<span v-if="!editing">{{ valueFor('storefront-owner') }}</span>
<TreeGridEditor for="storefront-owner" label="Storefront owner" />
</TreeGridCell>
<TreeGridCell value="storefront-status" :column-index="3">{{ valueFor('storefront-status') }}</TreeGridCell>
</TreeGridRow>
<TreeGridRow v-if="expandedValue.includes('storefront')" value="checkout" :row-index="3" :level="3">
<TreeGridCell value="checkout-name" :column-index="1">{{ valueFor('checkout-name') }}</TreeGridCell>
<TreeGridCell value="checkout-owner" :column-index="2" v-slot="{ editing }">
<span v-if="!editing">{{ valueFor('checkout-owner') }}</span>
<TreeGridEditor for="checkout-owner" label="Checkout owner" />
</TreeGridCell>
<TreeGridCell value="checkout-status" :column-index="3">{{ valueFor('checkout-status') }}</TreeGridCell>
</TreeGridRow>
</TreeGridRoot>
</template>Controlled
Let the parent own the current value and apply accepted changes back to the component.
<!-- Tree Grid / Controlled -->
<script setup lang="ts">
import { reactive } from 'vue'
import { TreeGridCell, TreeGridDisclosure, TreeGridEditor, TreeGridRoot, TreeGridRow } from '@sectile/vue/tree-grid'
const rows = [
{ id: 'platform', parentID: null, cells: ['platform-name', 'platform-owner', 'platform-status'] },
{ id: 'storefront', parentID: 'platform', cells: ['storefront-name', 'storefront-owner', 'storefront-status'] },
{ id: 'checkout', parentID: 'storefront', cells: ['checkout-name', 'checkout-owner', 'checkout-status'] },
]
const values = reactive(new Map([
['platform-name', 'Commerce platform'], ['platform-owner', 'Platform team'], ['platform-status', 'Healthy'],
['storefront-name', 'Storefront'], ['storefront-owner', 'Mina Kim'], ['storefront-status', 'Modified'],
['checkout-name', 'Checkout flow'], ['checkout-owner', 'Alex Chen'], ['checkout-status', 'In review'],
]))
const valueFor = (id: string) => values.get(id) ?? ''
const updateValue = (id: string, value: string) => values.set(id, value)
</script>
<template>
<TreeGridRoot
:rows="rows"
:get-cell-value="valueFor"
:set-cell-value="updateValue"
:default-expanded-value="['platform', 'storefront']"
aria-label="Project inventory"
v-slot="{ expandedValue }"
>
<div role="row"><span role="columnheader">Resource</span><span role="columnheader">Owner</span><span role="columnheader">Status</span></div>
<TreeGridRow value="platform" :row-index="1" :expandable="true">
<TreeGridCell value="platform-name" :column-index="1">
<TreeGridDisclosure for="platform">Toggle</TreeGridDisclosure>
{{ valueFor('platform-name') }}
</TreeGridCell>
<TreeGridCell value="platform-owner" :column-index="2" v-slot="{ editing }">
<span v-if="!editing">{{ valueFor('platform-owner') }}</span>
<TreeGridEditor for="platform-owner" label="Commerce platform owner" />
</TreeGridCell>
<TreeGridCell value="platform-status" :column-index="3">{{ valueFor('platform-status') }}</TreeGridCell>
</TreeGridRow>
<TreeGridRow v-if="expandedValue.includes('platform')" value="storefront" :row-index="2" :level="2" :expandable="true">
<TreeGridCell value="storefront-name" :column-index="1">
<TreeGridDisclosure for="storefront">Toggle</TreeGridDisclosure>
{{ valueFor('storefront-name') }}
</TreeGridCell>
<TreeGridCell value="storefront-owner" :column-index="2" v-slot="{ editing }">
<span v-if="!editing">{{ valueFor('storefront-owner') }}</span>
<TreeGridEditor for="storefront-owner" label="Storefront owner" />
</TreeGridCell>
<TreeGridCell value="storefront-status" :column-index="3">{{ valueFor('storefront-status') }}</TreeGridCell>
</TreeGridRow>
<TreeGridRow v-if="expandedValue.includes('storefront')" value="checkout" :row-index="3" :level="3">
<TreeGridCell value="checkout-name" :column-index="1">{{ valueFor('checkout-name') }}</TreeGridCell>
<TreeGridCell value="checkout-owner" :column-index="2" v-slot="{ editing }">
<span v-if="!editing">{{ valueFor('checkout-owner') }}</span>
<TreeGridEditor for="checkout-owner" label="Checkout owner" />
</TreeGridCell>
<TreeGridCell value="checkout-status" :column-index="3">{{ valueFor('checkout-status') }}</TreeGridCell>
</TreeGridRow>
</TreeGridRoot>
</template>API
Vue package: @sectile/vue/tree-grid
TreeGridRootTreeGridRowTreeGridCellTreeGridDisclosureTreeGridEditor
Props
TreeGridRootProps
asElement or component rendered for this part.
asChildWhether to merge this part into its single child instead of rendering a wrapper.
defaultEditModeInitial uncontrolled editing mode.
defaultExpandedValueInitially expanded values for uncontrolled state.
defaultHighlightedValueInitially highlighted value for uncontrolled state.
defaultValueInitial value used when the component owns its state.
disabledWhether interaction is unavailable.
editModeControlled editing mode.
expandedValueControlled values whose descendants are visible.
getCellValueReads the editable value represented by a grid cell.
highlightedValueValue currently highlighted for keyboard interaction.
modelValueCurrent value when state is controlled by the parent.
policiesBehavior policies that customize validation, movement, or selection.
readonlyWhether the value can be inspected but not changed.
rowsTwo-dimensional item structure managed by the component.
setCellValueCommits a new editable value for a grid cell.
TreeGridPartProps
asElement or component rendered for this part.
asChildWhether to merge this part into its single child instead of rendering a wrapper.
Slots
TreeGridRootSlotProps
disabledWhether interaction is unavailable.
editModeCurrent editing mode.
expandedValueValues whose descendants are visible.
highlightedValueValue currently highlighted for interaction.
readonlyWhether the value can be inspected but not changed.
valueCurrent value exposed by this contract.
TreeGridRowSlotProps
disabledWhether interaction is unavailable.
editModeCurrent editing mode.
expandedWhether descendants are visible.
expandedValueValues whose descendants are visible.
highlightedValueValue currently highlighted for interaction.
readonlyWhether the value can be inspected but not changed.
valueCurrent value exposed by this contract.
TreeGridCellSlotProps
disabledWhether interaction is unavailable.
editingWhether editing is active.
editModeCurrent editing mode.
expandedValueValues whose descendants are visible.
highlightedWhether this item is highlighted for interaction.
highlightedValueValue currently highlighted for interaction.
readonlyWhether the value can be inspected but not changed.
selectedWhether this item is selected.
valueCurrent value exposed by this contract.
Events
TreeGridRoot
update:editModeEmitted when the requested edit mode changes.
update:expandedValueEmitted when the requested expanded values change.
update:highlightedValueEmitted when the requested highlighted value changes.
update:modelValueEmitted when the component requests a new controlled value.
Other types
TreeGridCellValueResolver
type TreeGridCellValueResolver = NonNullable<TreeGridRootProps['getCellValue']>TreeGridCellValueSetter
type TreeGridCellValueSetter = NonNullable<TreeGridRootProps['setCellValue']>TreeGridValueChangeHandler
type TreeGridValueChangeHandler = (value: string | null) => voidTreeGridExpandedValueChangeHandler
type TreeGridExpandedValueChangeHandler = (value: readonly string[]) => voidTreeGridHighlightedValueChangeHandler
type TreeGridHighlightedValueChangeHandler = (value: string | null) => voidTreeGridEditModeChangeHandler
type TreeGridEditModeChangeHandler = (value: TreeGridEditMode) => voidTreeGridEditMode
type TreeGridEditMode = 'navigation' | 'editing'TreeGridPolicies
| Name | Type | Required |
|---|---|---|
eligible | (id: CellID) => boolean | — |
boundary | AxisBoundaryPolicy | — |
maxScan | number | — |
TreeGridRowInput
| Name | Type | Required |
|---|---|---|
id | RowID | Yes |
parentID | RowID | null | Yes |
cells | readonly (CellID | null)[] | Yes |
Parts
Shared scope: [data-scope="tree-grid"]. Combine it with a part selector to keep styles local to this component.
| Part | Selector | Role | Extra attributes |
|---|---|---|---|
root | [data-part="root"] | Defines the component boundary and owns its composed parts. | — |
row | [data-part="row"] | Groups cells that belong to one grid row. | — |
cell | [data-part="cell"] | Represents one navigable or selectable grid value. | — |
disclosure | [data-part="disclosure"] | Expands or collapses child content. | — |
editor | [data-part="editor"] | Edits the active grid or tree-grid cell. | — |
Keyboard interaction
| Key | Behavior |
|---|---|
| Arrow keys | Move between grid cells. |
| Space | Select the current cell or row. |
| Enter / F2 | Enter edit mode when the current cell supports editing. |
| Escape | Cancel the active edit. |
Accessibility
Rows and cells expose hierarchy, position, expansion, selection, and edit state inside a labeled grid.
See the corresponding WAI-ARIA pattern for the host accessibility contract.
