Vue3 TreeTable

Vue3 TreeTable

·

23 min read

Vue3 TreeTable is used to display hierarchical data in tabular format. It supports Vue 3 with PrimeVue 3 and Vue 2 with PrimeVue 2.

Setup

Refer to PrimeVue setup documentation for download and installation steps for your environment such as Vue CLI, Vite or browser.

Import

import TreeTable from 'primevue/treetable';
import Column from 'primevue/column';

Getting Started

Tree component requires an array of TreeNode objects as its value and columns defined with Column component.

Example below loads the nodes from a remote datasource via a service called NodeService.

<TreeTable :value="nodes">
    <Column field="name" header="Name" :expander="true"></Column>
    <Column field="size" header="Size"></Column>
    <Column field="type" header="Type"></Column>
</TreeTable>
import NodeService from '../../service/NodeService';

export default {
    data() {
        return {
            nodes: null
        }
    },
    nodeService: null,
    created() {
        this.nodeService = new NodeService();
    },
    mounted() {
        this.nodeService.getTreeTableNodes().then(data => this.nodes = data);
    }
}
import axios from 'axios';

export default class NodeService {

    getTreeTableNodes() {
        return axios.get('demo/data/treetablenodes.json').then(res => res.data.root);
    }

}

The json response sample would be as following.

