Color Picker for Slint
KitColorPicker · Slint Kit 0.3.0
Color editor with hex text, channel inputs and channel sliders.

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 a hexadecimal sRGB color or use the picker. Rust validates byte channels, keeps drafts separate and publishes the accepted color.
ui/app.slint
export { KitFormValidation, HtmlInputKind, NumberInputFormat, KitInputConstraints, KitValidationResult, KitValidityState } from "@kit/kit.slint";
import {KitColorPicker,KitThemes,SrgbColorView,ColorChannel,NavigationStep} from "@kit/kit.slint";
export {SrgbColorView,ColorChannel,NavigationStep}
export component App inherits Window {
title:"color-picker";preferred-width:540px;preferred-height:800px;max-width:100000px;max-height:100000px;background:KitThemes.light.canvas;
in-out property <string> hex;in property <string> error;in property <string> accepted;in property <SrgbColorView> color-value;in property <color> swatch;
callback commit(string);callback edit();callback choose(SrgbColorView);callback cancel();in-out property <string> red;in-out property <string> green;in-out property <string> blue;in-out property <string> alpha;
callback channel(ColorChannel,string);callback position(ColorChannel,float);callback navigate(ColorChannel,NavigationStep);callback begin-gesture(ColorChannel);callback finish(ColorChannel);callback cancel-channel(ColorChannel);
VerticalLayout{padding:24px;spacing:16px;alignment:start;
control:=KitColorPicker{label:"Accent color";accepted-color:root.color-value;swatch:root.swatch;color-requested(value)=>{root.choose(value);}hex-text<=>root.hex;red-text<=>root.red;green-text<=>root.green;blue-text<=>root.blue;alpha-text<=>root.alpha;hex-error:root.error;
hex-edited(value)=>{root.edit();}hex-commit-requested(value)=>{root.commit(value);}
channel-edited(channel,value)=>{root.edit();}channel-commit-requested(channel,value)=>{root.channel(channel,value);}
channel-position-requested(channel,position)=>{root.position(channel,position);}channel-navigate(channel,step)=>{root.navigate(channel,step);}
gesture-started(channel)=>{root.begin-gesture(channel);}committed(channel)=>{root.finish(channel);}cancelled(channel)=>{root.cancel-channel(channel);}}
Text{text:"Accepted: "+root.accepted;color:KitThemes.light.text-primary;wrap:word-wrap;}
}
}
src/main.rs
use slint::ComponentHandle;
use slint_kit::{AlphaPolicy, DraftValue, SrgbColor};
use std::{cell::RefCell, rc::Rc};
mod ui {
slint::include_modules!();
}
fn publish(app: &ui::App, color: SrgbColor, accepted: bool) {
app.set_hex(color.hex(AlphaPolicy::Opaque).into());
app.set_color_value(ui::SrgbColorView {
red: color.red.into(),
green: color.green.into(),
blue: color.blue.into(),
alpha: 255,
});
app.set_swatch(slint::Color::from_rgb_u8(
color.red,
color.green,
color.blue,
));
if accepted {
app.set_accepted(color.hex(AlphaPolicy::Opaque).into());
}
app.set_red(color.red.to_string().into());
app.set_green(color.green.to_string().into());
app.set_blue(color.blue.to_string().into());
app.set_alpha("255".into());
}
fn main() -> Result<(), slint::PlatformError> {
let app = ui::App::new()?;
slint_kit::init_form_validation!(ui, app);
let initial = SrgbColor {
red: 79,
green: 70,
blue: 229,
alpha: 255,
};
let state = Rc::new(RefCell::new((initial, initial)));
publish(&app, initial, true);
let weak = app.as_weak();
let shared = state.clone();
app.on_commit(move |text| {
if let Some(app) = weak.upgrade() {
match SrgbColor::parse_hex(&text, AlphaPolicy::Opaque) {
DraftValue::Parsed(value) => {
*shared.borrow_mut() = (value, value);
publish(&app, value, true);
app.set_error("".into());
}
_ => app.set_error("Use #RRGGBB or #RGB hexadecimal".into()),
}
}
});
let weak = app.as_weak();
app.on_edit(move || {
if let Some(app) = weak.upgrade() {
app.set_error("".into());
}
});
let weak = app.as_weak();
let shared = state.clone();
app.on_choose(move |view| {
if let Some(app) = weak.upgrade() {
if let (Ok(red), Ok(green), Ok(blue)) = (
view.red.try_into(),
view.green.try_into(),
view.blue.try_into(),
) {
let color = SrgbColor {
red,
green,
blue,
alpha: 255,
};
*shared.borrow_mut() = (color, color);
publish(&app, color, true);
app.set_error("".into());
}
}
});
fn set(color: &mut SrgbColor, channel: ui::ColorChannel, value: u8) {
match channel {
ui::ColorChannel::Red => color.red = value,
ui::ColorChannel::Green => color.green = value,
ui::ColorChannel::Blue => color.blue = value,
ui::ColorChannel::Alpha => {}
}
}
fn get(color: SrgbColor, channel: ui::ColorChannel) -> u8 {
match channel {
ui::ColorChannel::Red => color.red,
ui::ColorChannel::Green => color.green,
ui::ColorChannel::Blue => color.blue,
ui::ColorChannel::Alpha => 255,
}
}
let weak = app.as_weak();
let shared = state.clone();
app.on_channel(move |_channel, _text| {
if let Some(app) = weak.upgrade() {
let channels = app
.get_red()
.parse::<u8>()
.ok()
.zip(app.get_green().parse::<u8>().ok())
.zip(app.get_blue().parse::<u8>().ok());
if let Some(((red, green), blue)) = channels {
let color = SrgbColor {
red,
green,
blue,
alpha: 255,
};
*shared.borrow_mut() = (color, color);
publish(&app, color, true);
app.set_error("".into());
} else {
app.set_error("Use whole sRGB channels from 0 to 255".into());
}
}
});
let weak = app.as_weak();
let shared = state.clone();
app.on_position(move |channel, position| {
if let Some(app) = weak.upgrade() {
let mut state = shared.borrow_mut();
set(
&mut state.1,
channel,
(position.clamp(0.0, 1.0) * 255.0).round() as u8,
);
publish(&app, state.1, false);
}
});
let weak = app.as_weak();
let shared = state.clone();
app.on_navigate(move |channel, step| {
if let Some(app) = weak.upgrade() {
let mut state = shared.borrow_mut();
let old = get(state.1, channel) as i32;
let next = match step {
ui::NavigationStep::First => 0,
ui::NavigationStep::Last => 255,
ui::NavigationStep::Next => old + 1,
ui::NavigationStep::Previous => old - 1,
ui::NavigationStep::PageNext => old + 16,
ui::NavigationStep::PagePrevious => old - 16,
};
set(&mut state.1, channel, next.clamp(0, 255) as u8);
state.0 = state.1;
publish(&app, state.0, true);
}
});
let shared = state.clone();
app.on_begin_gesture(move |_| {
let mut state = shared.borrow_mut();
state.1 = state.0;
});
let weak = app.as_weak();
let shared = state.clone();
app.on_finish(move |_| {
if let Some(app) = weak.upgrade() {
let mut state = shared.borrow_mut();
state.0 = state.1;
publish(&app, state.0, true);
}
});
let weak = app.as_weak();
let shared = state.clone();
app.on_cancel_channel(move |_| {
if let Some(app) = weak.upgrade() {
let mut state = shared.borrow_mut();
state.1 = state.0;
publish(&app, state.0, true);
app.set_error("".into());
}
});
app.run()
}
Cargo.toml and build.rs
Cargo.toml
[package]
name = "slint-kit-color-picker-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
Keep accepted SrgbColor in Rust. Connect channel text commits, position requests and gesture start/commit/cancel callbacks.
The example accepts opaque hex colors and channel values from 0 to 255. Cancel restores the color saved at gesture start.
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 { KitColorPicker, KitTheme, KitThemes } from "@kit/kit.slint";
export component Example inherits Window {
title: "KitColorPicker example";
preferred-width: 480px;
preferred-height: 550px;
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: "Color Picker"; color: root.theme.text-primary; font-size: 20px * root.theme.text-scale; font-weight: 600; }
KitColorPicker { theme: root.theme; label: "Accent color"; swatch: #6366f1; accepted-color: { red: 99, green: 102, blue: 241, alpha: 255 }; hex-text: "#6366F1"; red-text: "99"; green-text: "102"; blue-text: "241"; }
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 |
control-size |
ControlSize |
in |
access |
FieldAccess |
in |
alpha-mode |
ColorAlphaMode |
in |
accepted-color |
SrgbColorView |
in |
swatch |
color |
in |
label |
string |
in |
hex-error |
string |
in |
red-error |
string |
in |
green-error |
string |
in |
blue-error |
string |
in |
alpha-error |
string |
in |
hex-text |
string |
in-out |
red-text |
string |
in-out |
green-text |
string |
in-out |
blue-text |
string |
in-out |
alpha-text |
string |
in-out |
Types and variants
| Type | Declaration |
|---|---|
enum ColorAlphaMode |
opaque, editable |
enum ColorChannel |
red, green, blue, alpha |
enum ControlSize |
compact, medium, comfortable |
enum FieldAccess |
editable, read-only, disabled |
enum NavigationStep |
next, previous, first, last, page-next, page-previous |
struct SrgbColorView |
red: int, green: int, blue: int, alpha: int |
Callbacks
hex-edited(string)hex-commit-requested(string)color-requested(SrgbColorView)channel-edited(ColorChannel,string)channel-commit-requested(ColorChannel,string)channel-position-requested(ColorChannel,float)channel-navigate(ColorChannel,NavigationStep)gesture-started(ColorChannel)committed(ColorChannel)cancelled(ColorChannel)