Skip to content

Data table

The data table organizes large sets of data into rows and columns, layering sorting, filtering, pagination, selection, grouping and expandable rows on top of a plain Table. It is built for server-driven data, so you feed it one page of rows at a time.

Person
Status
Naomi SchillerTerrence44@yahoo.com
Inactive
Khalil HintzAlexandrine_Reilly6@yahoo.com
Inactive
Hazel KlingRandal_Hartmann@hotmail.com
Inactive
Stacey Dibbert PhDCheryl.Runolfsdottir73@gmail.com
Inactive
Cecilia KohlerAlessandro.Pouros-Pollich23@hotmail.com
Active

Required icons

angle-right
arrow-down-1-9
arrow-down-a-z
arrow-down-short-wide
arrow-up-9-1
arrow-up-a-z
arrow-up-arrow-down
arrow-up-wide-short
check
circle-xmark
minus

Props

items: T[]
The data to show in the table. This should already be the current page's data. Pagination is handled server-side. Pass the subset of items for the active page here.

is-filled?: boolean
Renders a filler row that stretches to the bottom of the table when the page has fewer items than `per-page`.

group-by?: (item: T) => string | number
An accessor that returns the group id for each item. When set, rows are grouped and the `group` slot is rendered as the header for each group.

collapse-mode?: 'unmount' | 'hide'
How a collapsed group's rows are handled. Use `unmount` to drop them from the DOM, or `hide` to keep them mounted and hidden so toggling groups with many rich rows stays instant. Only applies when `group-by` is set.
Default: unmount

is-hoverable?: boolean
Enable a hover state for each row.

is-loading?: boolean
Show that the data table is loading.

is-sticky?: boolean
If the table header, including the filter bar, sticks to the top while scrolling.

limits: number[]
The available options for the pagination limit.

page: number
The currently active page, starting from 1. Used for slot bindings and the pagination bar display only; it does not slice the items array.

per-page: number
The number of rows to show per page.

expand-mode?: 'single' | 'multiple'
How expandable rows behave. Use `multiple` to allow several rows open at once, or `single` to keep only one open. Requires `unique-key` and the `expandable` slot.
Default: multiple

can-expand?: (item: T) => boolean
A predicate that decides whether a row shows its expand toggle. Return `false` to hide the toggle for rows without detail content. Requires the `expandable` slot.

row-color?: (item: T) => FluxColor | undefined
An accessor that returns the color for each row, for example `danger` for a row that failed. Return `undefined` to leave a row untinted. A selected row keeps its selected styling.

selection-mode?: 'single' | 'multiple'
Enables row selection. Use `single` for at most one selected row, or `multiple` for an array of selected rows. Requires `unique-key` to be set.

total: number
The total number of items in the data set.

unique-key?: string
The unique key for each row.

Emits

limit: [number]
Triggered when the per-page limit is changed by the user.

navigate: [number]
Triggered when the user navigates to a different page.

row-click: [T, number, MouseEvent]
Triggered when a row is activated by a click, or by pressing `Enter`/`Space` while the row is focused, with the row's item, the index of the clicked column and the original event. Activations from interactive elements within the row (buttons, links, inputs) are ignored. Not triggered when `selection-mode` is set, since there the row activation toggles the selection instead. The column index is the raw cell index (the selection and expand cells are counted), or `-1` when the row is activated by keyboard.

update:selected: [string | number | null | (string | number)[]]
Triggered when the selection changes. The payload type matches `selection-mode`, so it is a single id (or `null`) for `single` and an array of ids for `multiple`.

update:expanded: [(string | number)[]]
Triggered when the set of expanded rows changes, with the array of expanded ids.

update:collapsed-groups: [(string | number)[]]
Triggered when a group is collapsed or expanded, with the array of collapsed group ids.

Slots

empty
Renders in place of the rows when there is no data and the table is not loading. Defaults to a centered message.

loading ({
    readonly page: number;
    readonly per-page: number;
    readonly total: number;
})

Replaces the default spinner overlay while `is-loading` is set. Renders inside the table body on the table's column grid, typically with skeleton rows. Use `per-page` to render the expected number of rows. The regular rows are not rendered while this slot shows.

selection ({
    selected(string | number)[];
    readonly count: number;
    clear() => void;
})

Renders as a full-width bar in place of the `filter` row while at least one row is selected, typically with the selection count and bulk actions. Requires `selection-mode`.

filter ({
    readonly page: number;
    readonly per-page: number;
    readonly items: T[];
    readonly total: number;
})

Renders above the table header, typically a `FluxFilterBar` for filtering the data set.

footer ({
    readonly page: number;
    readonly per-page: number;
    readonly items: T[];
    readonly total: number;
})

The footer of the data table.

header ({
    readonly page: number;
    readonly per-page: number;
    readonly items: T[];
    readonly total: number;
})

The header of the data table.

pagination ({
    readonly page: number;
    readonly per-page: number;
    readonly items: T[];
    readonly total: number;
})

Replaces the default pagination bar, useful when you want to render a custom paginator.

expandable ({
    readonly index: number;
    readonly item: T;
    readonly is-expanded: boolean;
    toggle() => void;
})

When provided, each row gets an expand toggle and this slot renders the expanded detail content spanning the full table width. Requires `unique-key`.

group ({
    readonly id: string | number;
    readonly index: number;
    readonly items: T[];
    readonly is-expanded: boolean;
    toggle() => void;
})

Renders the header row for each group. Only used when `group-by` is set. Render a `FluxTableGroup` here, wiring `is-expanded` and `toggle` to make the group collapsible.

[key: string] ({
    readonly index: number;
    readonly page: number;
    readonly per-page: number;
    readonly item: T;
    readonly items: T[];
    readonly total: number;
    readonly is-selected: boolean;
})

A slot representing a cell for each visible row.

Server-side pagination

The data table does not paginate items internally. The items prop should contain only the rows for the currently active page, fetched from your server or API. The page and per-page props are used to drive the pagination bar and are exposed through slot bindings so you can display them, but the component never slices or filters the items array itself.

Fast group toggling

By default a collapsed group unmounts its rows, so re-expanding a group of rich rows (per-row flyouts, links, indicators) re-mounts all of them. Set collapse-mode="hide" to keep the rows mounted and hide them instead, making every toggle instant. Hidden rows are excluded from keyboard navigation and assistive technology, so there is no visible or behavioural change beyond the speed.

Examples

File manager

A data table that is used for file management.

Name
Owner
blaring_hearten.pngimage/png
Elbert Botsford-Koch
doubter.pngimage/png
Dr. Elisa Lynch
but_cork.pngimage/png
Hassan Goodwin
dampen_pine.pngimage/png
Joey Mohr
mechanically.pngimage/png
Arlene Konopelski

<template>
    <FluxPane>
        <FluxDataTable
            :items="dataSet"
            :limits="[]"
            :page="1"
            :per-page="20"
            :total="dataSet.length"
            is-hoverable>
            <template #header>
                <FluxTableHeader>Name</FluxTableHeader>
                <FluxTableHeader>Owner</FluxTableHeader>
                <FluxTableHeader is-shrinking/>
            </template>

            <template #name="{item: {name, type}}">
                <FluxTableCell>
                    <FluxFlex :gap="21">
                        <FluxBoxedIcon name="image"/>

                        <FluxFlex direction="vertical">
                            <strong>{{ name }}</strong>
                            <small>{{ type }}</small>
                        </FluxFlex>
                    </FluxFlex>
                </FluxTableCell>
            </template>

            <template #owner="{item: {owner}}">
                <FluxTableCell>{{ owner }}</FluxTableCell>
            </template>

            <template #actions="{}">
                <FluxTableCell>
                    <FluxTableActions>
                        <FluxAction icon="arrow-down-to-line"/>
                        <FluxAction icon="ellipsis-h"/>
                    </FluxTableActions>
                </FluxTableCell>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

<script
    lang="ts"
    setup>
    import { FluxAction, FluxBoxedIcon, FluxDataTable, FluxFlex, FluxPane, FluxTableActions, FluxTableCell, FluxTableHeader } from '@flux-ui/components';
    import { faker } from '@faker-js/faker';
    import { computed } from 'vue';

    const dataSet = computed(() => Array(5)
        .fill(null)
        .map((_, index) => ({
            id: index,
            name: faker.system.commonFileName('png'),
            owner: faker.person.fullName(),
            type: 'image/png'
        })));
</script>

Paginated

A data table that is split into pages.

Name
Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Item 7
Item 8
Item 9
Item 10
1–10 of 500

<template>
    <FluxPane>
        <FluxDataTable
            :items="visibleItems"
            is-hoverable
            :limits="[5, 10, 25, 50, 100]"
            :page="page"
            :per-page="perPage"
            :total="dataSet.length"
            @limit="limit => perPage = limit"
            @navigate="p => page = p">
            <template #header>
                <FluxTableHeader>Name</FluxTableHeader>
            </template>

            <template #name="{item: {name}}">
                <FluxTableCell>{{ name }}</FluxTableCell>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

<script
    lang="ts"
    setup>
    import { FluxDataTable, FluxPane, FluxTableCell, FluxTableHeader } from '@flux-ui/components';
    import { computed, ref, unref } from 'vue';

    const page = ref(1);
    const perPage = ref(10);

    const dataSet = computed(() => Array(500)
        .fill(null)
        .map((_, index) => ({
            id: index,
            name: `Item ${index + 1}`
        })));

    const visibleItems = computed(() => unref(dataSet).slice((unref(page) - 1) * unref(perPage), unref(page) * unref(perPage)) ?? []);
</script>

Custom pagination

A data table whose pagination slot renders a compact pager instead of the default bar.

Name
Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Item 7
Item 8
Item 9
Item 10

<template>
    <FluxPane>
        <FluxDataTable
            :items="visibleItems"
            is-hoverable
            :limits="[10]"
            :page="page"
            :per-page="perPage"
            :total="dataSet.length"
            @navigate="p => page = p">
            <template #header>
                <FluxTableHeader>Name</FluxTableHeader>
            </template>

            <template #name="{item: {name}}">
                <FluxTableCell>{{ name }}</FluxTableCell>
            </template>

            <template #pagination="{total}">
                <FluxPagination
                    arrows
                    is-compact
                    :page="page"
                    :per-page="perPage"
                    :total="total"
                    @navigate="p => page = p"/>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

<script
    lang="ts"
    setup>
    import { FluxDataTable, FluxPagination, FluxPane, FluxTableCell, FluxTableHeader } from '@flux-ui/components';
    import { computed, ref, unref } from 'vue';

    const page = ref(1);
    const perPage = ref(10);

    const dataSet = computed(() => Array(500)
        .fill(null)
        .map((_, index) => ({
            id: index,
            name: `Item ${index + 1}`
        })));

    const visibleItems = computed(() => unref(dataSet).slice((unref(page) - 1) * unref(perPage), unref(page) * unref(perPage)) ?? []);
</script>

Clickable rows

A data table that emits row-click per row, while ignoring clicks on the action button inside a cell. Focused rows can be walked with the arrow keys and activated with Enter/Space.

