Skip to content

Table

The table presents data in rows and columns, giving users a structured way to scan and compare information. It is the low-level building block behind the Data table and can be composed by hand for full control over headers, cells, groups and rows.

Header 1
Header 2
Header 3
Cell 1×1
Cell 2×1
Cell 3×1
Cell 1×2
Cell 2×2
Cell 3×2
Cell 1×3
Cell 2×3
Cell 3×3

Accessible loading

While is-loading is set the table exposes aria-busy on the underlying <table>, so assistive technologies announce that the data is updating.

Props

caption-side?: "top" | "bottom"
The side where the caption should be placed.
Default: bottom

is-filled?: boolean
Renders a filler row that stretches to the bottom of the table, so the column dividers reach the bottom on full-height tables.

is-hoverable?: boolean
If each row should be highlighted on hover.

is-loading?: boolean
If the table is in a loading state.

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

Slots

default
The slot for the table content.

caption
The slot for the table caption.

empty
Renders a full-width row that stretches to the remaining height of the table, typically for an empty state. The content is vertically centered.

footer
The slot for the table footer.

header
The slot for the table headers.

loading
Replaces the default spinner overlay while `is-loading` is set. Renders inside the table body on the table's column grid, so skeleton rows built from `FluxTableRow` and `FluxTableCell` align with the columns. The regular rows are not rendered while this slot shows.

pagination
The slot for pagination.

Examples

Basic

A basic table.

Header 1
Header 2
Header 3
Cell 1×1
Cell 2×1
Cell 3×1
Cell 1×2
Cell 2×2
Cell 3×2
Cell 1×3
Cell 2×3
Cell 3×3

<template>
    <FluxTable>
        <template #header>
            <FluxTableHeader
                v-for="header in 3">
                Header {{ header }}
            </FluxTableHeader>
        </template>

        <FluxTableRow
            v-for="row in 3"
            :key="row">
            <FluxTableCell
                v-for="cell in 3"
                :key="cell">
                Cell {{ cell }}&times;{{ row }}
            </FluxTableCell>
        </FluxTableRow>
    </FluxTable>
</template>

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

Pane

A table inside a pane.

Header 1
Header 2
Header 3
Cell 1×1
Cell 2×1
Cell 3×1
Cell 1×2
Cell 2×2
Cell 3×2
Cell 1×3
Cell 2×3
Cell 3×3

<template>
    <FluxPane>
        <FluxTable>
            <template #header>
                <FluxTableHeader
                    v-for="header in 3">
                    Header {{ header }}
                </FluxTableHeader>
            </template>

            <FluxTableRow
                v-for="row in 3"
                :key="row">
                <FluxTableCell
                    v-for="cell in 3"
                    :key="cell">
                    Cell {{ cell }}&times;{{ row }}
                </FluxTableCell>
            </FluxTableRow>
        </FluxTable>
    </FluxPane>
</template>

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

Caption

A table with an caption.

Header 1
Header 2
Header 3
Cell 1×1
Cell 2×1
Cell 3×1
Cell 1×2
Cell 2×2
Cell 3×2
Cell 1×3
Cell 2×3
Cell 3×3
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet animi asperiores, commodi cupiditate doloremque eligendi exercitationem facilis, illum ipsa ipsam modi nesciunt nobis, odit quidem sit temporibus ullam. Et, nam.

<template>
    <FluxPane>
        <FluxTable>
            <template #header>
                <FluxTableHeader
                    v-for="header in 3">
                    Header {{ header }}
                </FluxTableHeader>
            </template>

            <FluxTableRow
                v-for="row in 3"
                :key="row">
                <FluxTableCell
                    v-for="cell in 3"
                    :key="cell">
                    Cell {{ cell }}&times;{{ row }}
                </FluxTableCell>
            </FluxTableRow>

            <template #caption>
                Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet animi asperiores, commodi cupiditate doloremque eligendi exercitationem facilis, illum ipsa ipsam modi nesciunt nobis, odit quidem sit temporibus ullam. Et, nam.
            </template>
        </FluxTable>
    </FluxPane>
</template>

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

Caption on top

A table whose caption renders above the table instead of below it.

Header 1
Header 2
Header 3
Cell 1×1
Cell 2×1
Cell 3×1
Cell 1×2
Cell 2×2
Cell 3×2
Cell 1×3
Cell 2×3
Cell 3×3
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet animi asperiores, commodi cupiditate doloremque eligendi exercitationem facilis, illum ipsa ipsam modi nesciunt nobis, odit quidem sit temporibus ullam. Et, nam.

<template>
    <FluxPane>
        <FluxTable caption-side="top">
            <template #header>
                <FluxTableHeader
                    v-for="header in 3">
                    Header {{ header }}
                </FluxTableHeader>
            </template>

            <FluxTableRow
                v-for="row in 3"
                :key="row">
                <FluxTableCell
                    v-for="cell in 3"
                    :key="cell">
                    Cell {{ cell }}&times;{{ row }}
                </FluxTableCell>
            </FluxTableRow>

            <template #caption>
                Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet animi asperiores, commodi cupiditate doloremque eligendi exercitationem facilis, illum ipsa ipsam modi nesciunt nobis, odit quidem sit temporibus ullam. Et, nam.
            </template>
        </FluxTable>
    </FluxPane>
