Custom Controls
Custom Control Introduction
Section titled “Custom Control Introduction”A Clickable Button
Section titled “A Clickable Button”import { VerticalBox, Button } from "std-widgets.slint";export component Recipe inherits Window { in-out property <int> counter: 0; VerticalBox { button := Button { text: "Button, pressed " + root.counter + " times"; clicked => { root.counter += 1; } } }}In this first example, you see the basics of the Slint language:
- We import the
VerticalBoxlayout and theButtonwidget from the standard library using theimportstatement. This statement can import widgets or your own components declared in different files. You don’t need to import built-in element such asWindoworRectangle. - We declare the
Recipecomponent using thecomponentkeyword.Recipeinherits fromWindowand has elements: A layout (VerticalBox) with one button. - You instantiate elements using their name followed by a pair of braces (with optional contents.
You can assign a name to a specific element using
:= - Elements have properties. Use
:to set property values. Here we assign a binding that computes a string by concatenating some string literals, and thecounterproperty to theButton’stextproperty. - You can declare custom properties for any element with
property <...>. A property needs to have a type, and can have a default value and an access specifier. Access specifiers likeprivate,in,outorin-outdefines how outside elements can interact with the property.Privateis the default value and stops any outside element from accessing the property. Thecounterproperty is custom in this example. - Elements can also have callback. In this case we assign a callback
handler to the
clickedcallback of thebuttonwith=> { ... }. - Property bindings are automatically re-evaluated if any of the properties the
binding depends on changes. The
textbinding of the button is automatically re-computed whenever thecounterchanges.
React to a Button Click in Native Code
Section titled “React to a Button Click in Native Code”This example increments the counter using native code:
import { VerticalBox, Button } from "std-widgets.slint";export component Recipe inherits Window { in-out property <int> counter: 0; callback button-pressed <=> button.clicked; VerticalBox { button := Button { text: "Button, pressed " + root.counter + " times"; } }}The <=> syntax binds two callbacks together. Here the new button-pressed
callback binds to button.clicked.
The root element of the main component exposes all non-private properties and
callbacks to native code.
In Slint, - and _ are equivalent and interchangeable in all identifiers.
This is different in native code: Most programming languages forbid - in
identifiers, so - is replaced with _.
For technical reasons, this example uses export {Recipe} in the slint! macro.
In real code, you can put the whole Slint code in the slint! macro, or use
an external .slint file together with a build script.
slint::slint!(export { Recipe } from "docs/reference/src/recipes/button_native.slint";);
fn main() { let recipe = Recipe::new().unwrap(); let recipe_weak = recipe.as_weak(); recipe.on_button_pressed(move || { let recipe = recipe_weak.upgrade().unwrap(); let mut value = recipe.get_counter(); value = value + 1; recipe.set_counter(value); }); recipe.run().unwrap();}The Slint compiler generates a struct Recipe with a getter (get_counter) and
a setter (set_counter) for each accessible property of the root element of the
Recipe component. It also generates a function for each accessible callback,
like in this case on_button_pressed.
The Recipe struct implements the slint::ComponentHandle trait. A component
manages a strong and a weak reference count, similar to an Rc.
We call the as_weak function to get a weak handle to the component, which we
can move into the callback.
We can’t use a strong handle here, because that would form a cycle: The component handle has ownership of the callback, which itself has ownership of the closure’s captured variables.
In C++ you can write
#include "button_native.h"
int main(int argc, char **argv){ auto recipe = Recipe::create(); recipe->on_button_pressed([&]() { auto value = recipe->get_counter(); value += 1; recipe->set_counter(value); }); recipe->run();}The CMake integration handles the Slint compiler invocations as needed,
which will parse the .slint file and generate the button_native.h header.
This header file contains a Recipe class with a getter and setter for each
accessible property, as well as a function to set up a callback
for each accessible callback in Recipe. In this case we will have get_counter,
set_counter to access the counter property and on_button_pressed to
set up the callback.
In Python, you can write:
import slint
class App(slint.loader.recipe.Recipe): @slint.callback def button_pressed(self): value = self.counter value = value + 1 self.counter = value
app = App()app.run()The Slint auto-loader provides a Recipe class originating from recipe.slint, which is subclassed.
The Recipe class provides the counter property, and the @slint.callback decorator connects the
button_pressed method with the button-pressed callback.
Use Property Bindings to Synchronize Controls
Section titled “Use Property Bindings to Synchronize Controls”import { VerticalBox, Slider } from "std-widgets.slint";export component Recipe inherits Window { VerticalBox { slider := Slider { maximum: 100; } Text { text: "Value: \{round(slider.value)}"; } }}This example introduces the Slider widget.
It also introduces interpolation in string literals: Use \{...} to render
the result of code between the curly braces as a string.
Animation Examples
Section titled “Animation Examples”Animate the Position of an Element
Section titled “Animate the Position of an Element”import { CheckBox } from "std-widgets.slint";export component Recipe inherits Window { width: 200px; height: 100px;
rect := Rectangle { x:0; y: 5px; width: 40px; height: 40px; background: blue; animate x { duration: 500ms; easing: ease-in-out; } }
CheckBox { y: 25px; text: "Align rect to the right"; toggled => { if (self.checked) { rect.x = parent.width - rect.width; } else { rect.x = 0px; } } }}Layouts position elements automatically. In this example we manually position
elements instead, using the x, y, width, height properties.
Notice the animate x block that specifies an animation. It’s run whenever the
property changes: Either because a callback sets the property, or because
its binding value changes.
Animation Sequence
Section titled “Animation Sequence”import { CheckBox } from "std-widgets.slint";export component Recipe inherits Window { width: 200px; height: 100px;
rect := Rectangle { x:0; y: 5px; width: 40px; height: 40px; background: blue; animate x { duration: 500ms; easing: ease-in-out; } animate y { duration: 250ms; delay: 500ms; easing: ease-in; } }
CheckBox { y: 25px; text: "Align rect bottom right"; toggled => { if (self.checked) { rect.x = parent.width - rect.width; rect.y = parent.height - rect.height; } else { rect.x = 0px; rect.y = 0px; } } }}This example uses the delay property to make one animation run after another.
States Examples
Section titled “States Examples”Associate Property Values With States
Section titled “Associate Property Values With States”import { HorizontalBox, VerticalBox, Button } from "std-widgets.slint";
component Circle inherits Rectangle { width: 30px; height: 30px; border-radius: root.width / 2; animate x { duration: 250ms; easing: ease-in; } animate y { duration: 250ms; easing: ease-in-out; } animate background { duration: 250ms; }}
export component Recipe inherits Window { states [ left-aligned when b1.pressed: { circle1.x: 0px; circle1.y: 40px; circle1.background: green; circle2.x: 0px; circle2.y: 0px; circle2.background: blue; } right-aligned when b2.pressed: { circle1.x: 170px; circle1.y: 70px; circle1.background: green; circle2.x: 170px; circle2.y: 00px; circle2.background: blue; } ]
VerticalBox { HorizontalBox { max-height: self.min-height; b1 := Button { text: "State 1"; } b2 := Button { text: "State 2"; } } Rectangle { background: root.background.darker(20%); width: 200px; height: 100px;
circle1 := Circle { y:0; background: green; x: 85px; } circle2 := Circle { background: green; x: 85px; y: 40px; } } }}Transitions
Section titled “Transitions”import { HorizontalBox, VerticalBox, Button } from "std-widgets.slint";
component Circle inherits Rectangle { width: 30px; height: 30px; border-radius: root.width / 2;}
export component Recipe inherits Window { states [ left-aligned when b1.pressed: { circle1.x: 0px; circle1.y: 40px; circle2.x: 0px; circle2.y: 0px; in { animate circle1.x, circle2.x { duration: 250ms; } } out { animate circle1.x, circle2.x { duration: 500ms; } } } right-aligned when !b1.pressed: { circle1.x: 170px; circle1.y: 70px; circle2.x: 170px; circle2.y: 00px; } ]
VerticalBox { HorizontalBox { max-height: self.min-height; b1 := Button { text: "Press and hold to change state"; } } Rectangle { background: root.background.darker(20%); width: 250px; height: 100px;
circle1 := Circle { y:0; background: green; x: 85px; } circle2 := Circle { background: blue; x: 85px; y: 40px; } } }}Layout Examples
Section titled “Layout Examples”Vertical
Section titled “Vertical”import { VerticalBox, Button } from "std-widgets.slint";export component Recipe inherits Window { VerticalBox { Button { text: "First"; } Button { text: "Second"; } Button { text: "Third"; } }}Horizontal
Section titled “Horizontal”import { HorizontalBox, Button } from "std-widgets.slint";export component Recipe inherits Window { HorizontalBox { Button { text: "First"; } Button { text: "Second"; } Button { text: "Third"; } }}import { GridBox, Button, Slider } from "std-widgets.slint";export component Recipe inherits Window { GridBox { Row { Button { text: "First"; } Button { text: "Second"; } } Row { Button { text: "Third"; } Button { text: "Fourth"; } } Row { Slider { colspan: 2; } } }}Global Callbacks
Section titled “Global Callbacks”Invoke a Globally Registered Native Callback from Slint
Section titled “Invoke a Globally Registered Native Callback from Slint”This example uses a global singleton to implement common logic in native code. This singleton may also store properties that are accessible to native code.
Note: The preview visualize the Slint code only. It’s not connected to the native code.
import { HorizontalBox, VerticalBox, LineEdit } from "std-widgets.slint";
export global Logic { pure callback to-upper-case(string) -> string; // You can collect other global properties here}
export component Recipe inherits Window { VerticalBox { input := LineEdit { text: "Text to be transformed"; } HorizontalBox { Text { text: "Transformed:"; } // Callback invoked in binding expression Text { text: { Logic.to-upper-case(input.text); } } } }}In Rust you can set the callback like this:
fn main() { let recipe = Recipe::new().unwrap(); recipe.global::<Logic>().on_to_upper_case(|string| { string.as_str().to_uppercase().into() }); // ...}int main(int argc, char **argv){ auto recipe = Recipe::create(); recipe->global<Logic>().on_to_upper_case([](slint::SharedString str) -> slint::SharedString { std::string arg(str); std::transform(arg.begin(), arg.end(), arg.begin(), toupper); return slint::SharedString(arg); }); // ...}In JavaScript you can set the callback like this:
let slint = require("slint-ui");let file = slint.loadFile("recipe.slint");let recipe = new file.Recipe();recipe.Logic.to_upper_case = (str) => { return str.toUpperCase();};// ...In Python, the callback is associated with the global_name parameter of the @slint.callback decorator:
import slint
class App(slint.loader.recipe.Recipe): @slint.callback(global_name="Logic") def to_upper_case(&self, value: str) -> str: return value.upper()
# ...Custom Widgets
Section titled “Custom Widgets”Custom Button
Section titled “Custom Button”component Button inherits Rectangle { in-out property text <=> txt.text; callback clicked <=> touch.clicked; border-radius: root.height / 2; border-width: 1px; border-color: root.background.darker(25%); background: touch.pressed ? #6b8282 : touch.has-hover ? #6c616c : #456; height: txt.preferred-height * 1.33; min-width: txt.preferred-width + 20px; txt := Text { x: (parent.width - self.width)/2 + (touch.pressed ? 2px : 0); y: (parent.height - self.height)/2 + (touch.pressed ? 1px : 0); color: touch.pressed ? #fff : #eee; } touch := TouchArea { }}
export component Recipe inherits Window { VerticalLayout { alignment: start; Button { text: "Button"; } }}ToggleSwitch
Section titled “ToggleSwitch”export component ToggleSwitch inherits Rectangle { callback toggled; in-out property <string> text; in-out property <bool> checked; in-out property<bool> enabled <=> touch-area.enabled; height: 20px; horizontal-stretch: 0; vertical-stretch: 0;
HorizontalLayout { spacing: 8px; indicator := Rectangle { width: 40px; border-width: 1px; border-radius: root.height / 2; border-color: self.background.darker(25%); background: root.enabled ? (root.checked ? blue: white) : white; animate background { duration: 100ms; }
bubble := Rectangle { width: root.height - 8px; height: bubble.width; border-radius: bubble.height / 2; y: 4px; x: 4px + self.a * (indicator.width - bubble.width - 8px); property <float> a: root.checked ? 1 : 0; background: root.checked ? white : (root.enabled ? blue : gray); animate a, background { duration: 200ms; easing: ease;} } }
Text { min-width: max(100px, self.preferred-width); text: root.text; vertical-alignment: center; color: root.enabled ? black : gray; }
}
touch-area := TouchArea { width: root.width; height: root.height; clicked => { if (root.enabled) { root.checked = !root.checked; root.toggled(); } } }}
export component Recipe inherits Window { VerticalLayout { alignment: start; ToggleSwitch { text: "Toggle me"; } ToggleSwitch { text: "Disabled"; enabled: false; } }}CustomSlider
Section titled “CustomSlider”The TouchArea is covering the entire widget, so you can drag this slider from
any point within itself.
import { VerticalBox } from "std-widgets.slint";
export component MySlider inherits Rectangle { in-out property<float> maximum: 100; in-out property<float> minimum: 0; in-out property<float> value;
min-height: 24px; min-width: 100px; horizontal-stretch: 1; vertical-stretch: 0;
border-radius: root.height/2; background: touch.pressed ? #eee: #ddd; border-width: 1px; border-color: root.background.darker(25%);
handle := Rectangle { width: self.height; height: parent.height; border-width: 3px; border-radius: self.height / 2; background: touch.pressed ? #f8f: touch.has-hover ? #66f : #0000ff; border-color: self.background.darker(15%); x: (root.width - handle.width) * (root.value - root.minimum)/(root.maximum - root.minimum); } touch := TouchArea { property <float> pressed-value; pointer-event(event) => { if (event.button == PointerEventButton.left && event.kind == PointerEventKind.down) { self.pressed-value = root.value; } } moved => { if (self.enabled && self.pressed) { root.value = max(root.minimum, min(root.maximum, self.pressed-value + (touch.mouse-x - touch.pressed-x) * (root.maximum - root.minimum) / (root.width - handle.width)));
} } }}
export component Recipe inherits Window { VerticalBox { alignment: start; slider := MySlider { maximum: 100; } Text { text: "Value: \{round(slider.value)}"; } }}This example show another implementation that has a drag-able handle: The handle only moves when we click on that handle. The TouchArea is within the handle and moves with the handle.
import { VerticalBox } from "std-widgets.slint";
export component MySlider inherits Rectangle { in-out property<float> maximum: 100; in-out property<float> minimum: 0; in-out property<float> value;
min-height: 24px; min-width: 100px; horizontal-stretch: 1; vertical-stretch: 0;
border-radius: root.height/2; background: touch.pressed ? #eee: #ddd; border-width: 1px; border-color: root.background.darker(25%);
handle := Rectangle { width: self.height; height: parent.height; border-width: 3px; border-radius: self.height / 2; background: touch.pressed ? #f8f: touch.has-hover ? #66f : #0000ff; border-color: self.background.darker(15%); x: (root.width - handle.width) * (root.value - root.minimum)/(root.maximum - root.minimum);
touch := TouchArea { moved => { if (self.enabled && self.pressed) { root.value = max(root.minimum, min(root.maximum, root.value + (self.mouse-x - self.pressed-x) * (root.maximum - root.minimum) / root.width)); } } } }}
export component Recipe inherits Window { VerticalBox { alignment: start; slider := MySlider { maximum: 100; } Text { text: "Value: \{round(slider.value)}"; } }}Custom Tabs
Section titled “Custom Tabs”Use this recipe as a basis to when you want to create your own custom tab widget.
import { Button } from "std-widgets.slint";
export component Recipe inherits Window { preferred-height: 200px; in-out property <int> active-tab; VerticalLayout { tab_bar := HorizontalLayout { spacing: 3px; Button { text: "Red"; clicked => { root.active-tab = 0; } } Button { text: "Blue"; clicked => { root.active-tab = 1; } } Button { text: "Green"; clicked => { root.active-tab = 2; } } } Rectangle { clip: true; Rectangle { background: red; x: root.active-tab == 0 ? 0 : root.active-tab < 0 ? - self.width - 1px : parent.width + 1px; animate x { duration: 125ms; easing: ease; } } Rectangle { background: blue; x: root.active-tab == 1 ? 0 : root.active-tab < 1 ? - self.width - 1px : parent.width + 1px; animate x { duration: 125ms; easing: ease; } } Rectangle { background: green; x: root.active-tab == 2 ? 0 : root.active-tab < 2 ? - self.width - 1px : parent.width + 1px; animate x { duration: 125ms; easing: ease; } } } }}Custom Table View
Section titled “Custom Table View”Slint provides a table widget, but you can also do something custom based on a
ListView.
import { VerticalBox, ListView } from "std-widgets.slint";
component TableView inherits Rectangle { in property <[string]> columns; in property <[[string]]> values;
private property <length> e: self.width / root.columns.length; private property <[length]> column_sizes: [ root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, ];
VerticalBox { padding: 5px; HorizontalLayout { padding: 5px; spacing: 5px; vertical-stretch: 0; for title[idx] in root.columns : HorizontalLayout { width: root.column_sizes[idx]; Text { overflow: elide; text: title; } Rectangle { width: 1px; background: gray; TouchArea { width: 10px; x: (parent.width - self.width) / 2; property <length> cached; pointer-event(event) => { if (event.button == PointerEventButton.left && event.kind == PointerEventKind.down) { self.cached = root.column_sizes[idx]; } } moved => { if (self.pressed) { root.column_sizes[idx] += (self.mouse-x - self.pressed-x); if (root.column_sizes[idx] < 0) { root.column_sizes[idx] = 0; } } } mouse-cursor: ew-resize; } } } } ListView { for r in root.values : HorizontalLayout { padding: 5px; spacing: 5px; for t[idx] in r : HorizontalLayout { width: root.column_sizes[idx]; Text { overflow: elide; text: t; } } } } }}
export component Example inherits Window { TableView { columns: ["Device", "Mount Point", "Total", "Free"]; values: [ ["/dev/sda1", "/", "255GB", "82.2GB"] , ["/dev/sda2", "/tmp", "60.5GB", "44.5GB"] , ["/dev/sdb1", "/home", "255GB", "32.2GB"] , ]; }}Breakpoints for Responsive User Interfaces
Section titled “Breakpoints for Responsive User Interfaces”Use recipe implements a responsive SideBar that collapses when the parent width is smaller than the given break-point. When clicking the Button, the SideBar expands again. Use the blue Splitter to resize the container and test the responsive behavior.
import { Button, Palette } from "std-widgets.slint";
export component SideBar inherits Rectangle { private property <bool> collapsed: root.reference-width < root.break-point;
/// Defines the reference width to check `break-point`. in-out property <length> reference-width;
/// If `reference-width` is less `break-point` the `SideBar` collapses. in-out property <length> break-point: 600px;
/// Set the text of the expand button. in-out property <string> expand-button-text;
width: 160px;
container := Rectangle { private property <bool> expanded;
width: parent.width; background: Palette.background.darker(0.2);
VerticalLayout { padding: 2px; alignment: start;
HorizontalLayout { alignment: start;
if (root.collapsed) : Button { checked: container.expanded; text: root.expand-button-text;
clicked => { container.expanded = !container.expanded; } } }
@children }
states [ expanded when container.expanded && root.collapsed : { width: 160px;
in { animate width { duration: 200ms; } } out { animate width { duration: 200ms; } } in { animate width { duration: 200ms; } } out { animate width { duration: 200ms; } } } ] }
states [ collapsed when root.collapsed : { width: 62px; } ]}
component Splitter inherits TouchArea { width: 4px; mouse-cursor: ew-resize;
Rectangle { width: 100%; height: 100%; background: blue; }}
export component SideBarTest inherits Window { preferred-width: 700px; min-height: 400px; background: gray;
GridLayout { x: 0; width: splitter.x;
Rectangle { height: 100%; col: 1; background: white;
HorizontalLayout { padding: 8px;
Text { color: black; text: "Content"; } } } SideBar { col: 0; reference-width: parent.width; expand-button-text: "E"; } }
splitter := Splitter { x: root.width - self.width; height: 100%;
moved => { self.x = min(root.width - self.width, max(400px, self.x + self.mouse-x - self.pressed-x)); } }}© 2025 SixtyFPS GmbH