Table

A simple, stateless, composable table component for rendering tabular data. Unlike DataTable, this component does not include virtual scrolling, sorting, or column management — it is designed for straightforward data display using a declarative API.

Install

#Import

use gpui_kit::component::table::{
    Table, TableHeader, TableBody, TableFooter,
    TableRow, TableHead, TableCell, TableCaption,
};
// Installed in ui/components. That directory is on the Slint include path, so import by file name:
import { Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption } from "table.slint";
use iced::widget::{column, container, mouse_area, row, svg, text};
use iced::{Alignment, Element, Length, Task, Theme};

#Usage

#Basic table

Table::new()
    .child(TableHeader::new().child(
        TableRow::new()
            .child(TableHead::new().child("Name"))
            .child(TableHead::new().child("Email"))
            .child(TableHead::new().text_right().child("Amount"))
    ))
    .child(TableBody::new()
        .child(TableRow::new()
            .child(TableCell::new().child("John"))
            .child(TableCell::new().child("john@example.com"))
            .child(TableCell::new().text_right().child("$100.00")))
        .child(TableRow::new()
            .child(TableCell::new().child("Jane"))
            .child(TableCell::new().child("jane@example.com"))
            .child(TableCell::new().text_right().child("$200.00")))
    )
    .child(TableCaption::new().child("A list of recent invoices."))
Table {
    TableHeader {
        TableRow {
            TableHead {
                text: "Invoice";
            }
            TableHead {
                text: "Status";
            }
            TableHead {
                text: "Method";
            }
            TableHead {
                text: "Amount";
                align: TextHorizontalAlignment.right;
            }
        }
    }
    TableBody {
        for row[
            i
        ]
        in invoices: TableRow {
            index: i;
            TableCell {
                text: row.invoice;
            }
            TableCell {
                text: row.status;
            }
            TableCell {
                text: row.method;
            }
            TableCell {
                text: row.amount;
                align: TextHorizontalAlignment.right;
            }
        }
    }
    TableCaption {
        text: "A list of your recent invoices.";
    }
}
// The header band, twelve rows in a bounded scroll area, and the caption.
// Slint stacks the rows and lets the page scroll; this port puts the body on
// `scrollable`, so the header and the caption stay while the rows move. The
// `index` on each row is what draws the rule above it.
let mut basic_rows = Vec::new();
for (index, (invoice, status, method, amount)) in INVOICES.iter().enumerate() {
    basic_rows.push(invoice_row(
        index,
        invoice,
        status,
        method,
        amount,
        ControlSize::Medium,
        false,
    ));
}

let basic = Table::new()
    .header(TableHeader::new().row(
        TableRow::new()
            .head("Invoice")
            .head("Status")
            .head("Method")
            .head(TableHead::new("Amount").align(Align::End)),
    ))
    .body(TableBody::new(basic_rows).height(Length::Fixed(BODY_HEIGHT)))
    .caption("A list of your recent invoices.");

#Striped, bordered and sizes

Table::new()
    .child(TableHeader::new().child(
        TableRow::new()
            .child(TableHead::new().child("Invoice"))
            .child(TableHead::new().child("Status"))
            .child(TableHead::new().text_right().child("Amount"))
    ))
    .child(TableBody::new()
        .child(TableRow::new()
            .child(TableCell::new().child("INV001"))
            .child(TableCell::new().child("Paid"))
            .child(TableCell::new().text_right().child("$250.00")))
    )
    .child(TableFooter::new().child(
        TableRow::new()
            .child(TableCell::new().child("Total"))
            .child(TableCell::new().child(""))
            .child(TableCell::new().text_right().child("$250.00"))
    ))