{
    "root":
    [
        {
            "key": "0",
            "data":{
                "name":"Applications",
                "size":"100kb",
                "type":"Folder"
            },
            "children":[
                {
                    "key": "0-0",
                    "data":{
                        "name":"Vue",
                        "size":"25kb",
                        "type":"Folder"
                    },
                    "children":[
                        {
                            "key": "0-0-0",
                            "data":{
                                "name":"Vue.app",
                                "size":"10kb",
                                "type":"Application"
                            }
                        },
                        {
                            "key": "0-0-1",
                            "data":{
                                "name":"native.app",
                                "size":"10kb",
                                "type":"Application"
                            }
                        },
                        {
                            "key": "0-0-2",
                            "data":{
                                "name":"mobile.app",
                                "size":"5kb",
                                "type":"Application"
                            }
                        }
                    ]
                },
                {
                    "key": "0-1",
                    "data":{
                        "name":"editor.app",
                        "size":"25kb",
                        "type":"Application"
                    }
                },
                {
                    "key": "0-2",
                    "data":{
                        "name":"settings.app",
                        "size":"50kb",
                        "type":"Application"
                    }
                }
            ]
        },
        {
            "key": "1",
            "data":{
                "name":"Cloud",
                "size":"20kb",
                "type":"Folder"
            },
            "children":[
                {
                    "key": "1-0",
                    "data":{
                        "name":"backup-1.zip",
                        "size":"10kb",
                        "type":"Zip"
                    }
                },
                {
                    "key": "1-1",
                    "data":{
                        "name":"backup-2.zip",
                        "size":"10kb",
                        "type":"Zip"
                    }
                }
            ]
        },
        {
            "key": "2",
            "data": {
                "name":"Desktop",
                "size":"150kb",
                "type":"Folder"
            },
            "children":[
                {
                    "key": "2-0",
                    "data":{
                        "name":"note-meeting.txt",
                        "size":"50kb",
                        "type":"Text"
                    }
                },
                {
                    "key": "2-1",
                    "data":{
                        "name":"note-todo.txt",
                        "size":"100kb",
                        "type":"Text"
                    }
                }
            ]
        },
        {
            "key": "3",
            "data":{
                "name":"Documents",
                "size":"75kb",
                "type":"Folder"
            },
            "children":[
                {
                    "key": "3-0",
                    "data":{
                        "name":"Work",
                        "size":"55kb",
                        "type":"Folder"
                    },
                    "children":[
                        {
                            "key": "3-0-0",
                            "data":{
                                "name":"Expenses.doc",
                                "size":"30kb",
                                "type":"Document"
                            }
                        },
                        {
                            "key": "3-0-1",
                            "data":{
                                "name":"Resume.doc",
                                "size":"25kb",
                                "type":"Resume"
                            }
                        }
                    ]
                },
                {
                    "key": "3-1",
                    "data":{
                        "name":"Home",
                        "size":"20kb",
                        "type":"Folder"
                    },
                    "children":[
                        {
                            "key": "3-1-0",
                            "data":{
                                "name":"Invoices",
                                "size":"20kb",
                                "type":"Text"
                            }
                        }
                    ]
                }
            ]
        },
        {
            "key": "4",
            "data": {
                "name":"Downloads",
                "size":"25kb",
                "type":"Folder"
            },
            "children":[
                {
                    "key": "4-0",
                    "data": {
                        "name":"Spanish",
                        "size":"10kb",
                        "type":"Folder"
                    },
                    "children":[
                        {
                            "key": "4-0-0",
                            "data":{
                                "name":"tutorial-a1.txt",
                                "size":"5kb",
                                "type":"Text"
                            }
                        },
                        {
                            "key": "4-0-1",
                            "data":{
                                "name":"tutorial-a2.txt",
                                "size":"5kb",
                                "type":"Text"
                            }
                        }
                    ]
                },
                {
                    "key": "4-1",
                    "data":{
                        "name":"Travel",
                        "size":"15kb",
                        "type":"Text"
                    },
                    "children":[
                        {
                            "key": "4-1-0",
                            "data":{
                                "name":"Hotel.pdf",
                                "size":"10kb",
                                "type":"PDF"
                            }
                        },
                        {
                            "key": "4-1-1",
                            "data":{
                                "name":"Flight.pdf",
                                "size":"5kb",
                                "type":"PDF"
                            }
                        }
                    ]
                }
            ]
        },
        {
            "key": "5",
            "data": {
                "name":"Main",
                "size":"50kb",
                "type":"Folder"
            },
            "children":[
                {
                    "key": "5-0",
                    "data":{
                        "name":"bin",
                        "size":"50kb",
                        "type":"Link"
                    }
                },
                {
                    "key": "5-1",
                    "data":{
                        "name":"etc",
                        "size":"100kb",
                        "type":"Link"
                    }
                },
                {
                    "key": "5-2",
                    "data":{
                        "name":"var",
                        "size":"100kb",
                        "type":"Link"
                    }
                }
            ]
        },
        {
            "key": "6",
            "data":{
                "name":"Other",
                "size":"5kb",
                "type":"Folder"
            },
            "children":[
                {
                    "key": "6-0",
                    "data":{
                        "name":"todo.txt",
                        "size":"3kb",
                        "type":"Text"
                    }
                },
                {
                    "key": "6-1",
                    "data":{
                        "name":"logo.png",
                        "size":"2kb",
                        "type":"Picture"
                    }
                }
            ]
        },
        {
            "key": "7",
            "data":{
                "name":"Pictures",
                "size":"150kb",
                "type":"Folder"
            },
            "children":[
                {
                    "key": "7-0",
                    "data":{
                        "name":"barcelona.jpg",
                        "size":"90kb",
                        "type":"Picture"
                    }
                },
                {
                    "key": "7-1",
                    "data":{
                        "name":"primevue.png",
                        "size":"30kb",
                        "type":"Picture"
                    }
                },
                {
                    "key": "7-2",
                    "data":{
                        "name":"prime.jpg",
                        "size":"30kb",
                        "type":"Picture"
                    }
                }
            ]
        },
        {
            "key": "8",
            "data":{
                "name":"Videos",
                "size":"1500kb",
                "type":"Folder"
            },
            "children":[
                {
                    "key": "8-0",
                    "data":{
                        "name":"primefaces.mkv",
                        "size":"1000kb",
                        "type":"Video"
                    }
                },
                {
                    "key": "8-1",
                    "data":{
                        "name":"intro.avi",
                        "size":"500kb",
                        "type":"Video"
                    }
                }
            ]
        }
    ]
}