</template>

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

Sticky

A table with sticky headers.

Header 1
Header 2
Header 3
Cell 1×1
Cell 2×1
Cell 3×1
Cell 1×2
Cell 2×2
Cell 3×2
Cell 1×3
Cell 2×3
Cell 3×3
Cell 1×4
Cell 2×4
Cell 3×4
Cell 1×5
Cell 2×5
Cell 3×5
Cell 1×6
Cell 2×6
Cell 3×6
Cell 1×7
Cell 2×7
Cell 3×7
Cell 1×8
Cell 2×8
Cell 3×8
Cell 1×9
Cell 2×9
Cell 3×9
Cell 1×10
Cell 2×10
Cell 3×10
Cell 1×11
Cell 2×11
Cell 3×11
Cell 1×12
Cell 2×12
Cell 3×12
Cell 1×13
Cell 2×13
Cell 3×13
Cell 1×14
Cell 2×14
Cell 3×14
Cell 1×15
Cell 2×15
Cell 3×15
Cell 1×16
Cell 2×16
Cell 3×16
Cell 1×17
Cell 2×17
Cell 3×17
Cell 1×18
Cell 2×18
Cell 3×18
Cell 1×19
Cell 2×19
Cell 3×19
Cell 1×20
Cell 2×20
Cell 3×20
Cell 1×21
Cell 2×21
Cell 3×21
Cell 1×22
Cell 2×22
Cell 3×22
Cell 1×23
Cell 2×23
Cell 3×23
Cell 1×24
Cell 2×24
Cell 3×24
Cell 1×25
Cell 2×25
Cell 3×25
Cell 1×26
Cell 2×26
Cell 3×26
Cell 1×27
Cell 2×27
Cell 3×27
Cell 1×28
Cell 2×28
Cell 3×28
Cell 1×29
Cell 2×29
Cell 3×29
Cell 1×30
Cell 2×30
Cell 3×30

<template>
    <FluxPane>
        <FluxTable
            is-sticky
            style="max-height: 330px">
            <template #header>
                <FluxTableHeader
                    is-shrinking
                    v-for="header in 3">
                    Header {{ header }}
                </FluxTableHeader>
            </template>

            <FluxTableRow
                v-for="row in 30"
                :key="row">
                <FluxTableCell
                    v-for="cell in 3"
                    :key="cell">
                    Cell {{ cell }}&times;{{ row }}
                </FluxTableCell>
            </FluxTableRow>
        </FluxTable>
    </FluxPane>
</template>

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

Sticky with bar

A table where the bar and headers stick together.

The bar and headers stick together while scrolling.
Header 1
Header 2
Header 3
Cell 1×1
Cell 2×1
Cell 3×1
Cell 1×2
Cell 2×2
Cell 3×2
Cell 1×3
Cell 2×3
Cell 3×3
Cell 1×4
Cell 2×4
Cell 3×4
Cell 1×5
Cell 2×5
Cell 3×5
Cell 1×6
Cell 2×6
Cell 3×6
Cell 1×7
Cell 2×7
Cell 3×7
Cell 1×8
Cell 2×8
Cell 3×8
Cell 1×9
Cell 2×9
Cell 3×9
Cell 1×10
Cell 2×10
Cell 3×10
Cell 1×11
Cell 2×11
Cell 3×11
Cell 1×12
Cell 2×12
Cell 3×12
Cell 1×13
Cell 2×13
Cell 3×13
Cell 1×14
Cell 2×14
Cell 3×14
Cell 1×15
Cell 2×15
Cell 3×15
Cell 1×16
Cell 2×16
Cell 3×16
Cell 1×17
Cell 2×17
Cell 3×17
Cell 1×18
Cell 2×18
Cell 3×18
Cell 1×19
Cell 2×19
Cell 3×19
Cell 1×20
Cell 2×20
Cell 3×20
Cell 1×21
Cell 2×21
Cell 3×21
Cell 1×22
Cell 2×22
Cell 3×22
Cell 1×23
Cell 2×23
Cell 3×23
Cell 1×24
Cell 2×24
Cell 3×24
Cell 1×25
Cell 2×25
Cell 3×25
Cell 1×26
Cell 2×26
Cell 3×26
Cell 1×27
Cell 2×27
Cell 3×27
Cell 1×28
Cell 2×28
Cell 3×28
Cell 1×29
Cell 2×29
Cell 3×29
Cell 1×30
Cell 2×30
Cell 3×30

