Number Stepper for Slint
KitNumberStepper · Slint Kit 0.3.0
Decimal input with increment and decrement requests.

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.
Edit the decimal value or click the step buttons. Rust applies exact steps of one within 0–100, then accepts the result.
ui/app.slint
export { KitFormValidation, HtmlInputKind, NumberInputFormat, KitInputConstraints, KitValidationResult, KitValidityState } from "@kit/kit.slint";
import { KitNumberStepper, KitButton, KitThemes, ValidationPresentation, StepDirection } from "@kit/kit.slint";
export component App inherits Window {
title: "Quantity"; preferred-width: 520px; preferred-height: 320px;
max-width: 100000px; max-height: 100000px; background: KitThemes.light.canvas;
in-out property <string> draft; in property <string> error; in property <string> accepted;
callback edited(string); callback commit(); callback blurred(); callback cancel();
callback step(StepDirection);
VerticalLayout {
padding: 24px; spacing: 16px; alignment: start;
control := KitNumberStepper {
required: true;
label: "Quantity"; text <=> root.draft; error: root.error;
validation: root.error != "" ? ValidationPresentation.invalid : ValidationPresentation.pristine;
placeholder: "12";
edited(value) => { root.edited(value); }
commit-requested(value) => { root.commit(); }
cancelled => { root.cancel(); }
focus-observed(focused) => { if (!focused) { root.blurred(); } }
step-requested(direction) => { root.step(direction); }
}
KitButton { text: "Accept"; activated => { root.commit(); } }
Text { text: "Accepted: " + root.accepted; color: KitThemes.light.text-primary; wrap: word-wrap; }
}
}
src/main.rs
use slint::ComponentHandle;
use slint_kit::*;
mod ui {
slint::include_modules!();
}
use ui::App;
type Value = DecimalValue;
fn parse(text: &str) -> DraftValue<Value> {
DecimalRules {
locale: NumberLocale::EnCa,
scale: DecimalScale::new(2).unwrap(),
sign: SignPolicy::NonNegative,
bounds: None,
}
.parse(text)
}
fn format(value: &Value) -> String {
value.to_string()
}
fn main() -> Result<(), slint::PlatformError> {
let app = App::new()?;
slint_kit::init_form_validation!(ui, app);
let initial = DecimalValue::from_integer(12);
app.set_accepted(format(&initial).into());
let weak = app.as_weak();
let field = FieldBinding::<Value, ()>::new(
Some(initial),
Ownership::Local,
CommitPolicy::OnBlur,
AbsencePolicy::Required,
parse,
format,
|_| String::new(),
move |view| {
if let Some(app) = weak.upgrade() {
app.set_draft(view.draft.into());
app.set_error(view.error.into());
}
},
);
field.set_html_constraints(slint_kit::HtmlConstraints {
kind: slint_kit::HtmlInputKind::Number, required: true,
step: Some("1".into()),
..Default::default()
});
let weak = app.as_weak();
field.on_intent(move |intent| {
if let Some(app) = weak.upgrade() {
if let FieldIntent::Commit(value) = intent {
app.set_accepted(format(&value).into());
}
}
});
let binding = field.clone();
app.on_edited(move |text| {
let _ = binding.edited(&text);
});
let binding = field.clone();
app.on_commit(move || {
let _ = binding.commit(CommitTrigger::Submit);
});
let binding = field.clone();
app.on_blurred(move || {
let _ = binding.commit(CommitTrigger::Blur);
});
let binding = field.clone();
app.on_cancel(move || binding.cancel());
let binding = field.clone();
app.on_step(move |direction| {
let Some(value) = binding.accepted() else {
return;
};
let next = DecimalStep::new(DecimalValue::from_integer(1))
.unwrap()
.apply(
value,
if direction == ui::StepDirection::Increment {
StepDirection::Increment
} else {
StepDirection::Decrement
},
NumericBounds::new(DecimalValue::ZERO, DecimalValue::from_integer(100)).unwrap(),
);
if let Ok(next) = next {
let _ = binding.edited(&next.to_string());
let _ = binding.commit(CommitTrigger::Explicit);
}
});
app.run()
}
Cargo.toml and build.rs
Cargo.toml
[package]
name = "slint-kit-number-stepper-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
Connect step-requested to DecimalStep and NumericBounds. The example steps by 1 between 0 and 100.
Rust applies each step to the accepted DecimalValue. Text edits use the same parser and commit path as the other numeric fields.
Choose between built-in options and custom composition
Shared options
label— built-in property; custom composition exampledescription— 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 { KitNumberStepper, KitTheme, KitThemes } from "@kit/kit.slint";
export component Example inherits Window {
title: "KitNumberStepper example";
preferred-width: 420px;
preferred-height: 180px;
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: "Number Stepper"; color: root.theme.text-primary; font-size: 20px * root.theme.text-scale; font-weight: 600; }
KitNumberStepper { theme: root.theme; label: "Guests"; text: "3"; step-requested(direction) => { root.status = "Step requested; validate in Rust"; }; edited(value) => { root.status = "Draft: " + 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 KitField.
Properties
| Name | Type | Direction |
|---|---|---|
theme |
KitTheme |
in |
control-size |
ControlSize |
in |
access |
FieldAccess |
in |
validation |
ValidationPresentation |
in |
required |
bool |
in |
min |
string |
in |
max |
string |
in |
step |
string |
in |
number-format |
NumberInputFormat |
in |
default-value |
string |
in |
custom-validity |
string |
in-out |
validity |
KitValidityState |
out |
will-validate |
bool |
out |
validation-message |
string |
out |
label |
string |
in |
description |
string |
in |
error |
string |
in |
placeholder |
string |
in |
prefix |
string |
in |
suffix |
string |
in |
can-increment |
bool |
in |
can-decrement |
bool |
in |
increment-label |
string |
in |
decrement-label |
string |
in |
text |
string |
in-out |
has-focus |
bool |
out |
Types and variants
| Type | Declaration |
|---|---|
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, |
enum NumberInputFormat |
canonical, decimal-comma |
enum StepDirection |
increment, decrement |
enum ValidationPresentation |
pristine, checking, valid, invalid |
Callbacks
invalid()edited(string)commit-requested(string)cancelled()focus-observed(bool)step-requested(StepDirection)
Functions
check-validity() → boolreport-validity() → boolset-custom-validity(message: string)reset-validation()take-focus()