Skip to content
PorabuildHeroGPUI component library
v0.14.0 · Apache-2.0

Forms

Input OTP

A segmented one-time-password input.

Rust
use herogpui::components::input_otp::{InputOTP, OtpState};

InputOTP on docs.rsSource at v0.14.0

Usage

Showing Usage, live: HeroGPUI compiled to WebAssembly. The canvas has no accessibility tree; each example's description and code below carry the same information.

Usage

Custom slot content inherits 14px/20px text; the built-in digits use 18px/24px text. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{ActiveTheme, InputOTP, Label, Link, OtpState};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{App, px};
use herogpui::gpui;

let done = self.otp_done.clone();

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        Label::new("Verify account").into_any_element(),
        muted_para("We've sent a code to a****@gmail.com", cx),
        InputOTP::new(self.otp.clone())
            .separator()
            .slot_hover_bg(cx.colors().accent.soft())
            .radius(px(4.))
            .on_complete(cx.listener(|this, code: &str, _, cx| {
                this.otp_done = code.to_owned();
                cx.notify();
            }))
            .into_any_element(),
        gpui::div()
            .flex()
            .items_center()
            .gap(px(5.))
            .child(muted_para(
                &if done.is_empty() {
                    "Didn't receive a code?".to_owned()
                } else {
                    format!("Complete: {done}")
                },
                cx,
            ))
            .child(Link::new("input-otp-resend").label("Resend").href("#"))
            .into_any_element(),
    ])

Also calls muted_para (included when you copy).

Filled & Active

Filled slots use the focus background and the active cursor slot keeps its caret and keyboard-visible ring. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{InputOTP, OtpState};

InputOTP::new(self.demo_otp("otp-filled-active", 6, cx))
    .value("123")
    .auto_focus(true)

Variants

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{FieldVariant, InputOTP, OtpState};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        InputOTP::new(self.demo_otp("otp-primary", 6, cx)).into_any_element(),
        InputOTP::new(self.demo_otp("otp-secondary", 6, cx))
            .variant(FieldVariant::Secondary)
            .into_any_element(),
    ])

In Surface

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{FieldVariant, InputOTP, OtpState, Surface};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![Surface::new()
        .padding(px(24.))
        .child(
            InputOTP::new(self.demo_otp("otp-surface", 6, cx))
                .variant(FieldVariant::Secondary),
        )
        .into_any_element()])

Disabled State

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{InputOTP, OtpState};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![InputOTP::new(self.demo_otp("otp-disabled", 6, cx))
        .is_disabled(true)
        .into_any_element()])

Four Digits

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{InputOTP, OtpState};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        InputOTP::new(self.demo_otp("otp-four", 4, cx)).into_any_element()
    ])

Controlled

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{ActiveTheme, InputOTP, OtpState};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{App, px};
use herogpui::gpui;

let otp_typed = self.otp_typed.clone();

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        InputOTP::new(self.demo_otp("otp-controlled", 6, cx))
            // `value` seeds the caller's copy into the field
            // once, which is what "controlled" means here.
            .value(otp_typed.as_str())
            .on_change(cx.listener(|this, code: &str, _, cx| {
                this.otp_typed = code.to_owned();
                cx.notify();
            }))
            .into_any_element(),
        para(
            &if otp_typed.is_empty() {
                "Nothing typed yet".to_owned()
            } else {
                format!("Value: {otp_typed}")
            },
            cx,
        ),
    ])

Also calls para (included when you copy).

On Complete

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{ActiveTheme, InputOTP, OtpState};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{App, px};
use herogpui::gpui;

let done = self.otp_done.clone();

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        InputOTP::new(self.demo_otp("otp-complete", 6, cx))
            .on_complete(cx.listener(|this, code: &str, _, cx| {
                this.otp_done = code.to_owned();
                cx.notify();
            }))
            .into_any_element(),
        para(
            &if done.is_empty() {
                "`onComplete` fires once every slot is filled".to_owned()
            } else {
                format!("Completed with {done}")
            },
            cx,
        ),
    ])

Also calls para (included when you copy).

Custom Slots

The GPUI `slot` extension receives each slot's live index and character. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{InputOTP, OtpState};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![InputOTP::new(self.demo_otp(
        "otp-custom-slots",
        4,
        cx
    ))
    .slot(|index, value| {
        gpui::div()
            .flex()
            .flex_col()
            .items_center()
            .text_size(px(11.))
            .child(value.unwrap_or('·').to_string())
            .child(format!("#{index}"))
            .into_any_element()
    })
    .into_any_element(),])

Form Example

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{Button, Form, FormField, InputOTP, OtpState};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![{
        let state = self.demo_otp("otp-form", 6, cx);
        Form::new()
            .field(FormField::code("code", state.clone()).is_required(true))
            .child(InputOTP::new(state).name("code"))
            .child(Button::new("input-otp-form-submit").label("Verify"))
            .into_any_element()
    }])

With Pattern

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{InputOTP, OtpPattern, OtpState};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        InputOTP::new(self.demo_otp("otp-pat-digits", 4, cx))
            .pattern(OtpPattern::Digits)
            .into_any_element(),
        InputOTP::new(self.demo_otp("otp-pat-alnum", 4, cx))
            .pattern(OtpPattern::Alphanumeric)
            .into_any_element(),
        InputOTP::new(self.demo_otp("otp-pat-any", 4, cx))
            .pattern(OtpPattern::Any)
            .into_any_element(),
    ])