Person
Role
Myrtice JohnstonTheodore.Kunde@gmail.com
Senior Markets Assistant
Donnie McClureDemarco.Strosin4@yahoo.com
Direct Integration Strategist
Kirk BeerLula_Nolan@yahoo.com
Forward Communications Agent
Mr. Gary KeelingJodi25@yahoo.com
International Mobility Analyst
Emma BednarKelton52@gmail.com
Human Quality Supervisor

<template>
    <FluxPane>
        <FluxDataTable
            :items="dataSet"
            :limits="[]"
            :page="1"
            :per-page="5"
            :total="dataSet.length"
            is-hoverable
            @row-click="onRowClick">
            <template #header>
                <FluxTableHeader>
                    Person
                </FluxTableHeader>

                <FluxTableHeader>
                    Role
                </FluxTableHeader>

                <FluxTableHeader is-shrinking/>
            </template>

            <template #name="{item: {name, email}}">
                <FluxTableCell content-direction="column">
                    <strong>{{ name }}</strong>
                    <small>{{ email }}</small>
                </FluxTableCell>
            </template>

            <template #role="{item: {role}}">
                <FluxTableCell>
                    {{ role }}
                </FluxTableCell>
            </template>

            <template #actions="{item}">
                <FluxTableCell>
                    <FluxTableActions>
                        <FluxSecondaryButton
                            label="Email"
                            @click="onEmail(item)"/>
                    </FluxTableActions>
                </FluxTableCell>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

<script
    lang="ts"
    setup>
    import { FluxDataTable, FluxPane, FluxSecondaryButton, FluxTableActions, FluxTableCell, FluxTableHeader, showSnackbar } from '@flux-ui/components';
    import { faker } from '@faker-js/faker';
    import { computed } from 'vue';

    type Person = {
        readonly id: number;
        readonly name: string;
        readonly email: string;
        readonly role: string;
    };

    const dataSet = computed<Person[]>(() => Array(5)
        .fill(null)
        .map((_, index) => ({
            id: index,
            name: faker.person.fullName(),
            email: faker.internet.email(),
            role: faker.person.jobTitle()
        })));

    function onRowClick(item: Person, columnIndex: number): void {
        showSnackbar({
            icon: 'user',
            message: `You clicked ${item.name} in column ${columnIndex}.`
        });
    }

    function onEmail(item: Person): void {
        showSnackbar({
            color: 'info',
            icon: 'paper-plane',
            message: `Composing an email to ${item.email}.`
        });
    }
</script>

Row colors

A data table that tints rows based on their status.

Invoice
Customer
Status
INV-2031
Alice Johnson
Paid
INV-2032
Bas Milius
Open
INV-2033
Carol White
Expiring soon
INV-2034
Daniel Brown
Overdue
INV-2035
Emma Davis
Paid

<template>
    <FluxPane>
        <FluxDataTable
            :items="dataSet"
            :limits="[]"
            :page="1"
            :per-page="5"
            :row-color="getRowColor"
            :total="dataSet.length"
            is-hoverable>
            <template #header>
                <FluxTableHeader>
                    Invoice
                </FluxTableHeader>

                <FluxTableHeader>
                    Customer
                </FluxTableHeader>

                <FluxTableHeader>
                    Status
                </FluxTableHeader>
            </template>

            <template #number="{item: {number}}">
                <FluxTableCell>
                    {{ number }}
                </FluxTableCell>
            </template>

            <template #customer="{item: {customer}}">
                <FluxTableCell>
                    {{ customer }}
                </FluxTableCell>
            </template>

            <template #status="{item: {status}}">
                <FluxTableCell>
                    {{ status }}
                </FluxTableCell>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

<script
    lang="ts"
    setup>
    import type { FluxColor } from '@flux-ui/types';
    import { FluxDataTable, FluxPane, FluxTableCell, FluxTableHeader } from '@flux-ui/components';

    type Invoice = {
        readonly id: number;
        readonly number: string;
        readonly customer: string;
        readonly status: 'Paid' | 'Open' | 'Expiring soon' | 'Overdue';
    };

    const dataSet: Invoice[] = [
        {id: 1, number: 'INV-2031', customer: 'Alice Johnson', status: 'Paid'},
        {id: 2, number: 'INV-2032', customer: 'Bas Milius', status: 'Open'},
        {id: 3, number: 'INV-2033', customer: 'Carol White', status: 'Expiring soon'},
        {id: 4, number: 'INV-2034', customer: 'Daniel Brown', status: 'Overdue'},
        {id: 5, number: 'INV-2035', customer: 'Emma Davis', status: 'Paid'}
    ];

    function getRowColor(item: Invoice): FluxColor | undefined {
        switch (item.status) {
            case 'Expiring soon':
                return 'warning';

            case 'Overdue':
                return 'danger';

            default:
                return undefined;
        }
    }
</script>

Selectable rows

A data table where multiple rows can be selected via checkboxes.

MembersNo rows selected
Person
Status
Ernesto MillerAda89@yahoo.com
Inactive
Drew McGlynnFranklin64@gmail.com
Inactive
Terrance HeaneyElinore.Kiehn81@yahoo.com
Active
Muhammad LarkinBrent61@hotmail.com
Active
Joshua StreichPhyllis.Bayer4@hotmail.com
Inactive

<template>
    <FluxPane>
        <FluxPaneHeader
            title="Members"
            :subtitle="selected.length === 0 ? 'No rows selected' : `${selected.length} row${selected.length === 1 ? '' : 's'} selected`"/>

        <FluxDataTable
            v-model:selected="selected"
            :items="dataSet"
            :limits="[]"
            :page="1"
            :per-page="5"
            selection-mode="multiple"
            :total="dataSet.length"
            unique-key="id"
            is-hoverable>
            <template #header>
                <FluxTableHeader>
                    Person
                </FluxTableHeader>

                <FluxTableHeader is-shrinking>
                    Status
                </FluxTableHeader>
            </template>

            <template #name="{item: {name, email}}">
                <FluxTableCell content-direction="column">
                    <strong>{{ name }}</strong>
                    <small>{{ email }}</small>
                </FluxTableCell>
            </template>

            <template #isActive="{item: {isActive}}">
                <FluxTableCell>
                    <FluxBadgeStack>
                        <FluxBadge
                            v-if="isActive"
                            color="success"
                            icon="circle-check"
                            label="Active"/>

                        <FluxBadge
                            v-else
                            color="danger"
                            icon="circle-xmark"
                            label="Inactive"/>
                    </FluxBadgeStack>
                </FluxTableCell>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

<script
    lang="ts"
    setup>
    import { FluxBadge, FluxBadgeStack, FluxDataTable, FluxPane, FluxPaneHeader, FluxTableCell, FluxTableHeader } from '@flux-ui/components';
    import { faker } from '@faker-js/faker';
    import { computed, ref } from 'vue';

    const selected = ref<number[]>([]);

    const dataSet = computed(() => Array(5)
        .fill(null)
        .map((_, index) => ({
            id: index,
            name: faker.person.fullName(),
            email: faker.internet.email(),
            isActive: faker.datatype.boolean()
        })));
</script>

Single selection

A data table where at most one row can be selected.

MembersNo row selected
Person
Role
Ada Lovelaceada@example.com
Lead
Alan Turingalan@example.com
Engineer
Grace Hoppergrace@example.com
Engineer
Katherine Johnsonkatherine@example.com
Manager
Margaret Hamiltonmargaret@example.com
Engineer

<template>
    <FluxPane>
        <FluxPaneHeader
            title="Members"
            :subtitle="selectedPerson ? `${selectedPerson.name} is selected` : 'No row selected'"/>

        <FluxDataTable
            v-model:selected="selected"
            :items="people"
            :limits="[]"
            :page="1"
            :per-page="5"
            selection-mode="single"
            :total="people.length"
            unique-key="id"
            is-hoverable>
            <template #header>
                <FluxTableHeader>
                    Person
                </FluxTableHeader>

                <FluxTableHeader is-shrinking>
                    Role
                </FluxTableHeader>
            </template>

            <template #name="{item}">
                <FluxTableCell content-direction="column">
                    <strong>{{ item.name }}</strong>
                    <small>{{ item.email }}</small>
                </FluxTableCell>
            </template>

            <template #role="{item}">
                <FluxTableCell>{{ item.role }}</FluxTableCell>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

<script
    setup
    lang="ts">
    import { FluxDataTable, FluxPane, FluxPaneHeader, FluxTableCell, FluxTableHeader } from '@flux-ui/components';
    import { computed, ref } from 'vue';

    const selected = ref<number | null>(null);

    const people = [
        {id: 1, name: 'Ada Lovelace', email: 'ada@example.com', role: 'Lead'},
        {id: 2, name: 'Alan Turing', email: 'alan@example.com', role: 'Engineer'},
        {id: 3, name: 'Grace Hopper', email: 'grace@example.com', role: 'Engineer'},
        {id: 4, name: 'Katherine Johnson', email: 'katherine@example.com', role: 'Manager'},
        {id: 5, name: 'Margaret Hamilton', email: 'margaret@example.com', role: 'Engineer'}
    ];

    const selectedPerson = computed(() => people.find(person => person.id === selected.value) ?? null);
</script>

Selection toolbar

A bar with bulk actions that takes the place of the filter bar while rows are selected.

2 selected
Person
Role
Ada Lovelaceada@example.com
Lead
Alan Turingalan@example.com
Engineer
Grace Hoppergrace@example.com
Engineer
Katherine Johnsonkatherine@example.com
Manager
Margaret Hamiltonmargaret@example.com
Engineer
Radia Perlmanradia@example.com
Engineer

<template>
    <FluxPane>
        <FluxDataTable
            v-model:selected="selected"
            :items="filteredPeople"
            :limits="[]"
            :page="1"
            :per-page="6"
            selection-mode="multiple"
            :total="filteredPeople.length"
            unique-key="id"
            is-hoverable>
            <template #filter>
                <FluxTableBar>
                    <FluxFilterBar
                        v-model="filterState"
                        v-model:search="search"
                        is-searchable
                        search-placeholder="Search people...">
                        <FluxFilterOption
                            label="Role"
                            name="role"
                            :options="roleOptions"/>
                    </FluxFilterBar>
                </FluxTableBar>
            </template>

            <template #selection="{count, clear}">
                <FluxFlexItem :grow="1">
                    <FluxFlex
                        align="center"
                        :gap="9">
                        <span aria-live="polite">{{ count }} selected</span>

                        <FluxSpacer/>

                        <FluxSecondaryButton
                            icon-leading="trash"
                            label="Delete"
                            @click="deleteSelected"/>

                        <FluxSecondaryButton
                            label="Clear"
                            @click="clear()"/>
                    </FluxFlex>
                </FluxFlexItem>
            </template>

            <template #header>
                <FluxTableHeader>Person</FluxTableHeader>
                <FluxTableHeader is-shrinking>Role</FluxTableHeader>
            </template>

            <template #name="{item}">
                <FluxTableCell content-direction="column">
                    <strong>{{ item.name }}</strong>
                    <small>{{ item.email }}</small>
                </FluxTableCell>
            </template>

            <template #role="{item}">
                <FluxTableCell>{{ item.role }}</FluxTableCell>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

