Skip to content

Table tree cell

The table tree cell renders a tree branch, a round marker and its content in a single cell. Place it instead of a Cell in the column that should show the hierarchy, and give that column a (usually empty) Header. It draws the guide lines automatically from the level of the surrounding tree cells, so all you provide is the depth and whether a node is expandable.

Kind
Size
src
Folder
components
Folder
FluxButton.vue
File
4 KB
FluxTable.vue
File
12 KB
index.ts
File
1 KB
package.json
File
2 KB

Expansion stays in your app

The tree cell holds no expansion state. It only draws the marker and emits toggle when an expandable marker is clicked. Keep your own expanded/collapsed state, flatten your tree into the rows you want to render, and pass each node its level and is-expanded. Collapsing a node simply means rendering fewer rows.

WARNING

This component is best used within a Row, as the first cell of a column that has a matching Header.

Required icons

angle-right

Props

level: number
The depth of this node, starting at `0` for a root. The table derives the guide lines from the levels of the visible tree cells around it.

color?: FluxColor | string
The colour of the marker. Accepts a `FluxColor` or any custom CSS colour string. Defaults to gray, with no distinction between expandable and leaf markers.
Default: gray

is-expandable?: boolean
Renders the marker as a toggle with a chevron. Leave it off to render a plain leaf marker.

is-expanded?: boolean
Whether the node is currently expanded. Controls the chevron direction only; your application decides which child rows are rendered.

Emits

toggle: []
Triggered when the marker of an expandable node is clicked. Expansion state is owned by your application, so handle this by flipping your own expanded state and re-rendering the visible rows.

Slots

default
The content of the cell, shown after the marker.

Examples

File tree

A collapsible file tree whose expanded state lives in the page, driving which rows render.

Kind
Size
src
Folder
components
Folder
FluxButton.vue
File
4 KB
FluxTable.vue
File
12 KB
index.ts
File
1 KB
package.json
File
2 KB

<template>
    <FluxPane>
        <FluxTable is-hoverable>
            <template #header>
                <FluxTableHeader :min-width="270"/>
                <FluxTableHeader is-shrinking>Kind</FluxTableHeader>
                <FluxTableHeader
                    is-shrinking
                    is-numeric>Size</FluxTableHeader>
            </template>

            <FluxTableRow
                v-for="row in rows"
                :key="row.id">
                <FluxTableTreeCell
                    :level="row.level"
                    :is-expandable="row.hasChildren"
                    :is-expanded="!collapsed.has(row.id)"
                    @toggle="toggle(row.id)">
                    {{ row.name }}
                </FluxTableTreeCell>
                <FluxTableCell>{{ row.hasChildren ? 'Folder' : 'File' }}</FluxTableCell>
                <FluxTableCell is-numeric>{{ row.size ?? '—' }}</FluxTableCell>
            </FluxTableRow>
        </FluxTable>
    </FluxPane>
</template>

<script
    setup
    lang="ts">
    import { FluxPane, FluxTable, FluxTableCell, FluxTableHeader, FluxTableRow, FluxTableTreeCell } from '@flux-ui/components';
    import { computed, reactive } from 'vue';

    type Node = {
        readonly id: string;
        readonly name: string;
        readonly size?: string;
        readonly children?: Node[];
    };

    type Row = {
        readonly id: string;
        readonly name: string;
        readonly size?: string;
        readonly level: number;
        readonly hasChildren: boolean;
    };

    const tree: Node[] = [
        {
            id: 'src',
            name: 'src',
            children: [
                {
                    id: 'components',
                    name: 'components',
                    children: [
                        {id: 'button', name: 'FluxButton.vue', size: '4 KB'},
                        {id: 'table', name: 'FluxTable.vue', size: '12 KB'}
                    ]
                },
                {id: 'index', name: 'index.ts', size: '1 KB'}
            ]
        },
        {id: 'package', name: 'package.json', size: '2 KB'}
    ];

    const collapsed = reactive(new Set<string>());

    const rows = computed<Row[]>(() => {
        const flat: Row[] = [];

        const walk = (nodes: Node[], level: number): void => {
            for (const node of nodes) {
                const hasChildren = !!node.children?.length;

                flat.push({id: node.id, name: node.name, size: node.size, level, hasChildren});

                if (hasChildren && !collapsed.has(node.id)) {
                    walk(node.children!, level + 1);
                }
            }
        };

        walk(tree, 0);

        return flat;
    });

    function toggle(id: string): void {
        if (collapsed.has(id)) {
            collapsed.delete(id);
        } else {
            collapsed.add(id);
        }
    }
</script>

Coloured markers

The same tree with a color per level, mixing FluxColor names with your own palette.

Members
Pool West
Laurentius
Bernade
12
Cornelis
8
De Reger
5
Pool East
21