With Validation

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{InputOTP, OtpState};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        InputOTP::new(self.demo_otp("otp-validate", 6, cx))
            .validate(|code| {
                (code.chars().count() < 6).then(|| "Enter all six digits".into())
            })
            .into_any_element(),
        InputOTP::new(self.demo_otp("otp-invalid", 6, cx))
            // `isInvalid` from the outside: what a rejected code
            // looks like when the server says so.
            .is_invalid(true)
            .into_any_element(),
    ])

Helpers used by these examples

Plain functions over the public API, defined once here and copied with each example that calls them.

muted_para
/// A paragraph of body text.
/// A muted `text-sm` paragraph, for the supporting copy a v3 example composes
/// *inside* its preview (`<p className="text-sm text-muted">`). [`para`] is the
/// gallery's own prose voice and belongs outside the bordered preview.
fn muted_para(text: &str, cx: &App) -> gpui::AnyElement {
    let colors = cx.colors();
    gpui::div()
        .text_size(px(14.))
        .line_height(px(20.))
        .text_color(colors.muted)
        .child(text.to_owned())
        .into_any_element()
}
para
fn para(text: &str, cx: &App) -> gpui::AnyElement {
    let colors = cx.colors();
    gpui::div()
        .text_size(px(14.5))
        .line_height(px(24.))
        .text_color(colors.foreground)
        .max_w(px(720.))
        .child(text.to_owned())
        .into_any_element()
}

Anatomy

Rust types

InputOTP

Input OTP is assembled from these builders. The API reference lists each one.

Customization

Shared appearance belongs on ThemeBuilder::components and .recipe("name"). Typed builders change how Input OTP looks at one call site. Every builder also carries sx for placement, width and flex: it takes GPUI's own styling methods and refines them over the component's root after the theme, so an instance override wins. Behaviour builders live in the API reference below.

Styling

Input OTP styling
MethodValuesDescription
variant(FieldVariant)FieldVariant::Primary | FieldVariant::SecondaryVisual treatment of the slots.
Rust
// The one slot for caller-owned low-level styling: GPUI's
// styling methods, refined over the root element last.
.sx(|el| {
    el.bg(gpui::rgba(0xffa500ff))             // background
        .text_color(gpui::rgba(0x000000ff))   // text
        .w(gpui::px(13.))                     // size
        .h(gpui::px(12.))
})

API reference

Against HeroUI v3.2.6: 12 entries ported, 13 partial, 8 not ported. Partial rows are marked in the tables; entries not ported are listed under each table with the reason.

Builders

Input OTP builders
BuilderValuesDefaultDescription
with_length(cx, length)number— (required)Number of input slots.
value(code)string—Controlled value.
on_change(callback)Fn(value: string) -> ()—Handler called whenever the value changes.
on_complete(callback)PartialFn(value: string) -> ()—Handler called when all slots are filled.
variant(FieldVariant)FieldVariant::Primary | FieldVariant::SecondaryFieldVariant::PrimaryVisual treatment of the slots.
slot(render)PartialAnyElement—Composition of Group, Slot and Separator parts.
is_disabled(bool)true | falsefalseWhether the input is disabled.
is_invalid(bool)true | falsefalseWhether the input is invalid.
validation_errors(errors)Partialstring[]—Server-side or custom validation errors.
pattern(OtpPattern)Partialstring—Regular expression for accepted characters.
text_align(OtpTextAlign)PartialOtpTextAlign::Left | OtpTextAlign::Center | OtpTextAlign::RightOtpTextAlign::LeftText alignment within each slot.
placeholder(impl Into<SharedString>)string—Placeholder text for empty slots.
paste_transformer(callback)PartialFn(text: string) -> string—Transforms pasted text before filling the slots.
name(value)Partialstring—Name used by form_field(); explicit Form registration is required.
auto_focus(bool)true | false—Focuses the first slot on mount.
Not ported8 HeroUI entries not ported
Input OTP builders not ported
HeroUI v3Why it is not ported
InputOTP.classNameFor the root container.
InputOTP.containerClassNameFor the inner container.
InputOTP.validationDetailsValidity details.
InputOTP.inputModeVirtual-keyboard hint.
InputOTP.Group.classNameFor the group.
InputOTP.Group.childrenSlot children in the group.
InputOTP.Slot.classNameFor the slot.
InputOTP.Separator.classNameFor the separator.

Parts

Input OTP parts
PartDescription
InputOTPPartialRoot field and editing surface.
InputOTPPartialGroup of slots; implicit in the flat GPUI row.
InputOTPPartialOne indexed character slot.
InputOTPPartialOptional separator bar after each three slots.

States

Input OTP states
StateBuilderDescription
Hoveredcell.hover(variant fillHovered slot surface and field-border color follow the active variant while active/filled slots retain their focus background.
Activeactive_bgFocused slot carries the fake caret and focus background; keyboard modality adds the shared focus ring.
Filledactive_bgSlot containing a character keeps the focus background and animates the portable value entrance.
Disabledis_disabled focus/key gatesDisabled root and slots.
InvalidPartialvalidation::resolveInvalid field treatment.