<template>
    <FluxPane>
        <FluxTable
            is-sticky
            style="max-height: 330px">
            <template #header>
                <FluxTableBar>
                    <FluxFlex
                        align="center"
                        :gap="9">
                        <FluxIcon name="circle-info"/>
                        <span>The bar and headers stick together while scrolling.</span>
                    </FluxFlex>
                </FluxTableBar>

                <FluxTableRow>
                    <FluxTableHeader
                        v-for="header in 3"
                        :key="header">
                        Header {{ header }}
                    </FluxTableHeader>
                </FluxTableRow>
            </template>

            <FluxTableRow
                v-for="row in 30"
                :key="row">
                <FluxTableCell
                    v-for="cell in 3"
                    :key="cell">
                    Cell {{ cell }}&times;{{ row }}
                </FluxTableCell>
            </FluxTableRow>
        </FluxTable>
    </FluxPane>
</template>

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

Pinned columns

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

Project
Owner
Status
Priority
Start
Due
Client
Budget
Aurora Redesign
Mila Vega
In progress
High
Jan 8
Mar 2
Lumen Co.
€ 48,000
Borealis Mobile
Sven Halloran
Planned
Medium
Feb 1
Apr 18
Nordic Works
€ 32,500
Cinder Analytics
Priya Nandal
In progress
High
Jan 22
May 6
Clayworks
€ 61,200
Delta Onboarding
Theo Marsh
Review
Low
Dec 12
Feb 9
Nordic Works
€ 18,900
Ember Storefront
Lena Ortiz
In progress
Medium
Feb 14
Jun 1
Clayworks
€ 54,750

<template>
    <FluxPane>
        <FluxTable is-hoverable>
            <template #header>
                <FluxTableHeader
                    pinned
                    :min-width="180">
                    Project
                </FluxTableHeader>

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

                <FluxTableHeader :min-width="150">Status</FluxTableHeader>
                <FluxTableHeader :min-width="150">Priority</FluxTableHeader>
                <FluxTableHeader :min-width="150">Start</FluxTableHeader>
                <FluxTableHeader :min-width="150">Due</FluxTableHeader>
                <FluxTableHeader :min-width="180">Client</FluxTableHeader>

                <FluxTableHeader
                    align="end"
                    pinned="end"
                    :min-width="140">
                    Budget
                </FluxTableHeader>
            </template>

            <FluxTableRow
                v-for="project in projects"
                :key="project.id">
                <FluxTableCell>
                    <strong>{{ project.name }}</strong>
                </FluxTableCell>

                <FluxTableCell>{{ project.owner }}</FluxTableCell>
                <FluxTableCell>{{ project.status }}</FluxTableCell>
                <FluxTableCell>{{ project.priority }}</FluxTableCell>
                <FluxTableCell>{{ project.start }}</FluxTableCell>
                <FluxTableCell>{{ project.due }}</FluxTableCell>
                <FluxTableCell>{{ project.client }}</FluxTableCell>

                <FluxTableCell
                    align="end"
                    is-numeric>
                    {{ project.budget }}
                </FluxTableCell>
            </FluxTableRow>
        </FluxTable>
    </FluxPane>
</template>

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

    const projects = [
        {id: 1, name: 'Aurora Redesign', owner: 'Mila Vega', status: 'In progress', priority: 'High', start: 'Jan 8', due: 'Mar 2', client: 'Lumen Co.', budget: '€ 48,000'},
        {id: 2, name: 'Borealis Mobile', owner: 'Sven Halloran', status: 'Planned', priority: 'Medium', start: 'Feb 1', due: 'Apr 18', client: 'Nordic Works', budget: '€ 32,500'},
        {id: 3, name: 'Cinder Analytics', owner: 'Priya Nandal', status: 'In progress', priority: 'High', start: 'Jan 22', due: 'May 6', client: 'Clayworks', budget: '€ 61,200'},
        {id: 4, name: 'Delta Onboarding', owner: 'Theo Marsh', status: 'Review', priority: 'Low', start: 'Dec 12', due: 'Feb 9', client: 'Nordic Works', budget: '€ 18,900'},
        {id: 5, name: 'Ember Storefront', owner: 'Lena Ortiz', status: 'In progress', priority: 'Medium', start: 'Feb 14', due: 'Jun 1', client: 'Clayworks', budget: '€ 54,750'}
    ];
</script>

Column sizing

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

SKU
Product
Category
Stock
Status
AC-1024
Aurora Ceiling Light
Lighting
34
In stock
FD-2087
Borealis Standing Desk
Furniture
8
In stock
KD-3391
Cinder Ceramic Mug
Kitchen & dining
0
Sold out
FD-4410
Delta Lounge Chair
Furniture
15
In stock
KD-5502
Ember Electric Kettle
Kitchen & dining
21
In stock