Dynamic Columns

Column components can be dynamically generated using a v-for as well.

<TreeTable :value="nodes">
    <Column v-for="col of columns" :key="col.field"
        :field="col.field" :header="col.header" :expander="col.expander"></Column>
</TreeTable>
import NodeService from '../../service/NodeService';

export default {
    data() {
        return {
            nodes: null,
            columns: null
        }
    },
    nodeService: null,
    created() {
        this.nodeService = new NodeService();

        this.columns = [
            {field: 'name', header: 'Vin', expander: true},
            {field: 'size', header: 'Size'},
            {field: 'type', header: 'Type'}
        ];
    },
    mounted() {
        this.nodeService.getTreeTableNodes().then(data => this.nodes = data);
    }
}

Programmatic Control

Tree state can be controlled programmatically with the expandedKeys property that defines the keys that are expanded. This property is a Map instance whose key is the key of a node and value is a boolean. Note that expandedKeys also supports two-way binding with the v-model directive.

Example below expands and collapses all nodes with buttons.

<div style="margin-bottom: 1em">
    <Button type="button" icon="pi pi-plus" label="Expand All" @click="expandAll" />
    <Button type="button" icon="pi pi-minus" label="Collapse All" @click="collapseAll" />
</div>
<TreeTable :value="nodes" :expandedKeys="expandedKeys">
    <Column field="name" header="Name" :expander="true"></Column>
    <Column field="size" header="Size"></Column>
    <Column field="type" header="Type"></Column>
</TreeTable>
import NodeService from '../../service/NodeService';

export default {
    data() {
        return {
            nodes: null,
            expandedKeys: {}
        }
    },
    nodeService: null,
    created() {
        this.nodeService = new NodeService();
    },
    mounted() {
        this.nodeService.getTreeTableNodes().then(data => this.nodes = data);
    },
    methods: {
        expandAll() {
            for (let node of this.nodes) {
                this.expandNode(node);
            }

            this.expandedKeys = {...this.expandedKeys};
        },
        collapseAll() {
            this.expandedKeys = {};
        },
        expandNode(node) {
            if (node.children && node.children.length) {
                this.expandedKeys[node.key] = true;

                for (let child of node.children) {
                    this.expandNode(child);
                }
            }
        }
    }
}

To display some nodes as expanded by default, simply add their keys to the map.

import NodeService from '../../service/NodeService';

export default {
    data() {
        return {
            nodes: null,
            expandedKeys: {}
        }
    },
    nodeService: null,
    created() {
        this.nodeService = new NodeService();
    },
    mounted() {
        this.nodeService.getTreeTableNodes().then(data => {
            this.nodes = data;
            this.expandedKeys[this.nodes[0].key] = true;
            this.expandedKeys[this.nodes[1].key] = true;
        });
    }
}

Templating

Field data of a corresponding node is displayed as the cell content by default, this can be customized using a body template where current node data and column properties are passed via the slot props. On the other hand, header and footer sections of a column can either be defined with the properties or the templates. Similarly TreeTable itself also provides header and footer properties along with the templates for the main header and footer of the table.

<TreeTable :value="nodes">
    <template #header>
        FileSystem
    </template>
    <Column field="name" header="Name" :expander="true"></Column>
    <Column field="size" header="Size"></Column>
    <Column field="type" header="Type"></Column>
    <Column headerStyle="width: 8em" bodyStyle="text-align: center">
        <template #header>
            <Button type="button" icon="pi pi-cog"></Button>
        </template>
        <template #body="slotProps">
            <Button type="button" icon="pi pi-search" class="p-button-success" style="margin-right: .5em"></Button>
            <Button type="button" icon="pi pi-pencil" class="p-button-warning"></Button>
        </template>
    </Column>
    <template #footer>
        <div style="text-align:left">
            <Button icon="pi pi-refresh" />
        </div>
    </template>
</TreeTable>

Sizes