<script
    setup
    lang="ts">
    import { FluxDataTable, FluxFilterBar, FluxFilterOption, FluxFlex, FluxFlexItem, FluxPane, FluxSecondaryButton, FluxSpacer, FluxTableBar, FluxTableCell, FluxTableHeader } from '@flux-ui/components';
    import type { FluxFilterOptionItem, FluxFilterState } from '@flux-ui/types';
    import { computed, ref } from 'vue';

    const roleOptions: FluxFilterOptionItem[] = [
        {label: 'Lead', value: 'Lead'},
        {label: 'Engineer', value: 'Engineer'},
        {label: 'Manager', value: 'Manager'}
    ];

    const search = ref('');
    const selected = ref<number[]>([2, 4]);

    const filterState = ref<FluxFilterState>({
        role: null
    });

    const people = ref([
        {id: 1, name: 'Ada Lovelace', email: 'ada@example.com', role: 'Lead'},
        {id: 2, name: 'Alan Turing', email: 'alan@example.com', role: 'Engineer'},
        {id: 3, name: 'Grace Hopper', email: 'grace@example.com', role: 'Engineer'},
        {id: 4, name: 'Katherine Johnson', email: 'katherine@example.com', role: 'Manager'},
        {id: 5, name: 'Margaret Hamilton', email: 'margaret@example.com', role: 'Engineer'},
        {id: 6, name: 'Radia Perlman', email: 'radia@example.com', role: 'Engineer'}
    ]);

    const filteredPeople = computed(() => people.value.filter(person => {
        if (search.value && !person.name.toLowerCase().includes(search.value.toLowerCase())) {
            return false;
        }

        if (filterState.value.role && person.role !== filterState.value.role) {
            return false;
        }

        return true;
    }));

    function deleteSelected(): void {
        people.value = people.value.filter(person => !selected.value.includes(person.id));
        selected.value = [];
    }
</script>

Sortable columns

A data table whose columns drive a single, coordinated client-side sort.

Name
Role
Commits
Margaret Hamilton
Engineer
2,041
Grace Hopper
Engineer
1,530
Ada Lovelace
Lead
1,287
Alan Turing
Engineer
942
Katherine Johnson
Manager
318

<template>
    <FluxPane>
        <FluxDataTable
            :items="sortedItems"
            :limits="[]"
            :page="1"
            :per-page="people.length"
            :total="people.length"
            is-hoverable>
            <template #header>
                <FluxTableHeader
                    is-sortable
                    :sort="sortColumn === 'name' ? sortDirection : undefined"
                    @sort="setSort('name', $event)">
                    Name
                </FluxTableHeader>

                <FluxTableHeader
                    is-sortable
                    :sort="sortColumn === 'role' ? sortDirection : undefined"
                    @sort="setSort('role', $event)">
                    Role
                </FluxTableHeader>

                <FluxTableHeader
                    align="end"
                    is-sortable
                    :sort="sortColumn === 'commits' ? sortDirection : undefined"
                    @sort="setSort('commits', $event)">
                    Commits
                </FluxTableHeader>
            </template>

            <template #name="{item}">
                <FluxTableCell>
                    <strong>{{ item.name }}</strong>
                </FluxTableCell>
            </template>

            <template #role="{item}">
                <FluxTableCell>{{ item.role }}</FluxTableCell>
            </template>

            <template #commits="{item}">
                <FluxTableCell
                    align="end"
                    is-numeric>
                    {{ item.commits.toLocaleString() }}
                </FluxTableCell>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

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

    type SortDirection = 'ascending' | 'descending';

    type Person = {
        readonly id: number;
        readonly name: string;
        readonly role: string;
        readonly commits: number;
    };

    const people: Person[] = [
        {id: 1, name: 'Ada Lovelace', role: 'Lead', commits: 1287},
        {id: 2, name: 'Alan Turing', role: 'Engineer', commits: 942},
        {id: 3, name: 'Grace Hopper', role: 'Engineer', commits: 1530},
        {id: 4, name: 'Katherine Johnson', role: 'Manager', commits: 318},
        {id: 5, name: 'Margaret Hamilton', role: 'Engineer', commits: 2041}
    ];

    const sortColumn = ref<keyof Person | null>('commits');
    const sortDirection = ref<SortDirection>('descending');

    const sortedItems = computed(() => {
        const column = sortColumn.value;

        if (!column) {
            return people;
        }

        const factor = sortDirection.value === 'ascending' ? 1 : -1;

        return [...people].sort((a, b) => {
            const left = a[column];
            const right = b[column];

            if (typeof left === 'number' && typeof right === 'number') {
                return (left - right) * factor;
            }

            return String(left).localeCompare(String(right)) * factor;
        });
    });

    function setSort(column: keyof Person, direction: SortDirection | null): void {
        if (direction === null) {
            sortColumn.value = null;
            return;
        }

        sortColumn.value = column;
        sortDirection.value = direction;
    }
</script>

A data table with a filter bar, client-side filtering, pagination and selection.

Product
Status
Price
Aurora LampLighting
Active
€49.00
Borealis DeskFurniture
Active
€329.00
Cinder MugKitchen
Archived
€14.50
Delta ChairFurniture
Active
€189.00
Ember KettleKitchen
Active
€79.00
1–5 of 12

<template>
    <FluxPane>
        <FluxDataTable
            v-model:selected="selected"
            :items="paginatedItems"
            :limits="[5, 10, 25]"
            :page="page"
            :per-page="perPage"
            :total="filteredItems.length"
            selection-mode="multiple"
            unique-key="id"
            is-hoverable
            @limit="onLimit"
            @navigate="page = $event">
            <template #filter>
                <FluxTableBar>
                    <FluxFilterBar
                        v-model="filterState"
                        v-model:search="search"
                        is-searchable
                        search-placeholder="Search products...">
                        <FluxFilterOption
                            icon="circle-check"
                            label="Status"
                            name="status"
                            :options="statusOptions"/>

                        <FluxFilterOption
                            icon="clone"
                            label="Category"
                            name="category"
                            :options="categoryOptions"/>
                    </FluxFilterBar>
                </FluxTableBar>
            </template>

            <template #header>
                <FluxTableHeader>Product</FluxTableHeader>
                <FluxTableHeader is-shrinking>Status</FluxTableHeader>
                <FluxTableHeader
                    align="end"
                    is-shrinking>
                    Price
                </FluxTableHeader>
            </template>

            <template #name="{item}">
                <FluxTableCell content-direction="column">
                    <strong>{{ item.name }}</strong>
                    <small>{{ categoryLabels[item.category] }}</small>
                </FluxTableCell>
            </template>

            <template #status="{item}">
                <FluxTableCell>
                    <FluxBadgeStack>
                        <FluxBadge
                            :color="item.status === 'active' ? 'success' : 'gray'"
                            :label="item.status === 'active' ? 'Active' : 'Archived'"/>
                    </FluxBadgeStack>
                </FluxTableCell>
            </template>

            <template #price="{item}">
                <FluxTableCell
                    align="end"
                    is-numeric
                    no-wrap>
                    {{ priceLabel(item.price) }}
                </FluxTableCell>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

<script
    setup
    lang="ts">
    import { FluxBadge, FluxBadgeStack, FluxDataTable, FluxFilterBar, FluxFilterOption, FluxPane, FluxTableBar, FluxTableCell, FluxTableHeader } from '@flux-ui/components';
    import type { FluxFilterOptionItem, FluxFilterState } from '@flux-ui/types';
    import { computed, ref, watch } from 'vue';

    type Category = 'lighting' | 'furniture' | 'kitchen';

    type Product = {
        readonly id: number;
        readonly name: string;
        readonly category: Category;
        readonly status: 'active' | 'archived';
        readonly price: number;
    };

    const categoryLabels: Record<Category, string> = {
        lighting: 'Lighting',
        furniture: 'Furniture',
        kitchen: 'Kitchen'
    };

    const statusOptions: FluxFilterOptionItem[] = [
        {icon: 'circle-check', label: 'Active', value: 'active'},
        {icon: 'circle-xmark', label: 'Archived', value: 'archived'}
    ];

    const categoryOptions: FluxFilterOptionItem[] = [
        {label: 'Lighting', value: 'lighting'},
        {label: 'Furniture', value: 'furniture'},
        {label: 'Kitchen', value: 'kitchen'}
    ];

    const products: Product[] = [
        {id: 1, name: 'Aurora Lamp', category: 'lighting', status: 'active', price: 4900},
        {id: 2, name: 'Borealis Desk', category: 'furniture', status: 'active', price: 32900},
        {id: 3, name: 'Cinder Mug', category: 'kitchen', status: 'archived', price: 1450},
        {id: 4, name: 'Delta Chair', category: 'furniture', status: 'active', price: 18900},
        {id: 5, name: 'Ember Kettle', category: 'kitchen', status: 'active', price: 7900},
        {id: 6, name: 'Flare Sconce', category: 'lighting', status: 'archived', price: 6900},
        {id: 7, name: 'Glow Pendant', category: 'lighting', status: 'active', price: 12900},
        {id: 8, name: 'Harbor Stool', category: 'furniture', status: 'active', price: 9900},
        {id: 9, name: 'Iris Teapot', category: 'kitchen', status: 'active', price: 5400},
        {id: 10, name: 'Juno Floor Lamp', category: 'lighting', status: 'active', price: 21900},
        {id: 11, name: 'Koto Bench', category: 'furniture', status: 'archived', price: 27900},
        {id: 12, name: 'Lumen Toaster', category: 'kitchen', status: 'active', price: 8400}
    ];

    const page = ref(1);
    const perPage = ref(5);
    const search = ref('');
    const selected = ref<number[]>([]);

    const filterState = ref<FluxFilterState>({
        status: null,
        category: null
    });

    const filteredItems = computed(() => products.filter(product => {
        if (search.value && !product.name.toLowerCase().includes(search.value.toLowerCase())) {
            return false;
        }

        if (filterState.value.status && product.status !== filterState.value.status) {
            return false;
        }

        if (filterState.value.category && product.category !== filterState.value.category) {
            return false;
        }

        return true;
    }));

    const paginatedItems = computed(() => {
        const start = (page.value - 1) * perPage.value;

        return filteredItems.value.slice(start, start + perPage.value);
    });

    watch([search, filterState], () => {
        page.value = 1;
    }, {deep: true});

    function onLimit(limit: number): void {
        perPage.value = limit;
        page.value = 1;
    }

    function priceLabel(price: number): string {
        return new Intl.NumberFormat('en', {
            currency: 'EUR',
            style: 'currency'
        }).format(price / 100);
    }
</script>

Rich cells and row actions

A data table with avatars, badges, progress bars and a per-row actions menu.

Member
Status
Onboarding
Ada Lovelaceada@example.com
Active
Complete100%
Alan Turingalan@example.com
Active
Training75%
Grace Hoppergrace@example.com
Invited
Profile setup30%
Margaret Hamiltonmargaret@example.com
Active
Verification55%