<template>
    <FluxPane>
        <FluxTable is-hoverable>
            <template #header>
                <FluxTableHeader :min-width="270"/>
                <FluxTableHeader is-shrinking>Members</FluxTableHeader>
            </template>

            <FluxTableRow
                v-for="row in rows"
                :key="row.id">
                <FluxTableTreeCell
                    :level="row.level"
                    :color="levelColors[row.level] ?? 'gray'"
                    :is-expandable="row.hasChildren"
                    :is-expanded="!collapsed.has(row.id)"
                    @toggle="toggle(row.id)">
                    {{ row.name }}
                </FluxTableTreeCell>
                <FluxTableCell>{{ row.members ?? '—' }}</FluxTableCell>
            </FluxTableRow>
        </FluxTable>
    </FluxPane>
</template>

<script
    setup
    lang="ts">
    import { FluxPane, FluxTable, FluxTableCell, FluxTableHeader, FluxTableRow, FluxTableTreeCell } from '@flux-ui/components';
    import type { FluxColor } from '@flux-ui/types';
    import { computed, reactive } from 'vue';

    type Node = {
        readonly id: string;
        readonly name: string;
        readonly members?: number;
        readonly children?: Node[];
    };

    type Row = {
        readonly id: string;
        readonly name: string;
        readonly members?: number;
        readonly level: number;
        readonly hasChildren: boolean;
    };

    const levelColors: FluxColor[] = ['danger', 'warning', 'info'];

    const tree: Node[] = [
        {
            id: 'west',
            name: 'Pool West',
            children: [
                {
                    id: 'laurentius',
                    name: 'Laurentius',
                    children: [
                        {id: 'bernade', name: 'Bernade', members: 12},
                        {id: 'cornelis', name: 'Cornelis', members: 8}
                    ]
                },
                {id: 'reger', name: 'De Reger', members: 5}
            ]
        },
        {id: 'east', name: 'Pool East', members: 21}
    ];

    const collapsed = reactive(new Set<string>());

    const rows = computed<Row[]>(() => {
        const flat: Row[] = [];

        const walk = (nodes: Node[], level: number): void => {
            for (const node of nodes) {
                const hasChildren = !!node.children?.length;

                flat.push({id: node.id, name: node.name, members: node.members, level, hasChildren});

                if (hasChildren && !collapsed.has(node.id)) {
                    walk(node.children!, level + 1);
                }
            }
        };

        walk(tree, 0);

        return flat;
    });

    function toggle(id: string): void {
        if (collapsed.has(id)) {
            collapsed.delete(id);
        } else {
            collapsed.add(id);
        }
    }
</script>

Custom colours

Passing arbitrary CSS colour strings, so each node carries its own hex value.

Owner
Design system
Tokens
Colour
Anouk
Typography
Milan
Icons
Sara
Brand guidelines
Team

<template>
    <FluxPane>
        <FluxTable is-hoverable>
            <template #header>
                <FluxTableHeader :min-width="270"/>
                <FluxTableHeader is-shrinking>Owner</FluxTableHeader>
            </template>

            <FluxTableRow
                v-for="row in rows"
                :key="row.id">
                <FluxTableTreeCell
                    :level="row.level"
                    :color="row.color"
                    :is-expandable="row.hasChildren"
                    :is-expanded="!collapsed.has(row.id)"
                    @toggle="toggle(row.id)">
                    {{ row.name }}
                </FluxTableTreeCell>
                <FluxTableCell>{{ row.owner ?? '—' }}</FluxTableCell>
            </FluxTableRow>
        </FluxTable>
    </FluxPane>
</template>

<script
    setup
    lang="ts">
    import { FluxPane, FluxTable, FluxTableCell, FluxTableHeader, FluxTableRow, FluxTableTreeCell } from '@flux-ui/components';
    import { computed, reactive } from 'vue';

    type Node = {
        readonly id: string;
        readonly name: string;
        readonly color: string;
        readonly owner?: string;
        readonly children?: Node[];
    };

    type Row = {
        readonly id: string;
        readonly name: string;
        readonly color: string;
        readonly owner?: string;
        readonly level: number;
        readonly hasChildren: boolean;
    };

    const tree: Node[] = [
        {
            id: 'design',
            name: 'Design system',
            color: '#8b5cf6',
            children: [
                {
                    id: 'tokens',
                    name: 'Tokens',
                    color: '#ec4899',
                    children: [
                        {id: 'color', name: 'Colour', color: '#f97316', owner: 'Anouk'},
                        {id: 'type', name: 'Typography', color: '#f97316', owner: 'Milan'}
                    ]
                },
                {id: 'icons', name: 'Icons', color: '#ec4899', owner: 'Sara'}
            ]
        },
        {id: 'brand', name: 'Brand guidelines', color: '#8b5cf6', owner: 'Team'}
    ];

    const collapsed = reactive(new Set<string>());

    const rows = computed<Row[]>(() => {
        const flat: Row[] = [];

        const walk = (nodes: Node[], level: number): void => {
            for (const node of nodes) {
                const hasChildren = !!node.children?.length;

                flat.push({id: node.id, name: node.name, color: node.color, owner: node.owner, level, hasChildren});

                if (hasChildren && !collapsed.has(node.id)) {
                    walk(node.children!, level + 1);
                }
            }
        };

        walk(tree, 0);

        return flat;
    });

    function toggle(id: string): void {
        if (collapsed.has(id)) {
            collapsed.delete(id);
        } else {
            collapsed.add(id);
        }
    }
</script>

Used components