In addition to the regular table, a smal and a large version are available with different paddings. For a table with smaller paddings use p-treetable-sm class and for a larger one use p-treetable-lg.

<TreeTable :value="nodes" class="p-treetable-sm">
    <template #header>
        Small Table
    </template>
    <Column field="name" header="Name" :expander="true"></Column>
    <Column field="size" header="Size"></Column>
    <Column field="type" header="Type"></Column>
</TreeTable>

<TreeTable :value="nodes">
    <template #header>
        Small Table
    </template>
    <Column field="name" header="Name" :expander="true"></Column>
    <Column field="size" header="Size"></Column>
    <Column field="type" header="Type"></Column>
</TreeTable>

<TreeTable :value="nodes" class="p-treetable-lg">
    <template #header>
        Small Table
    </template>
    <Column field="name" header="Name" :expander="true"></Column>
    <Column field="size" header="Size"></Column>
    <Column field="type" header="Type"></Column>
</TreeTable>

Pagination

Pagination is enabled by setting paginator property to true and defining the rows property defines the number of rows per page. See the Paginator for the available customization options such as paginator templates, page links, rows per page options and more which can be passed through the TreeTable.

<TreeTable :value="nodes" :paginator="true" :rows="10">
    <Column field="name" header="Name" :expander="true"></Column>
    <Column field="size" header="Size"></Column>
    <Column field="type" header="Type"></Column>
</TreeTable>

paginatorLeft and paginatorLeft templates are available to specify custom content at the left and right side.

<TreeTable :value="nodes" :paginator="true" :rows="10">
    <Column field="name" header="Name" :expander="true"></Column>
    <Column field="size" header="Size"></Column>
    <Column field="type" header="Type"></Column>
    <template #paginatorLeft>
        <Button type="button" icon="pi pi-refresh" />
    </template>
    <template #paginatorRight>
        <Button type="button" icon="pi pi-cloud" />
    </template>
</TreeTable>

Paginator can also be programmed programmatically using a binding to the first property that defines the index of the first element to display. For example setting first to zero will reset the paginator to the very first page. This property also supports the v-model directive in case you'd like your binding to be updated whenever the user changes the page.

<TreeTable :value="nodes" :paginator="true" :rows="10" :first="firstRecordIndex">
    <Column field="name" header="Name" :expander="true"></Column>
    <Column field="size" header="Size"></Column>
    <Column field="type" header="Type"></Column>
</TreeTable>

Sorting

Enabling sortable property at column component would be enough to make a column sortable. The property to use when sorting is the field by default and can be customized using the sortField.

<TreeTable :value="nodes" sortMode="single">
    <Column field="name" header="Name" :expander="true" :sortable="true"></Column>
    <Column field="size" header="Size" :sortable="true"></Column>
    <Column field="type" header="Type" :sortable="true"></Column>
</TreeTable>

By default sorting is executed on the clicked column only. To enable multiple field sorting, set sortMode property to "multiple" and use metakey when clicking on another column.

<TreeTable :value="nodes" sortMode="multiple">
    <Column field="name" header="Name" :expander="true" :sortable="true"></Column>
    <Column field="size" header="Size" :sortable="true"></Column>
    <Column field="type" header="Type" :sortable="true"></Column>
</TreeTable>

In case you'd like to display the table as sorted per a single column by default on mount or programmatically apply sort, use sortField and sortOrder properties. These two properties also support the v-model directive to get updated when the user applies sort a column.

<TreeTable :value="nodes" sortField="size" :sortOrder="1"">
    <Column field="name" header="Name" :expander="true" :sortable="true"></Column>
    <Column field="size" header="Size" :sortable="true"></Column>
    <Column field="type" header="Type" :sortable="true"></Column>
</TreeTable>

<TreeTable :value="nodes" sortMode="single" sortField="dynamicSortField" :sortOrder="dynamicSortOrder">
    <Column field="name" header="Name" :expander="true" :sortable="true"></Column>
    <Column field="size" header="Size" :sortable="true"></Column>
    <Column field="type" header="Type" :sortable="true"></Column>