<template>
    <FluxPane>
        <FluxTable 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>
                    Stock
                </FluxTableHeader>

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

            <FluxTableRow
                v-for="product in products"
                :key="product.sku">
                <FluxTableCell>{{ product.sku }}</FluxTableCell>

                <FluxTableCell>
                    <strong>{{ product.name }}</strong>
                </FluxTableCell>

                <FluxTableCell>{{ product.category }}</FluxTableCell>

                <FluxTableCell
                    align="end"
                    is-numeric>
                    {{ product.stock }}
                </FluxTableCell>

                <FluxTableCell>
                    <FluxBadge
                        :color="product.stock > 0 ? 'success' : 'danger'"
                        :label="product.stock > 0 ? 'In stock' : 'Sold out'"/>
                </FluxTableCell>
            </FluxTableRow>
        </FluxTable>
    </FluxPane>
</template>

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

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

    const products: Product[] = [
        {sku: 'AC-1024', name: 'Aurora Ceiling Light', category: 'Lighting', stock: 34},
        {sku: 'FD-2087', name: 'Borealis Standing Desk', category: 'Furniture', stock: 8},
        {sku: 'KD-3391', name: 'Cinder Ceramic Mug', category: 'Kitchen & dining', stock: 0},
        {sku: 'FD-4410', name: 'Delta Lounge Chair', category: 'Furniture', stock: 15},
        {sku: 'KD-5502', name: 'Ember Electric Kettle', category: 'Kitchen & dining', stock: 21}
    ];
</script>

Numeric alignment

Right-aligned numeric columns with sortable numeric headers.

Endpoint
Requests
Avg latency
Error rate
GET /api/products
302,914
47 ms
0.05%
GET /api/orders
128,420
84 ms
0.12%
GET /api/customers
58,210
132 ms
0.42%
POST /api/orders
24,180
213 ms
1.34%
DELETE /api/sessions
9,120
76 ms
2.18%

<template>
    <FluxPane>
        <FluxTable is-hoverable>
            <template #header>
                <FluxTableHeader :min-width="200">Endpoint</FluxTableHeader>

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

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

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

            <FluxTableRow
                v-for="row in sortedRows"
                :key="row.endpoint">
                <FluxTableCell>{{ row.endpoint }}</FluxTableCell>
                <FluxTableCell>{{ row.requests.toLocaleString('en') }}</FluxTableCell>
                <FluxTableCell>{{ row.latency }} ms</FluxTableCell>
                <FluxTableCell>{{ row.errorRate.toFixed(2) }}%</FluxTableCell>
            </FluxTableRow>
        </FluxTable>
    </FluxPane>
</template>

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

    type SortDirection = 'ascending' | 'descending';
    type SortColumn = 'requests' | 'latency' | 'errorRate';

    type Metric = {
        readonly endpoint: string;
        readonly requests: number;
        readonly latency: number;
        readonly errorRate: number;
    };

    const rows: Metric[] = [
        {endpoint: 'GET /api/orders', requests: 128420, latency: 84, errorRate: 0.12},
        {endpoint: 'POST /api/orders', requests: 24180, latency: 213, errorRate: 1.34},
        {endpoint: 'GET /api/products', requests: 302914, latency: 47, errorRate: 0.05},
        {endpoint: 'GET /api/customers', requests: 58210, latency: 132, errorRate: 0.42},
        {endpoint: 'DELETE /api/sessions', requests: 9120, latency: 76, errorRate: 2.18}
    ];

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

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

        if (!column) {
            return rows;
        }

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

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

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

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

Spanning cells

Section rows whose single cell spans every column.

Time
Session
Speaker
Room
Morning track
09:00
Designing for the grid
Mila Vega
Hall A
10:15
Accessible tables in practice
Sven Halloran
Hall A
Afternoon track
13:30
Subgrid deep dive
Priya Nandal
Hall B
15:00
From table to layout
Theo Marsh
Hall B

<template>
    <FluxPane>
        <FluxTable is-hoverable>
            <template #header>
                <FluxTableHeader :width="96">Time</FluxTableHeader>
                <FluxTableHeader :min-width="220">Session</FluxTableHeader>
                <FluxTableHeader :min-width="160">Speaker</FluxTableHeader>
                <FluxTableHeader is-shrinking>Room</FluxTableHeader>
            </template>

            <template
                v-for="block in schedule"
                :key="block.label">
                <FluxTableRow>
                    <FluxTableCell :colspan="4">
                        <strong>{{ block.label }}</strong>
                    </FluxTableCell>
                </FluxTableRow>

                <FluxTableRow
                    v-for="session in block.sessions"
                    :key="session.title">
                    <FluxTableCell is-numeric>{{ session.time }}</FluxTableCell>
                    <FluxTableCell>{{ session.title }}</FluxTableCell>
                    <FluxTableCell>{{ session.speaker }}</FluxTableCell>
                    <FluxTableCell>{{ session.room }}</FluxTableCell>
                </FluxTableRow>
            </template>
        </FluxTable>
    </FluxPane>