<template>
    <FluxPane>
        <FluxDataTable
            :items="members"
            :limits="[]"
            :page="1"
            :per-page="members.length"
            :total="members.length"
            is-hoverable>
            <template #header>
                <FluxTableHeader>Member</FluxTableHeader>
                <FluxTableHeader is-shrinking>Status</FluxTableHeader>
                <FluxTableHeader :min-width="180">Onboarding</FluxTableHeader>
                <FluxTableHeader is-shrinking/>
            </template>

            <template #member="{item}">
                <FluxTableCell>
                    <FluxFlex :gap="15">
                        <FluxAvatar
                            :alt="item.name"
                            :fallback-initials="item.initials"
                            :size="36"/>

                        <FluxFlex
                            direction="vertical"
                            :gap="0">
                            <strong>{{ item.name }}</strong>
                            <small>{{ item.email }}</small>
                        </FluxFlex>
                    </FluxFlex>
                </FluxTableCell>
            </template>

            <template #status="{item}">
                <FluxTableCell>
                    <FluxBadgeStack>
                        <FluxBadge
                            v-if="item.isActive"
                            color="success"
                            icon="circle-check"
                            label="Active"/>

                        <FluxBadge
                            v-else
                            color="gray"
                            icon="circle-xmark"
                            label="Invited"/>
                    </FluxBadgeStack>
                </FluxTableCell>
            </template>

            <template #onboarding="{item}">
                <FluxTableCell>
                    <FluxProgressBar
                        :status="item.step"
                        :value="item.onboarding"/>
                </FluxTableCell>
            </template>

            <template #actions="{item}">
                <FluxTableCell>
                    <FluxTableActions>
                        <FluxFlyout>
                            <template #opener="{open}">
                                <FluxAction
                                    icon="ellipsis-h"
                                    @click="open()"/>
                            </template>

                            <FluxMenu style="width: 180px">
                                <FluxMenuGroup>
                                    <FluxMenuItem
                                        icon-leading="eye"
                                        label="View profile"
                                        @click="onAction('view', item)"/>

                                    <FluxMenuItem
                                        icon-leading="pen"
                                        label="Edit"
                                        @click="onAction('edit', item)"/>
                                </FluxMenuGroup>

                                <FluxSeparator/>

                                <FluxMenuGroup>
                                    <FluxMenuItem
                                        icon-leading="trash"
                                        is-destructive
                                        label="Remove"
                                        @click="onAction('remove', item)"/>
                                </FluxMenuGroup>
                            </FluxMenu>
                        </FluxFlyout>
                    </FluxTableActions>
                </FluxTableCell>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

<script
    setup
    lang="ts">
    import { FluxAction, FluxAvatar, FluxBadge, FluxBadgeStack, FluxDataTable, FluxFlex, FluxFlyout, FluxMenu, FluxMenuGroup, FluxMenuItem, FluxPane, FluxProgressBar, FluxSeparator, FluxTableActions, FluxTableCell, FluxTableHeader } from '@flux-ui/components';

    type Member = {
        readonly id: number;
        readonly name: string;
        readonly initials: string;
        readonly email: string;
        readonly isActive: boolean;
        readonly step: string;
        readonly onboarding: number;
    };

    const members: Member[] = [
        {id: 1, name: 'Ada Lovelace', initials: 'AL', email: 'ada@example.com', isActive: true, step: 'Complete', onboarding: 1},
        {id: 2, name: 'Alan Turing', initials: 'AT', email: 'alan@example.com', isActive: true, step: 'Training', onboarding: 0.75},
        {id: 3, name: 'Grace Hopper', initials: 'GH', email: 'grace@example.com', isActive: false, step: 'Profile setup', onboarding: 0.3},
        {id: 4, name: 'Margaret Hamilton', initials: 'MH', email: 'margaret@example.com', isActive: true, step: 'Verification', onboarding: 0.55}
    ];

    function onAction(action: 'view' | 'edit' | 'remove', member: Member): void {
        console.log(action, member.name);
    }
</script>

Expandable rows

A data table where each row can be expanded to reveal detail content.

Order
Total
#2024-0001Acme Inc.
€ 1,240.00
Customer
Acme Inc.
Items
3
Shipping
Standard
#2024-0002Globex
€ 320.00
#2024-0003Initech
€ 78.50

<template>
    <FluxPane>
        <FluxDataTable
            v-model:expanded="expanded"
            :items="dataSet"
            :limits="[]"
            :page="1"
            :per-page="5"
            :total="dataSet.length"
            unique-key="id"
            is-hoverable>
            <template #header>
                <FluxTableHeader>
                    Order
                </FluxTableHeader>

                <FluxTableHeader is-shrinking>
                    Total
                </FluxTableHeader>
            </template>

            <template #reference="{item}">
                <FluxTableCell content-direction="column">
                    <strong>{{ item.reference }}</strong>
                    <small>{{ item.customer }}</small>
                </FluxTableCell>
            </template>

            <template #total="{item}">
                <FluxTableCell
                    is-numeric
                    no-wrap>
                    {{ item.total }}
                </FluxTableCell>
            </template>

            <template #expandable="{item}">
                <FluxDescriptionList>
                    <FluxDescriptionItem label="Customer">
                        {{ item.customer }}
                    </FluxDescriptionItem>

                    <FluxDescriptionItem label="Items">
                        {{ item.items }}
                    </FluxDescriptionItem>

                    <FluxDescriptionItem label="Shipping">
                        {{ item.shipping }}
                    </FluxDescriptionItem>
                </FluxDescriptionList>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

<script
    setup
    lang="ts">
    import { FluxDataTable, FluxDescriptionItem, FluxDescriptionList, FluxPane, FluxTableCell, FluxTableHeader } from '@flux-ui/components';
    import { ref } from 'vue';

    const expanded = ref<number[]>([1]);

    const dataSet = [
        {id: 1, reference: '#2024-0001', customer: 'Acme Inc.', total: '€ 1,240.00', items: 3, shipping: 'Standard'},
        {id: 2, reference: '#2024-0002', customer: 'Globex', total: '€ 320.00', items: 1, shipping: 'Express'},
        {id: 3, reference: '#2024-0003', customer: 'Initech', total: '€ 78.50', items: 2, shipping: 'Standard'}
    ];
</script>

Single expansion

A data table where opening a row collapses the previously opened one.

Order
Total
#2024-0001Acme Inc.
€ 1,240.00
Customer
Acme Inc.
Items
3
Shipping
Standard
#2024-0002Globex
€ 320.00
#2024-0003Initech
€ 78.50

<template>
    <FluxPane>
        <FluxDataTable
            v-model:expanded="expanded"
            expand-mode="single"
            :items="dataSet"
            :limits="[]"
            :page="1"
            :per-page="5"
            :total="dataSet.length"
            unique-key="id"
            is-hoverable>
            <template #header>
                <FluxTableHeader>
                    Order
                </FluxTableHeader>

                <FluxTableHeader is-shrinking>
                    Total
                </FluxTableHeader>
            </template>

            <template #reference="{item}">
                <FluxTableCell content-direction="column">
                    <strong>{{ item.reference }}</strong>
                    <small>{{ item.customer }}</small>
                </FluxTableCell>
            </template>

            <template #total="{item}">
                <FluxTableCell
                    is-numeric
                    no-wrap>
                    {{ item.total }}
                </FluxTableCell>
            </template>

            <template #expandable="{item}">
                <FluxDescriptionList>
                    <FluxDescriptionItem label="Customer">
                        {{ item.customer }}
                    </FluxDescriptionItem>

                    <FluxDescriptionItem label="Items">
                        {{ item.items }}
                    </FluxDescriptionItem>

                    <FluxDescriptionItem label="Shipping">
                        {{ item.shipping }}
                    </FluxDescriptionItem>
                </FluxDescriptionList>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

<script
    setup
    lang="ts">
    import { FluxDataTable, FluxDescriptionItem, FluxDescriptionList, FluxPane, FluxTableCell, FluxTableHeader } from '@flux-ui/components';
    import { ref } from 'vue';

    const expanded = ref<number[]>([1]);

    const dataSet = [
        {id: 1, reference: '#2024-0001', customer: 'Acme Inc.', total: '€ 1,240.00', items: 3, shipping: 'Standard'},
        {id: 2, reference: '#2024-0002', customer: 'Globex', total: '€ 320.00', items: 1, shipping: 'Express'},
        {id: 3, reference: '#2024-0003', customer: 'Initech', total: '€ 78.50', items: 2, shipping: 'Standard'}
    ];
</script>

Conditional expansion

A data table where only rows with detail content show an expand toggle.

Order
Total
#2024-0001Acme Inc.
€ 1,240.00
Customer
Acme Inc.
Items
3
Shipping
Standard
#2024-0002Globex
€ 320.00
#2024-0003Initech
€ 78.50

<template>
    <FluxPane>
        <FluxDataTable
            v-model:expanded="expanded"
            :can-expand="(item) => item.hasDetails"
            :items="dataSet"
            :limits="[]"
            :page="1"
            :per-page="5"
            :total="dataSet.length"
            unique-key="id"
            is-hoverable>
            <template #header>
                <FluxTableHeader>
                    Order
                </FluxTableHeader>

                <FluxTableHeader is-shrinking>
                    Total
                </FluxTableHeader>
            </template>

            <template #reference="{item}">
                <FluxTableCell content-direction="column">
                    <strong>{{ item.reference }}</strong>
                    <small>{{ item.customer }}</small>
                </FluxTableCell>
            </template>

            <template #total="{item}">
                <FluxTableCell
                    is-numeric
                    no-wrap>
                    {{ item.total }}
                </FluxTableCell>
            </template>

            <template #expandable="{item}">
                <FluxDescriptionList>
                    <FluxDescriptionItem label="Customer">
                        {{ item.customer }}
                    </FluxDescriptionItem>

                    <FluxDescriptionItem label="Items">
                        {{ item.items }}
                    </FluxDescriptionItem>

                    <FluxDescriptionItem label="Shipping">
                        {{ item.shipping }}
                    </FluxDescriptionItem>
                </FluxDescriptionList>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

<script
    setup
    lang="ts">
    import { FluxDataTable, FluxDescriptionItem, FluxDescriptionList, FluxPane, FluxTableCell, FluxTableHeader } from '@flux-ui/components';
    import { ref } from 'vue';

    const expanded = ref<number[]>([1]);

    const dataSet = [
        {id: 1, reference: '#2024-0001', customer: 'Acme Inc.', total: '€ 1,240.00', items: 3, shipping: 'Standard', hasDetails: true},
        {id: 2, reference: '#2024-0002', customer: 'Globex', total: '€ 320.00', items: 1, shipping: 'Express', hasDetails: true},
        {id: 3, reference: '#2024-0003', customer: 'Initech', total: '€ 78.50', items: 2, shipping: 'Standard', hasDetails: false}
    ];
</script>

Grouped rows

A data table whose rows are grouped under collapsible headers.

Name
Role

<template>
    <FluxPane>
        <FluxDataTable
            v-model:collapsed-groups="collapsedGroups"
            :group-by="item => item.team"
            :items="dataSet"
            :limits="[]"
            :page="1"
            :per-page="dataSet.length"
            :total="dataSet.length"
            unique-key="id"
            is-hoverable>
            <template #header>
                <FluxTableHeader>Name</FluxTableHeader>
                <FluxTableHeader is-shrinking>Role</FluxTableHeader>
            </template>

            <template #group="{id, items, isExpanded, toggle}">
                <FluxTableGroup
                    :icon="teamIcon[id]"
                    :is-expanded="isExpanded"
                    :label="String(id)"
                    is-expandable
                    @update:is-expanded="toggle">
                    <template #after>
                        <FluxBadge :label="`${items.length}`"/>
                    </template>
                </FluxTableGroup>
            </template>

            <template #name="{item}">
                <FluxTableCell content-direction="column">
                    <strong>{{ item.name }}</strong>
                    <small>{{ item.email }}</small>
                </FluxTableCell>
            </template>

            <template #role="{item}">
                <FluxTableCell>
                    {{ item.role }}
                </FluxTableCell>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