</TreeTable>

In multiple mode, use the multiSortMeta property and bind an array of SortMeta objects instead.

<TreeTable :value="nodes" sortMode="multiple" :multiSortMeta="multiSortMeta">
    <Column field="name" header="Name" :expander="true" :sortable="true"></Column>
    <Column field="size" header="Size" :sortable="true"></Column>
    <Column field="type" header="Type" :sortable="true"></Column>
</TreeTable>
data() {
    return {
        multiSortMeta: [
            {field: 'year', order: 1},
            {field: 'brand', order: -1}
        ]
    }
}

Filtering

Filtering is enabled by defining a filter template per column to populate the filters property of the TreeTable. The filters property should be an key-value object where keys are the field name and the value is the filter value. The filter template receives the column properties via the slotProps and accepts any form element as the filter element. Default match mode is "startsWith" and this can be configured per column using the filterMatchMode property that also accepts "contains", "endsWith", "equals", "notEquals" and "in" as available modes.

Optionally a global filter is available to search against all the fields, in this case the special global keyword should be the property to be populated.

In addition filterMode specifies the filtering strategy. In lenient mode when the query matches a node, children of the node are not searched further as all descendants of the node are included. On the other hand, in strict mode when the query matches a node, filtering continues on all descendants.

<TreeTable :value="nodes" :filters="filters" filterMode="lenient">
    <template #header>
        <div style="text-align: right">
            <i class="pi pi-search" style="margin: 4px 4px 0px 0px;"></i>
            <InputText v-model="filters['global']" placeholder="Global Search" size="50" />
        </div>
    </template>
    <Column field="name" header="Name" :expander="true">
        <template #filter>
            <InputText type="text" v-model="filters['name']" class="p-column-filter" />
        </template>
    </Column>
    <Column field="size" header="Size">
        <template #filter>
            <InputText type="text" v-model="filters['size']" class="p-column-filter" />
        </template>
    </Column>
    <Column field="type" header="Type">
        <template #filter>
            <InputText type="text" v-model="filters['type']" class="p-column-filter" />
        </template>
    </Column>
</TreeTable>
import NodeService from '../../service/NodeService';

export default {
    data() {
        return {
            filters: {},
            nodes: null
        }
    },
    nodeService: null,
    created() {
        this.nodeService = new NodeService();
    },
    mounted() {
        this.nodeService.getTreeTableNodes().then(data => this.nodes = data);
    }
}

Selection

Tree supports single, multiple and checkbox selection modes. Define the selectionKeys with the v-model operator and the selectionMode properties to enable the selection. By default in multiple selection mode, metaKey is necessary to add to existing selections however this can be configured with metaKeySelection property. Note that in touch enabled devices, Tree does not require metaKey. In addition selection on a particular node can be disabled if the selectable is false on the node instance.

Similarly to the expandedKeys, selectionKeys is a Map instance whose key is the key of a node and value is a boolean in "single" and "multiple" cases. On the other hand in "checkbox" mode, instead of a boolean, value should be an object that has "checked" and "partialChecked" properties to represent the checked state of a node.

<TreeTable :value="nodes" selectionMode="multiple" v-model:selectionKeys="selectedKeys1">
    <Column field="name" header="Name" :expander="true"></Column>
    <Column field="size" header="Size"></Column>
    <Column field="type" header="Type"></Column>
</TreeTable>

<h3>Multiple Selection without MetaKey</h3>
<TreeTable :value="nodes" selectionMode="multiple" v-model:selectionKeys="selectedKeys2" :metaKeySelection="false">
    <Column field="name" header="Name" :expander="true"></Column>
    <Column field="size" header="Size"></Column>
    <Column field="type" header="Type"></Column>
</TreeTable>

<h3>Checkbox Selection</h3>
<TreeTable :value="nodes" selectionMode="checkbox" v-model:selectionKeys="selectedKeys3">
    <Column field="name" header="Name" :expander="true"></Column>
    <Column field="size" header="Size"></Column>
    <Column field="type" header="Type"></Column>