</template>

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

    type Session = {
        readonly time: string;
        readonly title: string;
        readonly speaker: string;
        readonly room: string;
    };

    type Block = {
        readonly label: string;
        readonly sessions: Session[];
    };

    const schedule: Block[] = [
        {
            label: 'Morning track',
            sessions: [
                {time: '09:00', title: 'Designing for the grid', speaker: 'Mila Vega', room: 'Hall A'},
                {time: '10:15', title: 'Accessible tables in practice', speaker: 'Sven Halloran', room: 'Hall A'}
            ]
        },
        {
            label: 'Afternoon track',
            sessions: [
                {time: '13:30', title: 'Subgrid deep dive', speaker: 'Priya Nandal', room: 'Hall B'},
                {time: '15:00', title: 'From table to layout', speaker: 'Theo Marsh', room: 'Hall B'}
            ]
        }
    ];
</script>

Spanning rows

A first column whose cell spans every row of its track through rowspan.

Track
Time
Session
Speaker
Hall A
09:00
Designing for the grid
Mila Vega
10:15
Accessible tables in practice
Sven Halloran
11:30
Layout without tables
Ada Fenwick
Hall B
13:30
Subgrid deep dive
Priya Nandal
15:00
From table to layout
Theo Marsh

<template>
    <FluxPane>
        <FluxTable is-hoverable>
            <template #header>
                <FluxTableHeader is-shrinking>Track</FluxTableHeader>
                <FluxTableHeader :width="96">Time</FluxTableHeader>
                <FluxTableHeader :min-width="220">Session</FluxTableHeader>
                <FluxTableHeader :min-width="160">Speaker</FluxTableHeader>
            </template>

            <template
                v-for="track in schedule"
                :key="track.name">
                <FluxTableRow
                    v-for="(session, index) in track.sessions"
                    :key="session.title">
                    <FluxTableCell
                        v-if="index === 0"
                        :rowspan="track.sessions.length">
                        <strong>{{ track.name }}</strong>
                    </FluxTableCell>

                    <FluxTableCell is-numeric>{{ session.time }}</FluxTableCell>
                    <FluxTableCell>{{ session.title }}</FluxTableCell>
                    <FluxTableCell>{{ session.speaker }}</FluxTableCell>
                </FluxTableRow>
            </template>
        </FluxTable>
    </FluxPane>
</template>

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

    type Session = {
        readonly time: string;
        readonly title: string;
        readonly speaker: string;
    };

    type Track = {
        readonly name: string;
        readonly sessions: Session[];
    };

    const schedule: Track[] = [
        {
            name: 'Hall A',
            sessions: [
                {time: '09:00', title: 'Designing for the grid', speaker: 'Mila Vega'},
                {time: '10:15', title: 'Accessible tables in practice', speaker: 'Sven Halloran'},
                {time: '11:30', title: 'Layout without tables', speaker: 'Ada Fenwick'}
            ]
        },
        {
            name: 'Hall B',
            sessions: [
                {time: '13:30', title: 'Subgrid deep dive', speaker: 'Priya Nandal'},
                {time: '15:00', title: 'From table to layout', speaker: 'Theo Marsh'}
            ]
        }
    ];
</script>

Line items with a footer that sums the amounts across spanning cells.

Description
Qty
Unit price
Amount
UX audit and research
12
€95.00
€1,140.00
Design system components
24
€110.00
€2,640.00
Prototype and handoff
8
€120.00
€960.00
Accessibility review
6
€85.00
€510.00
Subtotal
€5,250.00
VAT 21%
€1,102.50
Total
€6,352.50

<template>
    <FluxPane>
        <FluxTable>
            <template #header>
                <FluxTableHeader :min-width="240">Description</FluxTableHeader>

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

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

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

            <FluxTableRow
                v-for="line in lines"
                :key="line.id">
                <FluxTableCell>{{ line.description }}</FluxTableCell>

                <FluxTableCell
                    align="end"
                    is-numeric>
                    {{ line.quantity }}
                </FluxTableCell>

                <FluxTableCell
                    align="end"
                    is-numeric>
                    {{ formatCurrency(line.unitPrice) }}
                </FluxTableCell>

                <FluxTableCell
                    align="end"
                    is-numeric>
                    {{ formatCurrency(line.quantity * line.unitPrice) }}
                </FluxTableCell>
            </FluxTableRow>

            <template #footer>
                <FluxTableRow>
                    <FluxTableCell
                        align="end"
                        :colspan="3">
                        Subtotal
                    </FluxTableCell>

                    <FluxTableCell
                        align="end"
                        is-numeric>
                        {{ formatCurrency(subtotal) }}
                    </FluxTableCell>
                </FluxTableRow>

                <FluxTableRow>
                    <FluxTableCell
                        align="end"
                        :colspan="3">
                        VAT 21%
                    </FluxTableCell>

                    <FluxTableCell
                        align="end"
                        is-numeric>
                        {{ formatCurrency(vat) }}
                    </FluxTableCell>
                </FluxTableRow>

                <FluxTableRow>
                    <FluxTableCell
                        align="end"
                        :colspan="3">
                        <strong>Total</strong>
                    </FluxTableCell>

                    <FluxTableCell
                        align="end"
                        is-numeric>
                        <strong>{{ formatCurrency(total) }}</strong>
                    </FluxTableCell>
                </FluxTableRow>
            </template>
        </FluxTable>
    </FluxPane>
