Content list for Slint
KitContentList · Slint Kit 0.3.1
Virtualized Slint list with titles, excerpts and item 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.
Select an article with the pointer, or move with the arrow keys and press Enter. Rust displays the accepted article title.
ui/app.slint
export { KitFormValidation, HtmlInputKind, NumberInputFormat, KitInputConstraints, KitValidationResult, KitValidityState } from "@kit/kit.slint";
// SPDX-License-Identifier: GPL-3.0-or-later
import { KitContentList, KitThemes, ContentItem, NavigationStep } from "@kit/kit.slint";
export { ContentItem, NavigationStep }
export component App inherits Window {
title: "Slint content list";
preferred-width: 580px; preferred-height: 390px;
max-width: 100000px; max-height: 100000px;
background: KitThemes.light.canvas;
in property <[ContentItem]> items;
in property <int> active-index: -1;
in property <string> selected-label: "Choose an article";
callback choose(int, int);
callback move(NavigationStep);
VerticalLayout {
padding: 24px; spacing: 16px;
list := KitContentList { height: 280px; items: root.items; generation: 1;
active-index: root.active-index;
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 Article {
Setup,
State,
}
impl Article {
fn token(self) -> i32 {
match self {
Self::Setup => 11,
Self::State => 23,
}
}
fn title(self) -> &'static str {
match self {
Self::Setup => "Install Slint Kit",
Self::State => "Keep selection stable",
}
}
fn excerpt(self) -> &'static str {
match self {
Self::Setup => "Import a component and connect its Rust callback.",
Self::State => "Use an application key when the display order changes.",
}
}
}
const ARTICLES: [Article; 2] = [Article::Setup, Article::State];
#[derive(Default)]
struct State {
selected: Option<Article>,
active: Option<Article>,
}
fn publish(state: &RefCell<State>, app: &ui::App) {
let state = state.borrow();
let items = ARTICLES
.iter()
.map(|article| ui::ContentItem {
token: article.token(),
title: article.title().into(),
excerpt: article.excerpt().into(),
metadata: "Guide".into(),
link_label: "Select article".into(),
thumbnail: slint::Image::default(),
selected: state.selected == Some(*article),
disabled: false,
})
.collect::<Vec<_>>();
let index = ARTICLES
.iter()
.position(|a| Some(*a) == state.active)
.map_or(-1, |i| i as i32);
let label = state.selected.map_or("Choose an article".to_owned(), |a| {
format!("Selected: {}", a.title())
});
drop(state);
app.set_items(ModelRc::new(VecModel::from(items)));
app.set_active_index(index);
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::default()));
publish(&state, &app);
let (model, weak) = (state.clone(), app.as_weak());
app.on_choose(move |token, generation| {
if generation != 1 {
return;
}
let Some(article) = ARTICLES.iter().copied().find(|a| a.token() == token) else {
return;
};
{
let mut state = model.borrow_mut();
state.selected = Some(article);
state.active = Some(article);
}
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 = ARTICLES.iter().position(|a| Some(*a) == state.active);
state.active = Some(ARTICLES[next_index(current, ARTICLES.len(), step)]);
}
if let Some(app) = weak.upgrade() {
publish(&state, &app);
}
});
app.run()
}
Cargo.toml and build.rs
Cargo.toml
[package]
name = "slint-kit-content-list-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
Supply items, generation and active-index. Handle activate(token, generation) in Rust using the application key; handle navigate to move focus. Give the list a bounded height.
Each ContentItem supplies its title, description, metadata and optional thumbnail. Rows render within the viewport. Set empty-label for an empty model.
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 { KitContentList, KitTheme, KitThemes } from "@kit/kit.slint";
export component Example inherits Window {
title: "KitContentList example";
preferred-width: 620px;
preferred-height: 410px;
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: "Content List"; color: root.theme.text-primary; font-size: 20px * root.theme.text-scale; font-weight: 600; }
KitContentList { theme: root.theme; height: 260px; generation: 1; items: [{ token: 101, title: "A guide to native design", excerpt: "Build interfaces that feel at home on the desktop.", metadata: "Design · 5 min read", link-label: "Read article", selected: false, disabled: false }, { token: 205, title: "Keep domain values exact", excerpt: "Use validated values and stable application identity.", metadata: "Engineering · 8 min read", link-label: "Read article", selected: false, disabled: false }]; }
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 |
|---|---|---|
theme |
KitTheme |
in |
items |
[ContentItem] |
in |
generation |
int |
in |
active-index |
int |
in |
label |
string |
in |
empty-label |
string |
in |
scroll-y |
length |
out |
Types and variants
| Type | Declaration |
|---|---|
struct ContentItem |
token: int, title: string, excerpt: string, metadata: string, link-label: string, thumbnail: image, selected: bool, disabled: bool |
enum NavigationStep |
next, previous, first, last, page-next, page-previous |
Callbacks
activate(int,int)navigate(NavigationStep)
Functions
take-focus()