</TreeTable>

<h3>Events</h3>
<TreeTable :value="nodes" selectionMode="single" v-model:selectionKeys="selectedKey2"
    @node-select="onNodeSelect" @node-unselect="onNodeUnselect">
    <Column field="name" header="Name" :expander="true"></Column>
    <Column field="size" header="Size"></Column>
    <Column field="type" header="Type"></Column>
</TreeTable>
import NodeService from '../../service/NodeService';

export default {
    data() {
        return {
            selectedKey1: null,
            selectedKey2: null,
            selectedKeys1: null,
            selectedKeys2: null,
            selectedKeys3: null,
            nodes: null
        }
    },
    nodeService: null,
    created() {
        this.nodeService = new NodeService();
    },
    mounted() {
        this.nodeService.getTreeTableNodes().then(data => this.nodes = data);
    },
    methods: {
        onNodeSelect(node) {
            this.$toast.add({severity:'success', summary: 'Node Selected', detail: node.data.name, life: 3000});
        },
        onNodeUnselect(node) {
            this.$toast.add({severity:'success', summary: 'Node Unselected', detail: node.data.name, life: 3000});
        }
    }
}

To display some nodes as selected by default, simply add their keys to the map.

import NodeService from '../../service/NodeService';

export default {
    data() {
        return {
            selectedKey1: null,
            selectedKey2: null,
            selectedKeys1: null,
            selectedKeys2: null,
            selectedKeys3: null,
            nodes: null
        }
    },
    nodeService: null,
    created() {
        this.nodeService = new NodeService();
    },
    mounted() {
        this.nodeService.getTreeTableNodes().then(data => {
            this.nodes = data;

            //single preselection
            this.selectedKey1[this.nodes[0].key] = true;

            //multiple preselection
            this.selectedKeys2[this.nodes[0].key] = true;
            this.selectedKeys2[this.nodes[1].key] = true;

            //checkbox preselection
            this.selectedKeys2[this.nodes[1].key] = {checked: true};
        });
    }
}

Lazy

Lazy Loading is handy to deal with huge datasets. Idea is instead of loading the whole tree, load nodes on demand when necessary. The important part when lazy loading nodes is setting leaf to true on a node instance so that even without children, tree would render an expand icon. Example below uses an in memory collection to mimic a lazy loading scenario with timeouts.

In addition lazy loading of root level nodes is implemented by handling pagination and sorting using page and sort events by making a remote query using the information passed to the events such as first offset, number of rows and sort field for ordering. Filtering is handled differently as filter elements are defined using templates, use the event you prefer on your form elements such as input, change, blur to make a remote call by passing the filters property to update the displayed data. Note that, in lazy filtering, totalRecords should also be updated to align the data with the paginator. .

Example below uses an in memory collection to mimic a lazy loading scenario with timeouts.

<TreeTable :value="nodes" :lazy="true" :paginator="true" :rows="rows" :loading="loading"
    @node-expand="onExpand" @page="onPage" :totalRecords="totalRecords">
    <Column field="name" header="Name" :expander="true"></Column>
    <Column field="size" header="Size"></Column>
    <Column field="type" header="Type"></Column>