</template>

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

    type InvoiceLine = {
        readonly id: number;
        readonly description: string;
        readonly quantity: number;
        readonly unitPrice: number;
    };

    const lines: InvoiceLine[] = [
        {id: 1, description: 'UX audit and research', quantity: 12, unitPrice: 95},
        {id: 2, description: 'Design system components', quantity: 24, unitPrice: 110},
        {id: 3, description: 'Prototype and handoff', quantity: 8, unitPrice: 120},
        {id: 4, description: 'Accessibility review', quantity: 6, unitPrice: 85}
    ];

    const subtotal = computed(() => lines.reduce((sum, line) => sum + line.quantity * line.unitPrice, 0));
    const vat = computed(() => subtotal.value * 0.21);
    const total = computed(() => subtotal.value + vat.value);

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

Grouped rows

Rows organized under collapsible groups.

Name
Role

<template>
    <FluxPane>
        <FluxTable is-hoverable>
            <template #header>
                <FluxTableHeader :min-width="200">Name</FluxTableHeader>
                <FluxTableHeader is-shrinking>Role</FluxTableHeader>
            </template>

            <FluxTableGroup
                v-for="group in groups"
                :key="group.name"
                v-model:is-expanded="expanded[group.name]"
                :icon="group.icon"
                :label="group.name"
                is-expandable>
                <template #after>
                    <FluxBadge :label="`${group.members.length}`"/>
                </template>

                <FluxTableRow
                    v-for="member in group.members"
                    :key="member.name">
                    <FluxTableCell>{{ member.name }}</FluxTableCell>
                    <FluxTableCell>{{ member.role }}</FluxTableCell>
                </FluxTableRow>
            </FluxTableGroup>
        </FluxTable>
    </FluxPane>
</template>

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

    type Group = {
        readonly name: string;
        readonly icon: FluxIconName;
        readonly members: { readonly name: string; readonly role: string }[];
    };

    const groups: Group[] = [
        {
            name: 'Engineering',
            icon: 'users',
            members: [
                {name: 'Ada Lovelace', role: 'Lead'},
                {name: 'Alan Turing', role: 'Engineer'}
            ]
        },
        {
            name: 'Design',
            icon: 'palette',
            members: [
                {name: 'Grace Hopper', role: 'Designer'}
            ]
        },
        {
            name: 'Operations',
            icon: 'folder',
            members: [
                {name: 'Katherine Johnson', role: 'Manager'},
                {name: 'Margaret Hamilton', role: 'Analyst'}
            ]
        }
    ];

    const expanded = reactive<Record<string, boolean>>({
        Engineering: true,
        Design: true,
        Operations: false
    });
</script>

Sticky groups

Collapsible groups whose column header sticks while scrolling.

Name
Role
Location

<template>
    <FluxPane>
        <FluxTable
            is-hoverable
            is-sticky
            style="max-height: 360px">
            <template #header>
                <FluxTableHeader :min-width="200">Name</FluxTableHeader>
                <FluxTableHeader :min-width="160">Role</FluxTableHeader>
                <FluxTableHeader is-shrinking>Location</FluxTableHeader>
            </template>

            <FluxTableGroup
                v-for="group in groups"
                :key="group.name"
                v-model:is-expanded="expanded[group.name]"
                :icon="group.icon"
                :label="group.name"
                is-expandable>
                <template #after>
                    <FluxBadge :label="`${group.members.length}`"/>
                </template>

                <FluxTableRow
                    v-for="member in group.members"
                    :key="member.name">
                    <FluxTableCell>{{ member.name }}</FluxTableCell>
                    <FluxTableCell>{{ member.role }}</FluxTableCell>
                    <FluxTableCell no-wrap>{{ member.location }}</FluxTableCell>
                </FluxTableRow>
            </FluxTableGroup>
        </FluxTable>
    </FluxPane>
</template>

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

    type Member = {
        readonly name: string;
        readonly role: string;
        readonly location: string;
    };

    type Group = {
        readonly name: string;
        readonly icon: FluxIconName;
        readonly members: Member[];
    };

    const groups: Group[] = [
        {
            name: 'Engineering',
            icon: 'users',
            members: [
                {name: 'Ada Lovelace', role: 'Lead', location: 'London'},
                {name: 'Alan Turing', role: 'Engineer', location: 'Manchester'},
                {name: 'Linus Powell', role: 'Engineer', location: 'Berlin'},
                {name: 'Nadia Reyes', role: 'Engineer', location: 'Madrid'}
            ]
        },
        {
            name: 'Design',
            icon: 'palette',
            members: [
                {name: 'Grace Hopper', role: 'Lead designer', location: 'New York'},
                {name: 'Theo Marsh', role: 'Product designer', location: 'Toronto'},
                {name: 'Lena Ortiz', role: 'Brand designer', location: 'Lisbon'}
            ]
        },
        {
            name: 'Operations',
            icon: 'folder',
            members: [
                {name: 'Katherine Johnson', role: 'Manager', location: 'Hampton'},
                {name: 'Margaret Hamilton', role: 'Analyst', location: 'Boston'},
                {name: 'Sven Halloran', role: 'Coordinator', location: 'Oslo'}
            ]
        },
        {
            name: 'Sales',
            icon: 'chart-line',
            members: [
                {name: 'Mila Vega', role: 'Account lead', location: 'Amsterdam'},
                {name: 'Priya Nandal', role: 'Account manager', location: 'Mumbai'},
                {name: 'Oscar Bright', role: 'Sales rep', location: 'Chicago'}
            ]
        }
    ];

    const expanded = reactive<Record<string, boolean>>({
        Engineering: true,
        Design: true,
        Operations: true,
        Sales: true
    });