GridLayout {
    spacing: 8px;
    Button {
        horizontal-stretch: 0;
        text: "Striped";
        selected: striped;
        clicked => {
            striped = !striped;
        }
    }
    Button {
        horizontal-stretch: 0;
        text: "Bordered";
        selected: bordered;
        clicked => {
            bordered = !bordered;
        }
    }
    Rectangle {
        horizontal-stretch: 0;
        width: 8px;
    }
    Button {
        horizontal-stretch: 0;
        text: "Small";
        selected: table-size == ControlSize.small;
        clicked => {
            table-size = ControlSize.small;
        }
    }
    Button {
        horizontal-stretch: 0;
        text: "Medium";
        selected: table-size == ControlSize.medium;
        clicked => {
            table-size = ControlSize.medium;
        }
    }
    Button {
        horizontal-stretch: 0;
        text: "Large";
        selected: table-size == ControlSize.large;
        clicked => {
            table-size = ControlSize.large;
        }
    }
}
Table {
    bordered: bordered;
    TableHeader {
        TableRow {
            TableHead {
                text: "Invoice";
                size: table-size;
            }
            TableHead {
                text: "Status";
                size: table-size;
                align: TextHorizontalAlignment.center;
            }
            TableHead {
                text: "Method";
                size: table-size;
            }
            TableHead {
                text: "Amount";
                size: table-size;
                align: TextHorizontalAlignment.right;
            }
        }
    }
    TableBody {
        for row[
            i
        ]
        in invoices: TableRow {
            index: i;
            striped: striped;
            TableCell {
                text: row.invoice;
                size: table-size;
            }
            TableCell {
                size: table-size;
                align: TextHorizontalAlignment.center;
                VerticalLayout {
                    alignment: center;
                    Rectangle {
                        property <color> tone: row.status == "Paid" ? Theme.success : row.status == "Pending" ? Theme.warning : Theme.danger;
                        height: 20px;
                        border-radius: Theme.radius-sm;
                        background: Theme.tint(tone);
                        HorizontalLayout {
                            padding-left: 6px;
                            padding-right: 6px;
                            Text {
                                text: row.status;
                                color: tone;
                                font-size: Theme.font-size-xs;
                                font-weight: 500;
                                vertical-alignment: center;
                            }
                        }
                    }
                }
            }
            TableCell {
                text: row.method;
                size: table-size;
            }
            TableCell {
                text: row.amount;
                size: table-size;
                align: TextHorizontalAlignment.right;
            }
        }
    }
    TableFooter {
        TableRow {
            TableCell {
                text: "Total";
                size: table-size;
                col-span: 3;
            }
            TableCell {
                text: "$1,750.00";
                size: table-size;
                align: TextHorizontalAlignment.right;
            }
        }
    }
}
// The three switches the Slint example puts above its table. Only what the
// caller changes is state: the table itself reads the switch back.
let controls = row![
    Button::new("Striped")
        .selected(app.striped)
        .on_press(Message::ToggleStriped),
    Button::new("Bordered")
        .selected(app.bordered)
        .on_press(Message::ToggleBordered),
    Button::new("Small")
        .selected(app.size == ControlSize::Small)
        .on_press(Message::SetSize(ControlSize::Small)),
    Button::new("Medium")
        .selected(app.size == ControlSize::Medium)
        .on_press(Message::SetSize(ControlSize::Medium)),
    Button::new("Large")
        .selected(app.size == ControlSize::Large)
        .on_press(Message::SetSize(ControlSize::Large)),
]
.spacing(theme::SPACING_SM);

let mut striped_rows = Vec::new();
for (index, (invoice, status, method, amount)) in INVOICES.iter().take(5).enumerate() {
    striped_rows.push(
        invoice_row(
            index,
            invoice,
            status,
            method,
            amount,
            app.size,
            true,
        )
        .striped(app.striped),
    );
}

let striped = Table::new()
    .bordered(app.bordered)
    .header(TableHeader::new().row(
        TableRow::new()
            .head(TableHead::new("Invoice").size(app.size))
            .head(TableHead::new("Status").size(app.size).align(Align::Center))
            .head(TableHead::new("Method").size(app.size))
            .head(TableHead::new("Amount").size(app.size).align(Align::End)),
    ))
    .body(TableBody::new(striped_rows))
    .footer(TableFooter::new().row(
        TableRow::new()
            .cell(TableCell::new("Total").span(3).size(app.size))
            .cell(
                TableCell::new("$1,750.00")
                    .align(Align::End)
                    .size(app.size),
            ),
    ));

#Column widths

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// A width on the head and the cell of a column pins it, and the columns
// without one share what is left. Slint expresses the same thing with
// `width` and leaves the rest at its preferred width.
let widths = Table::new()
    .header(TableHeader::new().row(
        TableRow::new()
            .head(TableHead::new("ID").width(80.0))
            .head("Name")
            .head(TableHead::new("Date").width(120.0)),
    ))
    .body(TableBody::new([
        TableRow::new()
            .index(0)
            .cell(TableCell::new("INV-0042").mono(true).width(80.0))
            .cell("Northwind Trading")
            .cell(TableCell::new("2026-11-10").width(120.0)),
        TableRow::new()
            .index(1)
            .cell(TableCell::new("INV-0041").mono(true).width(80.0))
            .cell("Contoso Ltd")
            .cell(TableCell::new("2026-11-09").width(120.0)),
    ]));

Use .w() on TableHead and TableCell to set fixed column widths:

TableRow::new()
    .child(TableHead::new().w(px(80.)).child("ID"))
    .child(TableHead::new().child("Name"))  // flex-1
    .child(TableHead::new().w(px(120.)).child("Date"))
SlintNo Slint version of this example yet.

#Text Alignment

// Center-aligned header
TableHead::new().text_center().child("Status")