<script
    setup
    lang="ts">
    import { FluxBadge, FluxDataTable, FluxPane, FluxTableCell, FluxTableGroup, FluxTableHeader } from '@flux-ui/components';
    import type { FluxIconName } from '@flux-ui/types';
    import { ref } from 'vue';

    const teamIcon: Record<string, FluxIconName> = {
        Engineering: 'users',
        Design: 'palette',
        Operations: 'folder'
    };

    const collapsedGroups = ref<string[]>([]);

    const dataSet = [
        {id: 1, team: 'Engineering', name: 'Ada Lovelace', email: 'ada@example.com', role: 'Lead'},
        {id: 2, team: 'Engineering', name: 'Alan Turing', email: 'alan@example.com', role: 'Engineer'},
        {id: 3, team: 'Design', name: 'Grace Hopper', email: 'grace@example.com', role: 'Designer'},
        {id: 4, team: 'Operations', name: 'Katherine Johnson', email: 'katherine@example.com', role: 'Manager'},
        {id: 5, team: 'Operations', name: 'Margaret Hamilton', email: 'margaret@example.com', role: 'Engineer'}
    ];
</script>

Static groups

A data table whose rows are grouped under plain, non-collapsible headers.

Name
Role

<template>
    <FluxPane>
        <FluxDataTable
            :group-by="item => item.team"
            :items="dataSet"
            :limits="[]"
            :page="1"
            :per-page="dataSet.length"
            :total="dataSet.length"
            is-hoverable>
            <template #header>
                <FluxTableHeader>Name</FluxTableHeader>
                <FluxTableHeader is-shrinking>Role</FluxTableHeader>
            </template>

            <template #group="{id, items}">
                <FluxTableGroup :label="String(id)">
                    <template #after>
                        <FluxBadge :label="`${items.length}`"/>
                    </template>
                </FluxTableGroup>
            </template>

            <template #name="{item}">
                <FluxTableCell content-direction="column">
                    <strong>{{ item.name }}</strong>
                    <small>{{ item.email }}</small>
                </FluxTableCell>
            </template>

            <template #role="{item}">
                <FluxTableCell>
                    {{ item.role }}
                </FluxTableCell>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

<script
    setup
    lang="ts">
    import { FluxBadge, FluxDataTable, FluxPane, FluxTableCell, FluxTableGroup, FluxTableHeader } from '@flux-ui/components';

    const dataSet = [
        {id: 1, team: 'Engineering', name: 'Ada Lovelace', email: 'ada@example.com', role: 'Lead'},
        {id: 2, team: 'Engineering', name: 'Alan Turing', email: 'alan@example.com', role: 'Engineer'},
        {id: 3, team: 'Design', name: 'Grace Hopper', email: 'grace@example.com', role: 'Designer'},
        {id: 4, team: 'Operations', name: 'Katherine Johnson', email: 'katherine@example.com', role: 'Manager'},
        {id: 5, team: 'Operations', name: 'Margaret Hamilton', email: 'margaret@example.com', role: 'Engineer'}
    ];
</script>

Wide table

A wide data table with multiple pinned columns on the left and right edges.

Product
SKU
Category
Supplier
Stock
Price
Aurora Lamp
AUR-001
Lighting
Lumen Co.
128
€ 49.00
Borealis Desk
BOR-014
Furniture
Nordic Works
12
€ 329.00
Cinder Mug
CIN-220
Kitchen
Clayworks
1043
€ 14.50
Delta Chair
DEL-067
Furniture
Nordic Works
47
€ 189.00
Ember Kettle
EMB-309
Kitchen
Clayworks
6
€ 79.00

<template>
    <FluxPane>
        <FluxDataTable
            :items="items"
            :limits="[]"
            :page="1"
            :per-page="items.length"
            :total="items.length"
            is-hoverable>
            <template #header>
                <FluxTableHeader
                    pinned
                    :min-width="180">
                    Product
                </FluxTableHeader>

                <FluxTableHeader
                    pinned
                    :min-width="150">
                    SKU
                </FluxTableHeader>

                <FluxTableHeader :min-width="150">Category</FluxTableHeader>
                <FluxTableHeader :min-width="180">Supplier</FluxTableHeader>

                <FluxTableHeader
                    align="end"
                    :min-width="120">
                    Stock
                </FluxTableHeader>

                <FluxTableHeader
                    align="end"
                    pinned="end"
                    :min-width="120">
                    Price
                </FluxTableHeader>
            </template>

            <template #name="{item}">
                <FluxTableCell>
                    <strong>{{ item.name }}</strong>
                </FluxTableCell>
            </template>

            <template #sku="{item}">
                <FluxTableCell>{{ item.sku }}</FluxTableCell>
            </template>

            <template #category="{item}">
                <FluxTableCell>{{ item.category }}</FluxTableCell>
            </template>

            <template #supplier="{item}">
                <FluxTableCell>{{ item.supplier }}</FluxTableCell>
            </template>

            <template #stock="{item}">
                <FluxTableCell
                    align="end"
                    is-numeric>
                    {{ item.stock }}
                </FluxTableCell>
            </template>

            <template #price="{item}">
                <FluxTableCell
                    align="end"
                    is-numeric>
                    {{ item.price }}
                </FluxTableCell>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

<script
    setup
    lang="ts">
    import { FluxDataTable, FluxPane, FluxTableCell, FluxTableHeader } from '@flux-ui/components';

    const items = [
        {id: 1, name: 'Aurora Lamp', sku: 'AUR-001', category: 'Lighting', supplier: 'Lumen Co.', stock: 128, price: '€ 49.00'},
        {id: 2, name: 'Borealis Desk', sku: 'BOR-014', category: 'Furniture', supplier: 'Nordic Works', stock: 12, price: '€ 329.00'},
        {id: 3, name: 'Cinder Mug', sku: 'CIN-220', category: 'Kitchen', supplier: 'Clayworks', stock: 1043, price: '€ 14.50'},
        {id: 4, name: 'Delta Chair', sku: 'DEL-067', category: 'Furniture', supplier: 'Nordic Works', stock: 47, price: '€ 189.00'},
        {id: 5, name: 'Ember Kettle', sku: 'EMB-309', category: 'Kitchen', supplier: 'Clayworks', stock: 6, price: '€ 79.00'}
    ];
</script>

Pinned with selection

A data table where pinning the first column keeps the selection column pinned alongside it.

Person
Role
Department
Location
Start date
Salary
Mr. Hugo PfefferCeasar12@yahoo.com
Principal Communications Facilitator
Toys
Beatriceton, PN
Nov 16, 2025
€ 121000
Salvatore HandTom97@gmail.com
Product Configuration Supervisor
Movies
South Terrell, CF
Sep 13, 2025
€ 42000
Marshall BalistreriKimberly_Schmidt@hotmail.com
Principal Response Liaison
Kids
West Kayleigh, OM
Jan 17, 2026
€ 122000
Lorenzo PfefferStewart_Torp@gmail.com
Global Group Engineer
Health
Toledo, KN
Apr 24, 2026
€ 77000
Hulda HarveyKen_Leannon@yahoo.com
Forward Identity Strategist
Kids
Schultzfort, TW
Apr 28, 2026
€ 110000
Hattie SatterfieldArne.Lynch@yahoo.com
District Operations Agent
Books
Rowlett, KY
Mar 12, 2026
€ 60000

<template>
    <FluxPane>
        <FluxDataTable
            v-model:selected="selected"
            :items="dataSet"
            :limits="[]"
            :page="1"
            :per-page="dataSet.length"
            selection-mode="multiple"
            :total="dataSet.length"
            unique-key="id"
            is-hoverable>
            <template #header>
                <FluxTableHeader
                    pinned
                    :min-width="200">
                    Person
                </FluxTableHeader>

                <FluxTableHeader :min-width="160">Role</FluxTableHeader>
                <FluxTableHeader :min-width="170">Department</FluxTableHeader>
                <FluxTableHeader :min-width="170">Location</FluxTableHeader>
                <FluxTableHeader :min-width="150">Start date</FluxTableHeader>

                <FluxTableHeader
                    align="end"
                    pinned="end"
                    :min-width="150">
                    Salary
                </FluxTableHeader>
            </template>

            <template #name="{item: {name, email}}">
                <FluxTableCell content-direction="column">
                    <strong>{{ name }}</strong>
                    <small>{{ email }}</small>
                </FluxTableCell>
            </template>

            <template #role="{item: {role}}">
                <FluxTableCell>{{ role }}</FluxTableCell>
            </template>

            <template #department="{item: {department}}">
                <FluxTableCell>{{ department }}</FluxTableCell>
            </template>

            <template #location="{item: {location}}">
                <FluxTableCell>{{ location }}</FluxTableCell>
            </template>

            <template #startDate="{item: {startDate}}">
                <FluxTableCell>{{ startDate }}</FluxTableCell>
            </template>

            <template #salary="{item: {salary}}">
                <FluxTableCell
                    align="end"
                    is-numeric>
                    {{ salary }}
                </FluxTableCell>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

<script
    lang="ts"
    setup>
    import { FluxDataTable, FluxPane, FluxTableCell, FluxTableHeader } from '@flux-ui/components';
    import { faker } from '@faker-js/faker';
    import { computed, ref } from 'vue';

    const selected = ref<number[]>([]);

    const dataSet = computed(() => Array(6)
        .fill(null)
        .map((_, index) => ({
            id: index,
            name: faker.person.fullName(),
            email: faker.internet.email(),
            role: faker.person.jobTitle(),
            department: faker.commerce.department(),
            location: `${faker.location.city()}, ${faker.location.countryCode()}`,
            startDate: faker.date.past().toLocaleDateString('en-US', {year: 'numeric', month: 'short', day: 'numeric'}),
            salary: `€ ${faker.number.int({min: 42, max: 128}) * 1000}`
        })));
</script>

Column sizing

Headers mixing a fixed width, a min/max range and a shrinking column.

SKU
Product
Category
Price
Status
AC-1024
Aurora Ceiling Light
Lighting
€49.00
In stock
FD-2087
Borealis Standing Desk
Furniture
€329.00
In stock
KD-3391
Cinder Ceramic Mug
Kitchen & dining
€14.50
Sold out
FD-4410
Delta Lounge Chair
Furniture
€189.00
In stock
KD-5502
Ember Electric Kettle
Kitchen & dining
€79.00
In stock

