Data table for Slint
KitDataTable · Slint Kit 0.3.0
Slint data table with virtualized rows, sortable headers and row selection.

Try the component
Download the complete project, extract it and run cargo run --locked in its folder. Rust 1.92+; Slint Kit 0.3.0 from crates.io. You can also copy the four files below.
Click Name to sort, then select Ada. Sort again: Rust keeps Ada selected by key, even when the row moves.
ui/app.slint
export { KitFormValidation, HtmlInputKind, NumberInputFormat, KitInputConstraints, KitValidationResult, KitValidityState } from "@kit/kit.slint";
// SPDX-License-Identifier: GPL-3.0-or-later
import { KitDataTable, KitThemes, TableRow, TableColumn, DataSortDirection, DataAlignment, NavigationStep } from "@kit/kit.slint";
export { TableRow, TableColumn, DataSortDirection, DataAlignment, NavigationStep }
export component App inherits Window {
title: "Slint data table";
preferred-width: 520px; preferred-height: 320px;
max-width: 100000px; max-height: 100000px;
background: KitThemes.light.canvas;
in property <[TableRow]> rows;
in property <[TableColumn]> columns;
in property <int> generation;
in property <int> active-index: -1;
in property <string> selected-label: "No selection";
callback sort(int);
callback choose(int, int);
callback move(NavigationStep);
VerticalLayout {
padding: 24px; spacing: 16px;
table := KitDataTable {
height: 220px; rows: root.rows; columns: root.columns;
generation: root.generation; active-index: root.active-index;
sort-requested(token) => { root.sort(token); }
activate(token, generation) => { root.choose(token, generation); }
navigate(step) => { root.move(step); }
}
Text { text: root.selected-label; color: KitThemes.light.text-primary; }
}
}
src/main.rs
// SPDX-License-Identifier: GPL-3.0-or-later
use slint::{ComponentHandle, ModelRc, VecModel};
use std::{cell::RefCell, rc::Rc};
mod ui {
slint::include_modules!();
}
fn next_index(current: Option<usize>, length: usize, step: ui::NavigationStep) -> usize {
use ui::NavigationStep::*;
let current = current.unwrap_or(0);
match step {
First => 0,
Last => length.saturating_sub(1),
Previous | PagePrevious => current.saturating_sub(1),
Next | PageNext => (current + 1).min(length.saturating_sub(1)),
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Person {
Zoe,
Ada,
Lina,
}
impl Person {
fn token(self) -> i32 {
match self {
Self::Zoe => 11,
Self::Ada => 23,
Self::Lina => 37,
}
}
fn label(self) -> &'static str {
match self {
Self::Zoe => "Zoe",
Self::Ada => "Ada",
Self::Lina => "Lina",
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
struct Generation(i32);
struct State {
order: Vec<Person>,
selected: Option<Person>,
active: Option<Person>,
direction: ui::DataSortDirection,
generation: Generation,
}
fn publish(state: &RefCell<State>, app: &ui::App) {
let state = state.borrow();
let rows = state
.order
.iter()
.map(|person| ui::TableRow {
token: person.token(),
cells: ModelRc::new(VecModel::from(vec![person.label().into()])),
selected: state.selected == Some(*person),
disabled: false,
})
.collect::<Vec<_>>();
let columns = vec![ui::TableColumn {
token: 1,
label: "Name".into(),
sortable: true,
sort: state.direction,
alignment: ui::DataAlignment::Leading,
}];
let active = state
.order
.iter()
.position(|p| Some(*p) == state.active)
.map_or(-1, |i| i as i32);
let label = state.selected.map_or("No selection".to_owned(), |p| {
format!("Selected: {}", p.label())
});
let generation = state.generation.0;
drop(state);
app.set_generation(generation);
app.set_active_index(active);
app.set_rows(ModelRc::new(VecModel::from(rows)));
app.set_columns(ModelRc::new(VecModel::from(columns)));
app.set_selected_label(label.into());
}
fn main() -> Result<(), slint::PlatformError> {
let app = ui::App::new()?;
slint_kit::init_form_validation!(ui, app);
let state = Rc::new(RefCell::new(State {
order: vec![Person::Zoe, Person::Ada, Person::Lina],
selected: None,
active: None,
direction: ui::DataSortDirection::Unsorted,
generation: Generation(1),
}));
publish(&state, &app);
let (model, weak) = (state.clone(), app.as_weak());
app.on_sort(move |column| {
if column != 1 {
return;
}
{
let mut state = model.borrow_mut();
let Some(next) = state.generation.0.checked_add(1) else {
return;
};
state.generation = Generation(next);
state.direction = if state.direction == ui::DataSortDirection::Ascending {
ui::DataSortDirection::Descending
} else {
ui::DataSortDirection::Ascending
};
state.order.sort_by_key(|p| p.label());
if state.direction == ui::DataSortDirection::Descending {
state.order.reverse();
}
}
if let Some(app) = weak.upgrade() {
publish(&model, &app);
}
});
let (model, weak) = (state.clone(), app.as_weak());
app.on_choose(move |token, generation| {
{
let mut state = model.borrow_mut();
if generation != state.generation.0 {
return;
}
let Some(person) = state.order.iter().copied().find(|p| p.token() == token) else {
return;
};
state.selected = Some(person);
state.active = Some(person);
}
if let Some(app) = weak.upgrade() {
publish(&model, &app);
}
});
let weak = app.as_weak();
app.on_move(move |step| {
{
let mut state = state.borrow_mut();
let current = state.order.iter().position(|p| Some(*p) == state.active);
let index = next_index(current, state.order.len(), step);
state.active = Some(state.order[index]);
}
if let Some(app) = weak.upgrade() {
publish(&state, &app);
}
});
app.run()
}
Cargo.toml and build.rs
Cargo.toml
[package]
name = "slint-kit-data-table-example"
version = "0.1.0"
edition = "2024"
rust-version = "1.92"
license = "GPL-3.0-or-later"
publish = false
[dependencies]
slint-kit = "=0.3.0"
slint = { version = "=1.18.1", default-features = false, features = ["std", "compat-1-18", "backend-winit", "renderer-skia", "accessibility"] }
[build-dependencies]
slint-kit-build = "=0.3.0"
[workspace]
build.rs
fn main() {
slint_kit_build::compile("ui/app.slint").expect("compile component example");
}
Files: Cargo.toml · build.rs · ui/app.slint · src/main.rs
Give the table a bounded height, then supply columns, rows, generation and active-index. Handle sort-requested(column-token) in Rust by sorting your model and updating the column direction. Handle navigate to update the active index and activate(row-token, generation) to act on a row.
The table renders the supplied row order; clicking a header does not sort it automatically. Changing active-index reveals that row. Set empty-label for an empty model and interactive: false while a modal blocks the table.
To connect sorting, keyboard input and selection, use the complete sortable table application.
Choose between built-in options and custom composition
Shared options
label— built-in property; custom composition example
Visual composition
The file below produces the screenshot. For controller-driven components, it shows the displayed state without supplying the Rust wiring. Create a project.
Show the screenshot source

// SPDX-License-Identifier: GPL-3.0-or-later
import { DataAlignment, DataSortDirection, KitDataTable, KitTheme, KitThemes } from "@kit/kit.slint";
export component Example inherits Window {
title: "KitDataTable example";
preferred-width: 620px;
preferred-height: 400px;
max-width: 100000px;
max-height: 100000px;
background: root.theme.canvas;
in-out property <KitTheme> theme: KitThemes.light;
in-out property <string> status;
VerticalLayout {
padding: 24px; spacing: 16px; alignment: start;
Text { text: "Data Table"; color: root.theme.text-primary; font-size: 20px * root.theme.text-scale; font-weight: 600; }
KitDataTable { theme: root.theme; height: 250px; generation: 1; columns: [{ token: 11, label: "Project", sortable: true, sort: DataSortDirection.ascending, alignment: DataAlignment.leading }, { token: 23, label: "Status", sortable: true, sort: DataSortDirection.unsorted, alignment: DataAlignment.leading }, { token: 38, label: "Owner", sortable: false, sort: DataSortDirection.unsorted, alignment: DataAlignment.leading }]; rows: [{ token: 101, cells: ["Field notes", "In progress", "Alex"], selected: true, disabled: false }, { token: 205, cells: ["Design system", "Review", "Jamie"], selected: false, disabled: false }, { token: 309, cells: ["Launch plan", "Draft", "Taylor"], selected: false, disabled: false }]; activate(token, generation) => { root.status = "Project token " + token; } }
if root.status != "": Text { text: root.status; color: root.theme.text-secondary; font-size: 12px * root.theme.text-scale; wrap: word-wrap; }
}
}
Public API
Inherits Rectangle.
Properties
| Name | Type | Direction |
|---|---|---|
interactive |
bool |
in |
theme |
KitTheme |
in |
control-size |
ControlSize |
in |
columns |
[TableColumn] |
in |
rows |
[TableRow] |
in |
generation |
int |
in |
active-index |
int |
in |
label |
string |
in |
empty-label |
string |
in |
minimum-column-width |
length |
in |
scroll-y |
length |
out |
Types and variants
| Type | Declaration |
|---|---|
enum ControlSize |
compact, medium, comfortable |
enum DataAlignment |
leading, center, trailing |
enum DataSortDirection |
unsorted, ascending, descending |
enum NavigationStep |
next, previous, first, last, page-next, page-previous |
struct TableColumn |
token: int, label: string, sortable: bool, sort: DataSortDirection, alignment: DataAlignment |
struct TableRow |
token: int, cells: [string], selected: bool, disabled: bool |
Callbacks
activate(int,int)navigate(NavigationStep)sort-requested(int)
Functions
take-focus()reveal-active()