</TreeTable>
export default {
    data() {
        return {
            nodes: null,
            rows: 10,
            loading: false,
            totalRecords: 0
        }
    },
    mounted() {
        this.loading = true;

        setTimeout(() => {
            this.loading = false;
            this.nodes = this.loadNodes(0, this.rows);
            this.totalRecords = 1000;
        }, 1000);
    },
    methods: {
        onExpand(node) {
            if (!node.children) {
                this.loading = true;

                setTimeout(() => {
                    let lazyNode = {...node};

                    lazyNode.children = [
                        {
                            data: {
                                name: lazyNode.data.name + ' - 0',
                                size: Math.floor(Math.random() * 1000) + 1 + 'kb',
                                type: 'File'
                            },
                        },
                        {
                            data: {
                                name: lazyNode.data.name + ' - 1',
                                size: Math.floor(Math.random() * 1000) + 1 + 'kb',
                                type: 'File'
                            }
                        }
                    ];

                    let nodes = this.nodes.map(n => {
                        if (n.key === node.key) {
                            n = lazyNode;
                        }

                        return n;
                    });

                    this.loading = false;
                    this.nodes = nodes;
                }, 250);
            }
        },
        onPage(event) {
            this.loading = true;

            //imitate delay of a backend call
            setTimeout(() => {
                this.loading = false;
                this.nodes = this.loadNodes(event.first, this.rows);
            }, 1000);
        },
        loadNodes(first, rows) {
            let nodes = [];

            for(let i = 0; i < rows; i++) {
                let node = {
                    key: (first + i),
                    data: {
                        name: 'Item ' + (first + i),
                        size: Math.floor(Math.random() * 1000) + 1 + 'kb',
                        type: 'Type ' + (first + i)
                    },
                    leaf: false
                };

                nodes.push(node);
            }

            return nodes;
        }
    }
}

Column Resize

Columns can be resized using drag drop by setting the resizableColumns to true. There are two resize modes; "fit" and "expand". Fit is the default one and the overall table width does not change when a column is resized. In "expand" mode, table width also changes along with the column width. column-resize-end is a callback that passes the resized column header and delta change as a parameter.

<h3>Fit Mode</h3>
<TreeTable :value="nodes" :resizableColumns="true" columnResizeMode="fit">
    <Column field="name" header="Name" :expander="true"></Column>
    <Column field="size" header="Size"></Column>
    <Column field="type" header="Type"></Column>
</TreeTable>

<h3>Expand Mode</h3>
<TreeTable :value="nodes" :resizableColumns="true" columnResizeMode="expand">
    <Column field="name" header="Name" :expander="true"></Column>
    <Column field="size" header="Size"></Column>
    <Column field="type" header="Type"></Column>
</TreeTable>

It is important to note that when you need to change column widths, since table width is 100%, giving fixed pixel widths does not work well as browsers scale them, instead give percentage widths.

<TreeTable :value="nodes" :resizableColumns="true">
    <Column field="name" header="Name" :expander="true" headerStyle="width: 20%"></Column>
    <Column field="size" header="Size" headerStyle="width: 40%"></Column>
    <Column field="type" header="Type headerStyle="width: 40%""></Column>
</TreeTable>

Responsive

TreeTable display can be optimized according to screen sizes, this example demonstrates a demo where columns are stacked on small screens.

<TreeTable :value="nodes" class="p-treetable-responsive">
    <template #header>
        Responsive
    </template>
    <Column field="name" header="Name" :expander="true">
            <template #body="slotProps">
            {{slotProps.node.data.name}}
            <span class="sm-visible">{{slotProps.node.data.size}}</span>
            <span class="sm-visible">{{slotProps.node.data.type}}</span>
        </template>
    </Column>
    <Column field="size" header="Size" headerClass="sm-invisible" bodyClass="sm-invisible"></Column>
    <Column field="type" header="Type" headerClass="sm-invisible" bodyClass="sm-invisible"></Column>
</TreeTable>
import NodeService from '../../service/NodeService';

export default {
    data() {
        return {
            nodes: null
        }
    },
    nodeService: null,
    created() {
        this.nodeService = new NodeService();
    },
    mounted() {
        this.nodeService.getTreeTableNodes().then(data => this.nodes = data);
    }
}
.sm-visible {
    display: none;
}

@media screen and (max-width: 40em) {
    ::v-deep(.sm-visible) {
        display: inline;
        margin-right: .5rem;
    }
}

Theming

TreeTable supports various themes featuring Material, Bootstrap, Fluent as well as your own custom themes via the Designer tool.

Resources

Visit the PrimeVue TreeTable showcase for demos and documentation.