</script>

Stacked content

Cells that stack a primary and secondary line of content.

Member
Role
Location
Ada Lovelaceada@example.com
Lead engineerPlatform
London
Alan Turingalan@example.com
EngineerPlatform
Manchester
Grace Hoppergrace@example.com
Product designerDesign
New York
Katherine Johnsonkatherine@example.com
Engineering managerOperations
Hampton

<template>
    <FluxPane>
        <FluxTable is-hoverable>
            <template #header>
                <FluxTableHeader :min-width="220">Member</FluxTableHeader>
                <FluxTableHeader :min-width="160">Role</FluxTableHeader>
                <FluxTableHeader is-shrinking>Location</FluxTableHeader>
            </template>

            <FluxTableRow
                v-for="member in members"
                :key="member.email">
                <FluxTableCell
                    content-direction="column"
                    :content-gap="3">
                    <strong>{{ member.name }}</strong>
                    <small>{{ member.email }}</small>
                </FluxTableCell>

                <FluxTableCell
                    content-direction="column"
                    :content-gap="3">
                    <span>{{ member.role }}</span>
                    <small>{{ member.department }}</small>
                </FluxTableCell>

                <FluxTableCell>{{ member.location }}</FluxTableCell>
            </FluxTableRow>
        </FluxTable>
    </FluxPane>
</template>

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

    type Member = {
        readonly name: string;
        readonly email: string;
        readonly role: string;
        readonly department: string;
        readonly location: string;
    };

    const members: Member[] = [
        {name: 'Ada Lovelace', email: 'ada@example.com', role: 'Lead engineer', department: 'Platform', location: 'London'},
        {name: 'Alan Turing', email: 'alan@example.com', role: 'Engineer', department: 'Platform', location: 'Manchester'},
        {name: 'Grace Hopper', email: 'grace@example.com', role: 'Product designer', department: 'Design', location: 'New York'},
        {name: 'Katherine Johnson', email: 'katherine@example.com', role: 'Engineering manager', department: 'Operations', location: 'Hampton'}
    ];
</script>

Wrapping rows

A wrapping column whose siblings stay aligned and on a single line.

Version
Date
Change
2.4.0
Mar 2
Migrated the table components to CSS Grid so rows share a single track template and columns can be pinned, sized and spanned without wrapper elements.
2.3.1
Feb 18
Fixed sticky header alignment when a table bar is present and corrected the focus ring on sortable column headers.
2.3.0
Feb 4
Added per-column sizing props (width, min-width and max-width) and a shrinking mode for columns that should hug their content.
2.2.0
Jan 20
Introduced collapsible table groups with an optional trailing badge slot for counts and other metadata.

<template>
    <FluxPane>
        <FluxTable>
            <template #header>
                <FluxTableHeader :width="96">Version</FluxTableHeader>
                <FluxTableHeader is-shrinking>Date</FluxTableHeader>
                <FluxTableHeader :min-width="280">Change</FluxTableHeader>
            </template>

            <FluxTableRow
                v-for="entry in changelog"
                :key="entry.version">
                <FluxTableCell no-wrap>
                    <strong>{{ entry.version }}</strong>
                </FluxTableCell>

                <FluxTableCell no-wrap>{{ entry.date }}</FluxTableCell>
                <FluxTableCell>{{ entry.change }}</FluxTableCell>
            </FluxTableRow>
        </FluxTable>
    </FluxPane>
</template>

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

    type ChangelogEntry = {
        readonly version: string;
        readonly date: string;
        readonly change: string;
    };

    const changelog: ChangelogEntry[] = [
        {version: '2.4.0', date: 'Mar 2', change: 'Migrated the table components to CSS Grid so rows share a single track template and columns can be pinned, sized and spanned without wrapper elements.'},
        {version: '2.3.1', date: 'Feb 18', change: 'Fixed sticky header alignment when a table bar is present and corrected the focus ring on sortable column headers.'},
        {version: '2.3.0', date: 'Feb 4', change: 'Added per-column sizing props (width, min-width and max-width) and a shrinking mode for columns that should hug their content.'},
        {version: '2.2.0', date: 'Jan 20', change: 'Introduced collapsible table groups with an optional trailing badge slot for counts and other metadata.'}
    ];