<template>
    <FluxPane>
        <FluxDataTable
            :items="products"
            :limits="[]"
            :page="1"
            :per-page="products.length"
            :total="products.length"
            is-hoverable>
            <template #header>
                <FluxTableHeader :width="120">SKU</FluxTableHeader>
                <FluxTableHeader>Product</FluxTableHeader>

                <FluxTableHeader
                    :min-width="120"
                    :max-width="220">
                    Category
                </FluxTableHeader>

                <FluxTableHeader
                    align="end"
                    is-shrinking>
                    Price
                </FluxTableHeader>

                <FluxTableHeader is-shrinking>Status</FluxTableHeader>
            </template>

            <template #sku="{item}">
                <FluxTableCell>{{ item.sku }}</FluxTableCell>
            </template>

            <template #name="{item}">
                <FluxTableCell>
                    <strong>{{ item.name }}</strong>
                </FluxTableCell>
            </template>

            <template #category="{item}">
                <FluxTableCell>{{ item.category }}</FluxTableCell>
            </template>

            <template #price="{item}">
                <FluxTableCell
                    align="end"
                    is-numeric>
                    {{ formatCurrency(item.price) }}
                </FluxTableCell>
            </template>

            <template #status="{item}">
                <FluxTableCell>
                    <FluxBadge
                        :color="item.stock > 0 ? 'success' : 'gray'"
                        :label="item.stock > 0 ? 'In stock' : 'Sold out'"/>
                </FluxTableCell>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

<script
    setup
    lang="ts">
    import { FluxBadge, FluxDataTable, FluxPane, FluxTableCell, FluxTableHeader } from '@flux-ui/components';

    type Product = {
        readonly id: number;
        readonly sku: string;
        readonly name: string;
        readonly category: string;
        readonly price: number;
        readonly stock: number;
    };

    const products: Product[] = [
        {id: 1, sku: 'AC-1024', name: 'Aurora Ceiling Light', category: 'Lighting', price: 4900, stock: 34},
        {id: 2, sku: 'FD-2087', name: 'Borealis Standing Desk', category: 'Furniture', price: 32900, stock: 8},
        {id: 3, sku: 'KD-3391', name: 'Cinder Ceramic Mug', category: 'Kitchen & dining', price: 1450, stock: 0},
        {id: 4, sku: 'FD-4410', name: 'Delta Lounge Chair', category: 'Furniture', price: 18900, stock: 15},
        {id: 5, sku: 'KD-5502', name: 'Ember Electric Kettle', category: 'Kitchen & dining', price: 7900, stock: 21}
    ];

    function formatCurrency(value: number): string {
        return new Intl.NumberFormat('en', {
            currency: 'EUR',
            style: 'currency'
        }).format(value / 100);
    }
</script>

Numeric columns

Right-aligned numeric columns with sortable numeric and date headers.

Date
Description
Category
Amount
Feb 22
Refund — Harbor Studio
Refund
-€189.00
Feb 14
Design software licences
Expense
-€540.00
Feb 11
Invoice #10243 — Clayworks
Income
+€6,120.00
Feb 5
Cloud hosting
Expense
-€320.00
Feb 2
Invoice #10241 — Lumen Co.
Income
+€4,800.00

<template>
    <FluxPane>
        <FluxDataTable
            :items="sortedItems"
            :limits="[]"
            :page="1"
            :per-page="transactions.length"
            :total="transactions.length"
            is-hoverable>
            <template #header>
                <FluxTableHeader
                    data-type="date"
                    is-numeric
                    is-shrinking
                    is-sortable
                    no-wrap
                    :sort="sortColumn === 'date' ? sortDirection : undefined"
                    @sort="setSort('date', $event)">
                    Date
                </FluxTableHeader>

                <FluxTableHeader :min-width="200">Description</FluxTableHeader>
                <FluxTableHeader is-shrinking>Category</FluxTableHeader>

                <FluxTableHeader
                    align="end"
                    data-type="numeric"
                    is-numeric
                    is-sortable
                    no-wrap
                    :sort="sortColumn === 'amount' ? sortDirection : undefined"
                    @sort="setSort('amount', $event)">
                    Amount
                </FluxTableHeader>
            </template>

            <template #date="{item}">
                <FluxTableCell>{{ item.date }}</FluxTableCell>
            </template>

            <template #description="{item}">
                <FluxTableCell>{{ item.description }}</FluxTableCell>
            </template>

            <template #category="{item}">
                <FluxTableCell>
                    <FluxBadge
                        color="gray"
                        :label="item.category"/>
                </FluxTableCell>
            </template>

            <template #amount="{item}">
                <FluxTableCell>{{ formatCurrency(item.amount) }}</FluxTableCell>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

<script
    setup
    lang="ts">
    import { FluxBadge, FluxDataTable, FluxPane, FluxTableCell, FluxTableHeader } from '@flux-ui/components';
    import { computed, ref } from 'vue';

    type SortDirection = 'ascending' | 'descending';
    type SortColumn = 'date' | 'amount';

    type Transaction = {
        readonly id: number;
        readonly date: string;
        readonly order: number;
        readonly description: string;
        readonly category: string;
        readonly amount: number;
    };

    const transactions: Transaction[] = [
        {id: 1, date: 'Feb 2', order: 20260202, description: 'Invoice #10241 — Lumen Co.', category: 'Income', amount: 4800},
        {id: 2, date: 'Feb 5', order: 20260205, description: 'Cloud hosting', category: 'Expense', amount: -320},
        {id: 3, date: 'Feb 11', order: 20260211, description: 'Invoice #10243 — Clayworks', category: 'Income', amount: 6120},
        {id: 4, date: 'Feb 14', order: 20260214, description: 'Design software licences', category: 'Expense', amount: -540},
        {id: 5, date: 'Feb 22', order: 20260222, description: 'Refund — Harbor Studio', category: 'Refund', amount: -189}
    ];

    const sortColumn = ref<SortColumn | null>('date');
    const sortDirection = ref<SortDirection>('descending');

    const sortedItems = computed(() => {
        const column = sortColumn.value;

        if (!column) {
            return transactions;
        }

        const factor = sortDirection.value === 'ascending' ? 1 : -1;
        const key = column === 'date' ? 'order' : 'amount';

        return [...transactions].sort((a, b) => (a[key] - b[key]) * factor);
    });

    function setSort(column: SortColumn, direction: SortDirection | null): void {
        if (direction === null) {
            sortColumn.value = null;
            return;
        }

        sortColumn.value = column;
        sortDirection.value = direction;
    }

    function formatCurrency(value: number): string {
        return new Intl.NumberFormat('en', {
            currency: 'EUR',
            signDisplay: 'always',
            style: 'currency'
        }).format(value);
    }
</script>

A footer row that sums the page across spanning cells.

Order
Customer
Items
Total
#10241
Lumen Co.
4
€48,000.00
#10242
Nordic Works
2
€32,500.00
#10243
Clayworks
6
€61,200.00
#10244
Harbor Studio
1
€18,900.00
Page total
€160,600.00

<template>
    <FluxPane>
        <FluxDataTable
            :items="orders"
            :limits="[]"
            :page="1"
            :per-page="orders.length"
            :total="orders.length"
            is-hoverable>
            <template #header>
                <FluxTableHeader>Order</FluxTableHeader>
                <FluxTableHeader :min-width="180">Customer</FluxTableHeader>

                <FluxTableHeader
                    align="end"
                    is-shrinking>
                    Items
                </FluxTableHeader>

                <FluxTableHeader
                    align="end"
                    :min-width="120">
                    Total
                </FluxTableHeader>
            </template>

            <template #reference="{item}">
                <FluxTableCell>
                    <strong>{{ item.reference }}</strong>
                </FluxTableCell>
            </template>

            <template #customer="{item}">
                <FluxTableCell>{{ item.customer }}</FluxTableCell>
            </template>

            <template #items="{item}">
                <FluxTableCell
                    align="end"
                    is-numeric>
                    {{ item.items }}
                </FluxTableCell>
            </template>

            <template #total="{item}">
                <FluxTableCell
                    align="end"
                    is-numeric>
                    {{ formatCurrency(item.total) }}
                </FluxTableCell>
            </template>

            <template #footer>
                <FluxTableCell
                    align="end"
                    :colspan="3">
                    <strong>Page total</strong>
                </FluxTableCell>

                <FluxTableCell
                    align="end"
                    is-numeric>
                    <strong>{{ formatCurrency(pageTotal) }}</strong>
                </FluxTableCell>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

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

    type Order = {
        readonly id: number;
        readonly reference: string;
        readonly customer: string;
        readonly items: number;
        readonly total: number;
    };

    const orders: Order[] = [
        {id: 1, reference: '#10241', customer: 'Lumen Co.', items: 4, total: 48000},
        {id: 2, reference: '#10242', customer: 'Nordic Works', items: 2, total: 32500},
        {id: 3, reference: '#10243', customer: 'Clayworks', items: 6, total: 61200},
        {id: 4, reference: '#10244', customer: 'Harbor Studio', items: 1, total: 18900}
    ];

    const pageTotal = computed(() => orders.reduce((sum, order) => sum + order.total, 0));

    function formatCurrency(value: number): string {
        return new Intl.NumberFormat('en', {
            currency: 'EUR',
            style: 'currency'
        }).format(value);
    }
</script>

Fixed-width columns

A weekly timesheet with fixed-width day columns and a total.

Member
Mon
Tue
Wed
Thu
Fri
Total
Ada Lovelace
8
8
7
8
6
37
Alan Turing
7
8
8
8
8
39
Grace Hopper
8
6
8
4
8
34
Katherine Johnson
4
8
8
8
5
33

<template>
    <FluxPane>
        <FluxDataTable
            :items="rows"
            :limits="[]"
            :page="1"
            :per-page="rows.length"
            :total="rows.length"
            is-hoverable>
            <template #header>
                <FluxTableHeader :min-width="160">Member</FluxTableHeader>

                <FluxTableHeader
                    align="end"
                    :width="66">
                    Mon
                </FluxTableHeader>

                <FluxTableHeader
                    align="end"
                    :width="66">
                    Tue
                </FluxTableHeader>

                <FluxTableHeader
                    align="end"
                    :width="66">
                    Wed
                </FluxTableHeader>

                <FluxTableHeader
                    align="end"
                    :width="66">
                    Thu
                </FluxTableHeader>

                <FluxTableHeader
                    align="end"
                    :width="66">
                    Fri
                </FluxTableHeader>

                <FluxTableHeader
                    align="end"
                    :width="78">
                    Total
                </FluxTableHeader>
            </template>

            <template #member="{item}">
                <FluxTableCell no-wrap>{{ item.member }}</FluxTableCell>
            </template>

            <template #mon="{item}">
                <FluxTableCell
                    align="end"
                    is-numeric>
                    {{ item.hours.mon }}
                </FluxTableCell>
            </template>

            <template #tue="{item}">
                <FluxTableCell
                    align="end"
                    is-numeric>
                    {{ item.hours.tue }}
                </FluxTableCell>
            </template>

            <template #wed="{item}">
                <FluxTableCell
                    align="end"
                    is-numeric>
                    {{ item.hours.wed }}
                </FluxTableCell>
            </template>

            <template #thu="{item}">
                <FluxTableCell
                    align="end"
                    is-numeric>
                    {{ item.hours.thu }}
                </FluxTableCell>
            </template>

            <template #fri="{item}">
                <FluxTableCell
                    align="end"
                    is-numeric>
                    {{ item.hours.fri }}
                </FluxTableCell>
            </template>

            <template #total="{item}">
                <FluxTableCell
                    align="end"
                    is-numeric>
                    <strong>{{ weekTotal(item) }}</strong>
                </FluxTableCell>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