// Right-aligned cell (e.g., for numbers)
TableCell::new().text_right().child("$1,000.00")
SlintNo Slint version of this example yet.
// A center-aligned header and an end-aligned number, the two alignments a
// table needs beyond the default start.
let alignment = Table::new()
    .header(TableHeader::new().row(
        TableRow::new()
            .head("Item")
            .head(TableHead::new("Status").align(Align::Center))
            .head(TableHead::new("Amount").align(Align::End)),
    ))
    .body(TableBody::new([
        TableRow::new()
            .index(0)
            .cell("Team plan")
            .cell(TableCell::new("Active").align(Align::Center))
            .cell(TableCell::new("$250.00").align(Align::End).mono(true)),
        TableRow::new()
            .index(1)
            .cell("Seats")
            .cell(TableCell::new("12").align(Align::Center))
            .cell(TableCell::new("$60.00").align(Align::End).mono(true)),
    ]));

#Without Border (via Styled)

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// GPUI drops the edge with `.border_0().rounded_none()`. iced has no `Styled`
// trait, so the component carries the switch: `bordered(false)` keeps the
// fill and the bands and removes the edge and the corners.
let plain = Table::new()
    .bordered(false)
    .header(TableHeader::new().row(
        TableRow::new()
            .head("Method")
            .head(TableHead::new("Amount").align(Align::End)),
    ))
    .body(TableBody::new([
        TableRow::new()
            .index(0)
            .cell("Credit Card")
            .cell(TableCell::new("$250.00").align(Align::End).mono(true)),
        TableRow::new()
            .index(1)
            .cell("PayPal")
            .cell(TableCell::new("$150.00").align(Align::End).mono(true)),
    ]));

All table sub-components implement the Styled trait, so you can customize styles directly:

// Remove border and rounded corners
Table::new()
    .border_0()
    .rounded_none()
    .child(/* ... */)
SlintNo Slint version of this example yet.

#Custom Styling

Since all components implement Styled, you can apply any GPUI style:

// Custom row hover
TableRow::new()
    .bg(cx.theme().table_even)
    .child(/* ... */)

// Custom cell padding
TableCell::new()
    .px_4()
    .child("Custom padded content")
SlintNo Slint version of this example yet.

#Sub-components

ComponentDescription
TableRoot container with border, rounded corners, and background
TableHeaderHeader section with distinct background and font weight
TableBodyBody section wrapping data rows
TableFooterFooter section with top border
TableRowA flex row with bottom border
TableHeadHeader cell with alignment and width options
TableCellData cell with alignment and width options
TableCaptionCaption text below the table

#Table vs DataTable

FeatureTableDataTable
Virtual scrollingNoYes
Column sortingNoYes
Column resizingNoYes
Column movingNoYes
Cell selectionNoYes
Row selectionNoYes
Infinite loadingNoYes
Keyboard navigationNoYes
State managementStatelessTableState
Best forSmall, static dataLarge, interactive datasets

#API Reference

#Table

  • new() - Create a new table
  • Implements Styled, ParentElement, Sizable, RenderOnce

#TableHead / TableCell

  • new() - Create a new head/cell
  • w(width) - Set fixed width (otherwise flex-1)
  • text_center() - Center-align content
  • text_right() - Right-align content
  • Implements Styled, ParentElement, RenderOnce

#TableHeader / TableBody / TableFooter / TableRow / TableCaption

  • new() - Create a new instance
  • Implements Styled, ParentElement, RenderOnce

#Table

A static table. Mirrors GPUI Component's `Table`: a bordered, rounded surface that stacks a `TableHeader`, a `TableBody`, an optional `TableFooter` and a `TableCaption`. Rows are `TableRow`s of `TableHead` or `TableCell`; cells share the row width equally (100px minimum) unless given an explicit `width`, and align their content with `align`. Set `index` on body rows so they draw the row separator, and `striped` for alternating backgrounds. `bordered: false` removes the outer border and radius.

NameKindTypeDefault
borderedinbooltrue

#TableHeader

The header section of a `Table`: a muted band with a bottom separator.

#TableBody

The body section of a `Table`, wrapping data rows.

#TableFooter

The footer section of a `Table`: a muted band with a top separator.

#TableRow

A row of a `Table`. Rows after the first (`index > 0`) draw a top separator; with `striped`, odd rows get the even-row background.

NameKindTypeDefault
indexinint
stripedinbool

#TableHead

A header cell. Shows `text` in the muted header style, or custom children.

NameKindTypeDefault
textinstring
aligninTextHorizontalAlignmentTextHorizontalAlignment.left
sizeinControlSizeControlSize.medium
col-spaninint1

#TableCell

A data cell. Shows `text`, or custom children (badges, icons, buttons).

NameKindTypeDefault
textinstring
aligninTextHorizontalAlignmentTextHorizontalAlignment.left
sizeinControlSizeControlSize.medium
col-spaninint1
text-colorincolorTheme.foreground
monoinbool

#TableCaption

A caption below the table body, centered in the muted style.

NameKindTypeDefault
textinstring
sizeinControlSizeControlSize.medium