</script>

Consistent height

A short table that keeps a fixed height with filler rows.

Time
Guest
Party
Table
18:30
Vega party
2
T4
19:00
Halloran
4
T9
20:15
Nandal
3
T2

<template>
    <FluxPane>
        <FluxTable
            is-filled
            is-hoverable
            style="min-height: 360px">
            <template #header>
                <FluxTableHeader :width="96">Time</FluxTableHeader>
                <FluxTableHeader :min-width="180">Guest</FluxTableHeader>

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

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

            <FluxTableRow
                v-for="booking in bookings"
                :key="booking.id">
                <FluxTableCell is-numeric>{{ booking.time }}</FluxTableCell>
                <FluxTableCell>{{ booking.guest }}</FluxTableCell>

                <FluxTableCell
                    align="end"
                    is-numeric>
                    {{ booking.party }}
                </FluxTableCell>

                <FluxTableCell>{{ booking.table }}</FluxTableCell>
            </FluxTableRow>
        </FluxTable>
    </FluxPane>
</template>

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

    type Booking = {
        readonly id: number;
        readonly time: string;
        readonly guest: string;
        readonly party: number;
        readonly table: string;
    };

    const bookings: Booking[] = [
        {id: 1, time: '18:30', guest: 'Vega party', party: 2, table: 'T4'},
        {id: 2, time: '19:00', guest: 'Halloran', party: 4, table: 'T9'},
        {id: 3, time: '20:15', guest: 'Nandal', party: 3, table: 'T2'}
    ];
</script>

Actions

A table with actions.

Header 1
Header 2
Header 3
Cell 1×1
Cell 2×1
Cell 3×1
Cell 1×2
Cell 2×2
Cell 3×2
Cell 1×3
Cell 2×3
Cell 3×3

<template>
    <FluxPane>
        <FluxTable>
            <template #header>
                <FluxTableHeader
                    v-for="header in 3">
                    Header {{ header }}
                </FluxTableHeader>
                <FluxTableHeader is-shrinking/>
            </template>

            <FluxTableRow
                v-for="row in 3"
                :key="row">
                <FluxTableCell
                    v-for="cell in 3"
                    :key="cell">
                    Cell {{ cell }}&times;{{ row }}
                </FluxTableCell>

                <FluxTableCell>
                    <FluxTableActions>
                        <FluxAction
                            icon="pen"/>
                        <FluxAction
                            icon="ellipsis-h"/>
                    </FluxTableActions>
                </FluxTableCell>
            </FluxTableRow>
        </FluxTable>
    </FluxPane>
</template>

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

Loading

A table with a loading state.

Header 1
Header 2
Header 3
Cell 1×1
Cell 2×1
Cell 3×1
Cell 1×2
Cell 2×2
Cell 3×2
Cell 1×3
Cell 2×3
Cell 3×3

<template>
    <FluxPane>
        <FluxTable
            is-loading>
            <template #header>
                <FluxTableHeader
                    v-for="header in 3">
                    Header {{ header }}
                </FluxTableHeader>
            </template>

            <FluxTableRow
                v-for="row in 3"
                :key="row">
                <FluxTableCell
                    v-for="cell in 3"
                    :key="cell">
                    Cell {{ cell }}&times;{{ row }}
                </FluxTableCell>
            </FluxTableRow>
        </FluxTable>
    </FluxPane>
</template>

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

Skeleton loading

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

Header 1
Header 2
Header 3

<template>
    <FluxPane>
        <FluxTable is-loading>
            <template #header>
                <FluxTableHeader
                    v-for="header in 3">
                    Header {{ header }}
                </FluxTableHeader>
            </template>

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

            <FluxTableRow
                v-for="row in 3"
                :key="row">
                <FluxTableCell
                    v-for="cell in 3"
                    :key="cell">
                    Cell {{ cell }}&times;{{ row }}
                </FluxTableCell>
            </FluxTableRow>
        </FluxTable>
    </FluxPane>
</template>

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

Hoverable

A table with rows that have a hoverable state.

Header 1
Header 2
Header 3
Cell 1×1
Cell 2×1
Cell 3×1
Cell 1×2
Cell 2×2
Cell 3×2
Cell 1×3
Cell 2×3
Cell 3×3

<template>
    <FluxPane>
        <FluxTable
            is-hoverable>
            <template #header>
                <FluxTableHeader
                    v-for="header in 3">
                    Header {{ header }}
                </FluxTableHeader>
            </template>

            <FluxTableRow
                v-for="row in 3"
                :key="row">
                <FluxTableCell
                    v-for="cell in 3"
                    :key="cell">
                    Cell {{ cell }}&times;{{ row }}
                </FluxTableCell>
            </FluxTableRow>
        </FluxTable>
    </FluxPane>
</template>

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

Used components