<script
    setup
    lang="ts">
    import { FluxDataTable, FluxPane, FluxTableCell, FluxTableHeader } from '@flux-ui/components';

    type Hours = {
        readonly mon: number;
        readonly tue: number;
        readonly wed: number;
        readonly thu: number;
        readonly fri: number;
    };

    type Row = {
        readonly id: number;
        readonly member: string;
        readonly hours: Hours;
    };

    const rows: Row[] = [
        {id: 1, member: 'Ada Lovelace', hours: {mon: 8, tue: 8, wed: 7, thu: 8, fri: 6}},
        {id: 2, member: 'Alan Turing', hours: {mon: 7, tue: 8, wed: 8, thu: 8, fri: 8}},
        {id: 3, member: 'Grace Hopper', hours: {mon: 8, tue: 6, wed: 8, thu: 4, fri: 8}},
        {id: 4, member: 'Katherine Johnson', hours: {mon: 4, tue: 8, wed: 8, thu: 8, fri: 5}}
    ];

    function weekTotal(row: Row): number {
        return row.hours.mon + row.hours.tue + row.hours.wed + row.hours.thu + row.hours.fri;
    }
</script>

Stacked cells

Cells that stack a primary and secondary line of content.

Customer
Company
Plan
Since
Mila Vegamila@lumen.co
Lumen Co.
Enterprise
2021
Sven Halloransven@nordicworks.io
Nordic Works
Growth
2022
Priya Nandalpriya@clayworks.com
Clayworks
Enterprise
2020
Theo Marshtheo@harbor.studio
Harbor Studio
Starter
2024

<template>
    <FluxPane>
        <FluxDataTable
            :items="customers"
            :limits="[]"
            :page="1"
            :per-page="customers.length"
            :total="customers.length"
            is-hoverable>
            <template #header>
                <FluxTableHeader :min-width="220">Customer</FluxTableHeader>
                <FluxTableHeader :min-width="160">Company</FluxTableHeader>
                <FluxTableHeader is-shrinking>Plan</FluxTableHeader>
                <FluxTableHeader is-shrinking>Since</FluxTableHeader>
            </template>

            <template #name="{item}">
                <FluxTableCell
                    content-direction="column"
                    :content-gap="3">
                    <strong>{{ item.name }}</strong>
                    <small>{{ item.email }}</small>
                </FluxTableCell>
            </template>

            <template #company="{item}">
                <FluxTableCell>{{ item.company }}</FluxTableCell>
            </template>

            <template #plan="{item}">
                <FluxTableCell>
                    <FluxBadge
                        :color="item.plan === 'Enterprise' ? 'primary' : 'gray'"
                        :label="item.plan"/>
                </FluxTableCell>
            </template>

            <template #since="{item}">
                <FluxTableCell no-wrap>{{ item.since }}</FluxTableCell>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

<script
    setup
    lang="ts">
    import { FluxBadge, FluxDataTable, FluxPane, FluxTableCell, FluxTableHeader } from '@flux-ui/components';

    type Customer = {
        readonly id: number;
        readonly name: string;
        readonly email: string;
        readonly company: string;
        readonly plan: 'Starter' | 'Growth' | 'Enterprise';
        readonly since: string;
    };

    const customers: Customer[] = [
        {id: 1, name: 'Mila Vega', email: 'mila@lumen.co', company: 'Lumen Co.', plan: 'Enterprise', since: '2021'},
        {id: 2, name: 'Sven Halloran', email: 'sven@nordicworks.io', company: 'Nordic Works', plan: 'Growth', since: '2022'},
        {id: 3, name: 'Priya Nandal', email: 'priya@clayworks.com', company: 'Clayworks', plan: 'Enterprise', since: '2020'},
        {id: 4, name: 'Theo Marsh', email: 'theo@harbor.studio', company: 'Harbor Studio', plan: 'Starter', since: '2024'}
    ];
</script>

Wrapping content

A wrapping subject column whose meta columns stay on a single line.

Ticket
Subject
Status
Updated
SUP-482
Column widths jump when the data table loadsThe layout briefly shows equal columns before the grid template resolves, which looks like a flash on slower connections.
Open
Mar 2
SUP-479
Sticky header overlaps the filter bar on SafariWhen scrolling quickly the header and the table bar overlap by a few pixels only in Safari 17.
Pending
Mar 1
SUP-471
Pinned column shadow missing in dark modeThe edge shadow that indicates a pinned column is not visible against the dark surface token.
Resolved
Feb 27

<template>
    <FluxPane>
        <FluxDataTable
            :items="tickets"
            :limits="[]"
            :page="1"
            :per-page="tickets.length"
            :total="tickets.length"
            is-hoverable>
            <template #header>
                <FluxTableHeader :width="90">Ticket</FluxTableHeader>
                <FluxTableHeader :min-width="260">Subject</FluxTableHeader>
                <FluxTableHeader is-shrinking>Status</FluxTableHeader>
                <FluxTableHeader is-shrinking>Updated</FluxTableHeader>
            </template>

            <template #reference="{item}">
                <FluxTableCell no-wrap>
                    <strong>{{ item.reference }}</strong>
                </FluxTableCell>
            </template>

            <template #subject="{item}">
                <FluxTableCell
                    content-direction="column"
                    :content-gap="3">
                    <strong>{{ item.subject }}</strong>
                    <small>{{ item.excerpt }}</small>
                </FluxTableCell>
            </template>

            <template #status="{item}">
                <FluxTableCell>
                    <FluxBadge
                        :color="statusColor[item.status]"
                        :label="item.status"/>
                </FluxTableCell>
            </template>

            <template #updated="{item}">
                <FluxTableCell no-wrap>{{ item.updated }}</FluxTableCell>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

<script
    setup
    lang="ts">
    import { FluxBadge, FluxDataTable, FluxPane, FluxTableCell, FluxTableHeader } from '@flux-ui/components';
    import type { FluxColor } from '@flux-ui/types';

    type Status = 'Open' | 'Pending' | 'Resolved';

    type Ticket = {
        readonly id: number;
        readonly reference: string;
        readonly subject: string;
        readonly excerpt: string;
        readonly status: Status;
        readonly updated: string;
    };

    const statusColor: Record<Status, FluxColor> = {
        Open: 'primary',
        Pending: 'warning',
        Resolved: 'success'
    };

    const tickets: Ticket[] = [
        {id: 1, reference: 'SUP-482', subject: 'Column widths jump when the data table loads', excerpt: 'The layout briefly shows equal columns before the grid template resolves, which looks like a flash on slower connections.', status: 'Open', updated: 'Mar 2'},
        {id: 2, reference: 'SUP-479', subject: 'Sticky header overlaps the filter bar on Safari', excerpt: 'When scrolling quickly the header and the table bar overlap by a few pixels only in Safari 17.', status: 'Pending', updated: 'Mar 1'},
        {id: 3, reference: 'SUP-471', subject: 'Pinned column shadow missing in dark mode', excerpt: 'The edge shadow that indicates a pinned column is not visible against the dark surface token.', status: 'Resolved', updated: 'Feb 27'}
    ];
</script>

Consistent height

A partial page that keeps a fixed height with filler rows.

SKU
Item
Location
Quantity
A-01
Aurora Ceiling Light
Aisle 1 · Bay A
34
A-02
Borealis Standing Desk
Aisle 1 · Bay C
8
A-03
Cinder Ceramic Mug
Aisle 2 · Bay A
120
A-04
Delta Lounge Chair
Aisle 2 · Bay D
15
A-05
Ember Electric Kettle
Aisle 3 · Bay B
21
A-06
Flare Wall Sconce
Aisle 3 · Bay E
47
1–6 of 8

<template>
    <FluxPane>
        <FluxDataTable
            :items="visibleItems"
            :limits="[6, 12]"
            :page="page"
            :per-page="perPage"
            :total="inventory.length"
            is-filled
            is-hoverable
            style="min-height: 420px"
            @limit="onLimit"
            @navigate="page = $event">
            <template #header>
                <FluxTableHeader :width="90">SKU</FluxTableHeader>
                <FluxTableHeader>Item</FluxTableHeader>
                <FluxTableHeader is-shrinking>Location</FluxTableHeader>

                <FluxTableHeader
                    align="end"
                    is-shrinking>
                    Quantity
                </FluxTableHeader>
            </template>

            <template #sku="{item}">
                <FluxTableCell no-wrap>{{ item.sku }}</FluxTableCell>
            </template>

            <template #name="{item}">
                <FluxTableCell>{{ item.name }}</FluxTableCell>
            </template>

            <template #location="{item}">
                <FluxTableCell no-wrap>{{ item.location }}</FluxTableCell>
            </template>

            <template #quantity="{item}">
                <FluxTableCell
                    align="end"
                    is-numeric>
                    {{ item.quantity }}
                </FluxTableCell>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

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

    type InventoryItem = {
        readonly id: number;
        readonly sku: string;
        readonly name: string;
        readonly location: string;
        readonly quantity: number;
    };

    const inventory: InventoryItem[] = [
        {id: 1, sku: 'A-01', name: 'Aurora Ceiling Light', location: 'Aisle 1 · Bay A', quantity: 34},
        {id: 2, sku: 'A-02', name: 'Borealis Standing Desk', location: 'Aisle 1 · Bay C', quantity: 8},
        {id: 3, sku: 'A-03', name: 'Cinder Ceramic Mug', location: 'Aisle 2 · Bay A', quantity: 120},
        {id: 4, sku: 'A-04', name: 'Delta Lounge Chair', location: 'Aisle 2 · Bay D', quantity: 15},
        {id: 5, sku: 'A-05', name: 'Ember Electric Kettle', location: 'Aisle 3 · Bay B', quantity: 21},
        {id: 6, sku: 'A-06', name: 'Flare Wall Sconce', location: 'Aisle 3 · Bay E', quantity: 47},
        {id: 7, sku: 'A-07', name: 'Glow Pendant Lamp', location: 'Aisle 4 · Bay A', quantity: 12},
        {id: 8, sku: 'A-08', name: 'Harbor Bar Stool', location: 'Aisle 4 · Bay C', quantity: 3}
    ];

    const page = ref(1);
    const perPage = ref(6);

    const visibleItems = computed(() => inventory.slice((page.value - 1) * perPage.value, page.value * perPage.value));

    function onLimit(limit: number): void {
        perPage.value = limit;
        page.value = 1;
    }
</script>

Sticky header

A long list whose header and bar stick while scrolling.

