Component 35 of 60
Number Field
A numeric input with step controls.
Starting the preview. GPUI compiles to WebAssembly, so the first load takes a few seconds. This browser has neither WebGPU nor WebGL 2, so the live preview cannot run. The code on this page still works. The preview did not start.
fn number_field_preview(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let state = window.use_keyed_state("quantity", cx, |window, cx| {
InputState::new(window, cx)
.default_value("3")
.min(0.)
.max(20.)
.step(1.)
});
NumberField::new(&state).aria_label("Quantity").w(px(200.))
}
Installation
After the one-time setup, run the command from your app root. The CLI
writes the modules and keeps src/ui/mod.rs in step.
gpuicn add number-field
If you prefer to copy the files by hand, declare them yourself:
// src/ui/mod.rs
pub mod theme;
pub mod input;
pub mod field;
pub mod number_field;
// src/main.rs
mod ui;
Usage
Put this in a view module and return example(window, cx) from its render method.
use crate::ui::input::InputState;
use crate::ui::number_field::*;
use gpui_kit::{App, IntoElement, Window};
fn example(window: &mut Window, cx: &mut App) -> impl IntoElement {
let state = window.use_keyed_state("quantity", cx, |window, cx| {
InputState::new(window, cx)
.default_value("3")
.min(0.)
.max(20.)
.step(1.)
});
NumberField::new(&state).aria_label("Quantity")
}
API reference
pub fn new(state: &Entity<InputState>) -> Self
Configure range and step on the caller-owned `InputState`.
pub fn aria_label(mut self, value: impl Into<SharedString>) -> Self
Sets the accessible name of the control.
pub fn disabled(mut self, value: bool) -> Self
Disables interaction and applies the disabled appearance.
pub fn read_only(mut self, value: bool) -> Self
Prevents user changes while preserving focus and reading.
pub fn invalid(mut self, value: bool) -> Self
Applies validation error colors and an invalid accessibility description.
Installed source, 140 lines
//! Nova presentation for GPUI Kit's numeric input and step actions.
use super::{
field::FieldControl,
input::Input,
theme::{
UiTheme,
primitives::{control, disabled, focus},
},
};
use gpui_icons::{LucideIcon, lucide};
use gpui_kit::base::{NumberInput, input::InputState};
pub use gpui_kit::base::{NumberInputEvent, NumberStep, StepAction};
use gpui_kit::{
AnyElement, App, Entity, FocusHandle, Focusable as _, IntoElement, ParentElement as _,
RenderOnce, SharedString, Styled, Window, prelude::FluentBuilder as _,
};
#[derive(IntoElement)]
/// A Kit numeric editor with Nova stepper controls.
pub struct NumberField {
state: Entity<InputState>,
style: gpui_kit::StyleRefinement,
label: Option<SharedString>,
disabled: bool,
read_only: bool,
invalid: bool,
}
impl NumberField {
/// Configure range and step on the caller-owned `InputState`.
pub fn new(state: &Entity<InputState>) -> Self {
Self {
state: state.clone(),
style: Default::default(),
label: None,
disabled: false,
read_only: false,
invalid: false,
}
}
/// Sets the accessible name of the control.
pub fn aria_label(mut self, value: impl Into<SharedString>) -> Self {
self.label = Some(value.into());
self
}
/// Disables interaction and applies the disabled appearance.
pub fn disabled(mut self, value: bool) -> Self {
self.disabled = value;
self
}
/// Prevents user changes while preserving focus and reading.
pub fn read_only(mut self, value: bool) -> Self {
self.read_only = value;
self
}
/// Applies validation error colors and an invalid accessibility description.
pub fn invalid(mut self, value: bool) -> Self {
self.invalid = value;
self
}
}
impl Styled for NumberField {
fn style(&mut self) -> &mut gpui_kit::StyleRefinement {
&mut self.style
}
}
impl RenderOnce for NumberField {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let t = UiTheme::read(cx).clone();
let focused = self.state.focus_handle(cx).is_focused(window) && !self.disabled;
let mut input = Input::new(&self.state)
.bordered(false)
.disabled(self.disabled)
.read_only(self.read_only)
.invalid(self.invalid);
if let Some(label) = self.label {
input = input.aria_label(label);
}
let mut root = NumberInput::new(&self.state)
.disabled(self.disabled)
.w_full()
.h(t.space(8.))
.map(|root| {
focus::shown_for(
control::frame(root, &t, self.invalid),
&t,
focused,
self.invalid,
)
})
.when(self.disabled, disabled::style)
.decrement_button({
let t = t.clone();
move |button| {
button
.accessibility_label("Decrease value")
.size(t.space(8.))
.cursor_pointer()
.child(
lucide(LucideIcon::Minus)
.size(t.space(3.5))
.text_color(t.colors.foreground),
)
}
})
.increment_button({
let t = t.clone();
move |button| {
button
.accessibility_label("Increase value")
.size(t.space(8.))
.cursor_pointer()
.child(
lucide(LucideIcon::Plus)
.size(t.space(3.5))
.text_color(t.colors.foreground),
)
}
})
.input(input);
if self.read_only {
root = root.on_step(|_, _, _| {});
}
super::theme::apply_style(root, &self.style)
}
}
impl FieldControl for NumberField {
fn field_focus_handle(&self, cx: &App) -> FocusHandle {
self.state.read(cx).focus_handle(cx)
}
fn into_field_control(self, label: SharedString, disabled: bool, invalid: bool) -> AnyElement {
self.aria_label(label)
.disabled(disabled)
.invalid(invalid)
.into_any_element()
}
}
Platform notes
Visual reference: shadcn/ui 4.19.0, Neutral Nova. Implementation:
crates/ui/registry/number_field/number_field.rs, using GPUI Kit 0.6.4.
NumberField takes a Kit InputState configured with min, max and step. Kit NumberInput and step actions own parsing and numeric changes; gpuicn composes the shared Input and Nova stepper buttons. Disabled and read-only state guard editing and stepping. Pointer scrubbing is outside this API.
The invalid builder applies the destructive border and accessibility
description directly or through Field::from_control.
The Rust source and Usage example define the supported API. Browser DOM/CSS behavior, exact exit animations and cross-platform screen-reader parity are not implied by the native component. See migration qualification for the checks completed on this revision.