Tag Input for Slint
KitTagInput · Slint Kit 0.3.0
Input for creating tags with a controlled chip list.

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.
Type Research and press Enter to add a tag. Remove a chip with its close button; Rust retains stable TagId values.
ui/app.slint
export { KitFormValidation, HtmlInputKind, NumberInputFormat, KitInputConstraints, KitValidationResult, KitValidityState } from "@kit/kit.slint";
// SPDX-License-Identifier: GPL-3.0-or-later
import { KitTagInput, ChipRow, KitTheme, KitThemes } from "@kit/kit.slint";
export component App inherits Window {
title: "KitTagInput example";
preferred-width: 520px;
preferred-height: 300px;
max-width: 100000px;
max-height: 100000px;
background: root.theme.canvas;
in-out property <KitTheme> theme: KitThemes.light;
in property <[ChipRow]> chips;
in-out property <string> draft;
in property <string> error;
callback create(string);
callback remove(int);
VerticalLayout {
padding: 24px;
spacing: 16px;
alignment: start;
control := KitTagInput {
theme: root.theme; label: "Project tags"; chips: root.chips; text <=> root.draft; error: root.error;
create-requested(text) => { root.create(text); }
remove-requested(token) => { root.remove(token); }
}
result := Text { text: root.chips.length + (root.chips.length == 1 ? " tag" : " tags"); color: root.theme.text-primary; }
}
}
src/main.rs
// SPDX-License-Identifier: GPL-3.0-or-later
use slint::{ComponentHandle, ModelRc, VecModel};
use std::{cell::{Cell, RefCell}, rc::Rc};
mod ui { slint::include_modules!(); }
#[derive(Clone, Copy, PartialEq, Eq)]
struct TagId(i32);
#[derive(Clone)]
struct Tag { id: TagId, label: String }
fn publish(app: &ui::App, tags: &[Tag]) {
app.set_chips(ModelRc::new(VecModel::from(tags.iter().map(|tag| ui::ChipRow { token: tag.id.0, label: tag.label.as_str().into(), removable: true }).collect::<Vec<_>>())));
}
fn main() -> Result<(), slint::PlatformError> {
let app = ui::App::new()?;
slint_kit::init_form_validation!(ui, app);
let tags = Rc::new(RefCell::new(vec![Tag { id: TagId(1), label: "Design".into() }]));
let next = Cell::new(2_i32);
publish(&app, &tags.borrow());
let weak = app.as_weak();
let state = tags.clone();
app.on_create(move |draft| { if let Some(app) = weak.upgrade() {
let label = draft.trim();
if label.is_empty() || label.chars().count() > 40 { app.set_error("Use 1 to 40 characters".into()); return; }
if state.borrow().iter().any(|tag| tag.label == label) { app.set_error("Tag already exists".into()); return; }
let token = next.get();
next.set(token.checked_add(1).expect("tag identity capacity"));
state.borrow_mut().push(Tag { id: TagId(token), label: label.into() });
let snapshot = state.borrow().clone();
publish(&app, &snapshot); app.set_draft("".into()); app.set_error("".into());
} });
let weak = app.as_weak();
app.on_remove(move |token| { if let Some(app) = weak.upgrade() {
tags.borrow_mut().retain(|tag| tag.id != TagId(token));
let snapshot = tags.borrow().clone(); publish(&app, &snapshot);
} });
app.run()
}
Cargo.toml and build.rs
Cargo.toml
[package]
name = "slint-kit-tag-input-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
Bind text, supply chips and handle create-requested(string) to validate and create a tag. Handle remove-requested(token) to remove an existing item.
Enter passes the draft to the controller without adding a chip locally. Errors remain visible near the editor and actions respect FieldAccess.
Choose between built-in options and custom composition
Shared options
label— built-in property; custom composition exampleerror— 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 { KitTagInput, KitTheme, KitThemes } from "@kit/kit.slint";
export component Example inherits Window {
title: "KitTagInput example";
preferred-width: 420px;
preferred-height: 220px;
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: "Tag Input"; color: root.theme.text-primary; font-size: 20px * root.theme.text-scale; font-weight: 600; }
KitTagInput { theme: root.theme; label: "Project tags"; chips: [{ token: 11, label: "Design", removable: true }, { token: 23, label: "Research", removable: true }]; text: ""; create-requested(value) => { root.status = "Create tag: " + value; } }
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 |
|---|---|---|
required |
bool |
in |
required-message |
string |
in |
custom-validity |
string |
in-out |
validity |
KitValidityState |
out |
will-validate |
bool |
out |
validation-message |
string |
out |
theme |
KitTheme |
in |
control-size |
ControlSize |
in |
access |
FieldAccess |
in |
chips |
[ChipRow] |
in |
label |
string |
in |
error |
string |
in |
text |
string |
in-out |
Types and variants
| Type | Declaration |
|---|---|
struct ChipRow |
token: int, label: string, removable: bool |
enum ControlSize |
compact, medium, comfortable |
enum FieldAccess |
editable, read-only, disabled |
struct KitValidityState |
valid: bool, value-missing: bool, type-mismatch: bool, pattern-mismatch: bool, too-long: bool, too-short: bool, range-underflow: bool, range-overflow: bool, step-mismatch: bool, bad-input: bool, custom-error: bool, |
Callbacks
invalid()edited(string)create-requested(string)remove-requested(int)
Functions
check-validity() → boolreport-validity() → boolset-custom-validity(message: string)reset-validation()take-focus()