Activity log
40 events
Time
Actor
Event
09:00
Mila Vega
signed in
09:07
Sven Halloran
updated the invoice #10241
09:14
Priya Nandal
archived a product
09:21
Theo Marsh
invited a team member
09:28
Lena Ortiz
exported the customer list
09:35
Mila Vega
changed the billing plan
09:42
Sven Halloran
signed in
09:49
Priya Nandal
updated the invoice #10241
09:56
Theo Marsh
archived a product
10:03
Lena Ortiz
invited a team member
10:10
Mila Vega
exported the customer list
10:17
Sven Halloran
changed the billing plan
10:24
Priya Nandal
signed in
10:31
Theo Marsh
updated the invoice #10241
10:38
Lena Ortiz
archived a product
10:45
Mila Vega
invited a team member
10:52
Sven Halloran
exported the customer list
10:59
Priya Nandal
changed the billing plan
11:06
Theo Marsh
signed in
11:13
Lena Ortiz
updated the invoice #10241
11:20
Mila Vega
archived a product
11:27
Sven Halloran
invited a team member
11:34
Priya Nandal
exported the customer list
11:41
Theo Marsh
changed the billing plan
11:48
Lena Ortiz
signed in
11:55
Mila Vega
updated the invoice #10241
12:02
Sven Halloran
archived a product
12:09
Priya Nandal
invited a team member
12:16
Theo Marsh
exported the customer list
12:23
Lena Ortiz
changed the billing plan
12:30
Mila Vega
signed in
12:37
Sven Halloran
updated the invoice #10241
12:44
Priya Nandal
archived a product
12:51
Theo Marsh
invited a team member
12:58
Lena Ortiz
exported the customer list
13:05
Mila Vega
changed the billing plan
13:12
Sven Halloran
signed in
13:19
Priya Nandal
updated the invoice #10241
13:26
Theo Marsh
archived a product
13:33
Lena Ortiz
invited a team member

<template>
    <FluxPane>
        <FluxDataTable
            :items="events"
            :limits="[]"
            :page="1"
            :per-page="events.length"
            :total="events.length"
            is-hoverable
            is-sticky
            style="max-height: 360px">
            <template #filter>
                <FluxTableBar>
                    <FluxFlex
                        align="center"
                        justify="between">
                        <strong>Activity log</strong>
                        <FluxBadge :label="`${events.length} events`"/>
                    </FluxFlex>
                </FluxTableBar>
            </template>

            <template #header>
                <FluxTableHeader is-shrinking>Time</FluxTableHeader>
                <FluxTableHeader :min-width="180">Actor</FluxTableHeader>
                <FluxTableHeader :min-width="220">Event</FluxTableHeader>
            </template>

            <template #time="{item}">
                <FluxTableCell
                    is-numeric
                    no-wrap>
                    {{ item.time }}
                </FluxTableCell>
            </template>

            <template #actor="{item}">
                <FluxTableCell no-wrap>{{ item.actor }}</FluxTableCell>
            </template>

            <template #event="{item}">
                <FluxTableCell>{{ item.event }}</FluxTableCell>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

<script
    setup
    lang="ts">
    import { FluxBadge, FluxDataTable, FluxFlex, FluxPane, FluxTableBar, FluxTableCell, FluxTableHeader } from '@flux-ui/components';

    type Event = {
        readonly id: number;
        readonly time: string;
        readonly actor: string;
        readonly event: string;
    };

    const actors = ['Mila Vega', 'Sven Halloran', 'Priya Nandal', 'Theo Marsh', 'Lena Ortiz'];
    const actions = [
        'signed in',
        'updated the invoice #10241',
        'archived a product',
        'invited a team member',
        'exported the customer list',
        'changed the billing plan'
    ];

    const events: Event[] = Array.from({length: 40}, (_, index) => {
        const minutes = index * 7;
        const hours = 9 + Math.floor(minutes / 60);
        const mins = minutes % 60;

        return {
            id: index + 1,
            time: `${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`,
            actor: actors[index % actors.length],
            event: actions[index % actions.length]
        };
    });
</script>

Sticky groups

Grouped rows whose header sticks while scrolling through the groups.

Name
Role
Location

<template>
    <FluxPane>
        <FluxDataTable
            v-model:collapsed-groups="collapsedGroups"
            :group-by="item => item.team"
            :items="members"
            :limits="[]"
            :page="1"
            :per-page="members.length"
            :total="members.length"
            unique-key="id"
            is-hoverable
            is-sticky
            style="max-height: 360px">
            <template #header>
                <FluxTableHeader :min-width="220">Name</FluxTableHeader>
                <FluxTableHeader :min-width="160">Role</FluxTableHeader>
                <FluxTableHeader is-shrinking>Location</FluxTableHeader>
            </template>

            <template #group="{id, items, isExpanded, toggle}">
                <FluxTableGroup
                    :icon="teamIcon[id]"
                    :is-expanded="isExpanded"
                    :label="String(id)"
                    is-expandable
                    @update:is-expanded="toggle">
                    <template #after>
                        <FluxBadge :label="`${items.length}`"/>
                    </template>
                </FluxTableGroup>
            </template>

            <template #name="{item}">
                <FluxTableCell
                    content-direction="column"
                    :content-gap="3">
                    <strong>{{ item.name }}</strong>
                    <small>{{ item.email }}</small>
                </FluxTableCell>
            </template>

            <template #role="{item}">
                <FluxTableCell>{{ item.role }}</FluxTableCell>
            </template>

            <template #location="{item}">
                <FluxTableCell no-wrap>{{ item.location }}</FluxTableCell>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

<script
    setup
    lang="ts">
    import { FluxBadge, FluxDataTable, FluxPane, FluxTableCell, FluxTableGroup, FluxTableHeader } from '@flux-ui/components';
    import type { FluxIconName } from '@flux-ui/types';
    import { ref } from 'vue';

    type Member = {
        readonly id: number;
        readonly team: string;
        readonly name: string;
        readonly email: string;
        readonly role: string;
        readonly location: string;
    };

    const teamIcon: Record<string, FluxIconName> = {
        Engineering: 'users',
        Design: 'palette',
        Operations: 'folder',
        Sales: 'chart-line'
    };

    const members: Member[] = [
        {id: 1, team: 'Engineering', name: 'Ada Lovelace', email: 'ada@example.com', role: 'Lead', location: 'London'},
        {id: 2, team: 'Engineering', name: 'Alan Turing', email: 'alan@example.com', role: 'Engineer', location: 'Manchester'},
        {id: 3, team: 'Engineering', name: 'Linus Powell', email: 'linus@example.com', role: 'Engineer', location: 'Berlin'},
        {id: 4, team: 'Engineering', name: 'Nadia Reyes', email: 'nadia@example.com', role: 'Engineer', location: 'Madrid'},
        {id: 5, team: 'Design', name: 'Grace Hopper', email: 'grace@example.com', role: 'Lead designer', location: 'New York'},
        {id: 6, team: 'Design', name: 'Theo Marsh', email: 'theo@example.com', role: 'Product designer', location: 'Toronto'},
        {id: 7, team: 'Design', name: 'Lena Ortiz', email: 'lena@example.com', role: 'Brand designer', location: 'Lisbon'},
        {id: 8, team: 'Operations', name: 'Katherine Johnson', email: 'katherine@example.com', role: 'Manager', location: 'Hampton'},
        {id: 9, team: 'Operations', name: 'Margaret Hamilton', email: 'margaret@example.com', role: 'Analyst', location: 'Boston'},
        {id: 10, team: 'Operations', name: 'Sven Halloran', email: 'sven@example.com', role: 'Coordinator', location: 'Oslo'},
        {id: 11, team: 'Sales', name: 'Mila Vega', email: 'mila@example.com', role: 'Account lead', location: 'Amsterdam'},
        {id: 12, team: 'Sales', name: 'Priya Nandal', email: 'priya@example.com', role: 'Account manager', location: 'Mumbai'},
        {id: 13, team: 'Sales', name: 'Oscar Bright', email: 'oscar@example.com', role: 'Sales rep', location: 'Chicago'}
    ];

    const collapsedGroups = ref<string[]>([]);
</script>

Skeleton loading

A data table whose loading slot replaces the spinner with skeleton rows.

Members
Name
Role
Commits
Ada Lovelace
Lead
1287
Alan Turing
Engineer
942
Grace Hopper
Engineer
1530
Margaret Hamilton
Engineer
2041

<template>
    <FluxPane>
        <FluxPaneHeader title="Members">
            <template #after>
                <FluxSecondaryButton
                    :disabled="isLoading"
                    icon-leading="rotate"
                    label="Refresh"
                    @click="refresh"/>
            </template>
        </FluxPaneHeader>

        <FluxDataTable
            :is-loading="isLoading"
            :items="people"
            :limits="[]"
            :page="1"
            :per-page="4"
            :total="people.length"
            is-hoverable>
            <template #header>
                <FluxTableHeader>Name</FluxTableHeader>
                <FluxTableHeader>Role</FluxTableHeader>
                <FluxTableHeader is-shrinking>Commits</FluxTableHeader>
            </template>

            <template #loading="{perPage}">
                <FluxTableRow
                    v-for="row in perPage"
                    :key="row">
                    <FluxTableCell
                        v-for="cell in 3"
                        :key="cell">
                        <FluxSkeleton/>
                    </FluxTableCell>
                </FluxTableRow>
            </template>

            <template #name="{item}">
                <FluxTableCell>{{ item.name }}</FluxTableCell>
            </template>

            <template #role="{item}">
                <FluxTableCell>{{ item.role }}</FluxTableCell>
            </template>

            <template #commits="{item}">
                <FluxTableCell is-numeric>{{ item.commits }}</FluxTableCell>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

<script
    setup
    lang="ts">
    import { FluxDataTable, FluxPane, FluxPaneHeader, FluxSecondaryButton, FluxSkeleton, FluxTableCell, FluxTableHeader, FluxTableRow } from '@flux-ui/components';
    import { ref } from 'vue';

    const isLoading = ref(false);

    const people = [
        {id: 1, name: 'Ada Lovelace', role: 'Lead', commits: 1287},
        {id: 2, name: 'Alan Turing', role: 'Engineer', commits: 942},
        {id: 3, name: 'Grace Hopper', role: 'Engineer', commits: 1530},
        {id: 4, name: 'Margaret Hamilton', role: 'Engineer', commits: 2041}
    ];

    function refresh(): void {
        isLoading.value = true;
        setTimeout(() => isLoading.value = false, 2000);
    }
</script>

Empty

A data table that shows a custom message when there is no data.

Name
Email
No customers found.

<template>
    <FluxPane>
        <FluxDataTable
            :items="items"
            :limits="[]"
            :page="1"
            :per-page="5"
            :total="0">
            <template #header>
                <FluxTableHeader>Name</FluxTableHeader>
                <FluxTableHeader>Email</FluxTableHeader>
            </template>

            <template #name="{item}">
                <FluxTableCell>{{ item.name }}</FluxTableCell>
            </template>

            <template #email="{item}">
                <FluxTableCell>{{ item.email }}</FluxTableCell>
            </template>

            <template #empty>
                <FluxFlex
                    align="center"
                    direction="vertical"
                    :gap="9"
                    justify="center"
                    style="padding: 45px 15px">
                    <FluxIcon
                        name="users"
                        :size="24"/>
                    <span>No customers found.</span>
                </FluxFlex>
            </template>
        </FluxDataTable>
    </FluxPane>
</template>

<script
    setup
    lang="ts">
    import { FluxDataTable, FluxFlex, FluxIcon, FluxPane, FluxTableCell, FluxTableHeader } from '@flux-ui/components';

    type Customer = {
        readonly id: number;
        readonly name: string;
        readonly email: string;
    };

    const items: Customer[] = [];
</script>

Used components