A small introduction to Slint

Slint has its own language for describing interfaces. Here's a small introduction to how it works with Rust.
I've been working with Slint in my desktop app, Flinch, and I wanted to write a small introduction to what it is and what its code looks like.
I'll keep the example small. There are plenty of things involved in a complete application, but a counter is enough to show the basic pieces without getting lost in the process.
What is Slint?
Slint is a toolkit for building graphical interfaces. It has its own declarative UI language, with application logic written in a supported language such as Rust, C++, JavaScript, or Python. It can be used for desktop applications and embedded devices, among other targets.
Declarative means the code describes the interface and the relationships between its values. Instead of writing each step needed to update a label, a binding describes what that label should display.
I'll use Rust for the application code here. The interface itself is written in Slint.
A window with some text
This is a complete UI declaration:
The component is called App, and it inherits from Window. Inside it is a Text element. Its properties set the content and alignment.
The nested structure might look familiar from other UI tools, but this is Slint's own language. The declaration can live in a .slint file, or inside a Rust macro, which I'll use a little later.
Adding a button
A label on its own doesn't show much about interaction. Let's give it something to display:
The import brings in two standard widgets. VerticalBox arranges its children vertically, and Button provides the clickable control.
The count property starts at zero. When the button is clicked, its handler adds one to that value. root refers to the root element of the component, so root.count accesses the counter declared on App.
The interesting part is the label:
The expression inside \{...} inserts the property's value into the string. Slint tracks that dependency, so changing count also updates the text. There isn't another instruction telling the label to refresh.
The property binding examples show this with other controls too, including a slider whose value is displayed in a label.
Calling Rust from the interface
For this counter, the behavior can stay entirely in Slint. A real button might need to save settings, load a file, or start some application operation. That's where a callback connects the UI to Rust.
Here's the same counter with its increment handled in Rust. Start with a new Cargo project and add Slint. This example uses version 1.18.1:
Replace the contents of src/main.rs with:
Running cargo run builds the application and opens the window.
The label uses "Count: " + root.count here. It still updates when the count changes, but avoids \{...} interpolation, which isn't supported inside the Rust macro. Rust reads those strings first and rejects the escape sequence.
The in-out declaration exposes the property for reading and writing from application code. Slint generates get_count() and set_count() for it, and on_increment() lets Rust supply the callback's behavior.
The callback captures a weak handle to the component. Capturing a strong handle here would create a reference cycle: the component owns the handler, which would keep the component alive. Upgrading the weak handle gives access while the component still exists.
The question marks propagate errors from creating or running the interface. This Rust version also uses saturating_add so the counter stops at the integer limit instead of overflowing.
These methods are covered in the Rust integration documentation. For a larger interface, the UI can move into separate .slint files compiled through a build script. Putting everything in one file just keeps this example easier to follow.
Trying it out: SlintPad
The Slint-only counter can also be tried in SlintPad, without setting up the Rust project. Paste the example from "Adding a button" into the editor (or simply click here), then change a few properties and see what happens.
There's plenty more to Slint, but I'll leave it here for this introduction. The documentation linked above goes further if this small example has made you curious.
Thanks for reading, see you next time!