OtpInput
A specialized input component for one-time passwords (OTP) that displays multiple input fields in a grid layout. Perfect for SMS verification codes, authenticator app codes, and other numeric verification scenarios.
Import
rust
use gpui_component::input::{OtpInput, OtpState};
Usage
Basic OTP Input
rust
let otp_state = cx.new(|cx| OtpState::new(6, window, cx));
OtpInput::new(&otp_state)
With Default Value
rust
let otp_state = cx.new(|cx|
OtpState::new(6, window, cx)
.default_value("123456")
);
OtpInput::new(&otp_state)
Masked OTP Input
rust
let otp_state = cx.new(|cx|
OtpState::new(6, window, cx)
.masked(true)
.default_value("123456")
);
OtpInput::new(&otp_state)
Different Sizes
rust
// Small size
OtpInput::new(&otp_state).small()
// Medium size (default)
OtpInput::new(&otp_state)
// Large size
OtpInput::new(&otp_state).large()
// Custom size
OtpInput::new(&otp_state).with_size(px(55.))
Grouped Layout
rust
// Single group (all fields together)
OtpInput::new(&otp_state).groups(1)
// Two groups (default) - splits fields in half
OtpInput::new(&otp_state).groups(2)
// Three groups - splits fields into thirds
OtpInput::new(&otp_state).groups(3)
Disabled State
rust
OtpInput::new(&otp_state).disabled(true)
Different Length Codes
rust
// 4-digit PIN
let pin_state = cx.new(|cx| OtpState::new(4, window, cx));
OtpInput::new(&pin_state).groups(1)
// 6-digit SMS code (most common)
let sms_state = cx.new(|cx| OtpState::new(6, window, cx));
OtpInput::new(&sms_state)
// 8-digit authenticator code
let auth_state = cx.new(|cx| OtpState::new(8, window, cx));
OtpInput::new(&auth_state).groups(2)
Handle OTP Events
rust
let otp_state = cx.new(|cx| OtpState::new(6, window, cx));
cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| {
match event {
InputEvent::Change => {
let code = state.read(cx).value();
if code.len() == 6 {
println!("Complete OTP: {}", code);
// Automatically submit when complete
this.verify_otp(&code, cx);
}
}
InputEvent::Focus => println!("OTP input focused"),
InputEvent::Blur => println!("OTP input lost focus"),
_ => {}
}
});
Programmatic Control
rust
// Set value programmatically
otp_state.update(cx, |state, cx| {
state.set_value("123456", window, cx);
});
// Toggle masking
otp_state.update(cx, |state, cx| {
state.set_masked(true, window, cx);
});
// Focus the input
otp_state.update(cx, |state, cx| {
state.focus(window, cx);
});
// Get current value
let current_value = otp_state.read(cx).value();
API Reference
OtpState
Method | Description |
---|---|
new(length, window, cx) | Create a new OTP state with specified length |
default_value(str) | Set initial value |
masked(bool) | Enable masked display (shows asterisks) |
set_value(str, window, cx) | Set OTP value programmatically |
value() | Get current OTP value |
set_masked(bool, window, cx) | Toggle masked display |
focus(window, cx) | Focus the OTP input |
focus_handle(cx) | Get focus handle |
OtpInput
Method | Description |
---|---|
new(state) | Create OTP input with state entity |
groups(n) | Set number of visual groups (default: 2) |
disabled(bool) | Set disabled state |
small() | Small size (6x6 px fields) |
large() | Large size (11x11 px fields) |
with_size(px) | Custom field size |
InputEvent
Event | Description |
---|---|
Change | Emitted when OTP is complete (all digits entered) |
Focus | Input received focus |
Blur | Input lost focus |
Examples
SMS Verification
rust
struct SmsVerification {
otp_state: Entity<OtpState>,
phone_number: String,
is_verifying: bool,
}
impl SmsVerification {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let otp_state = cx.new(|cx| OtpState::new(6, window, cx));
cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| {
if let InputEvent::Change = event {
let code = state.read(cx).value();
this.verify_sms_code(&code, cx);
}
});
Self {
otp_state,
phone_number: "+1234567890".to_string(),
is_verifying: false,
}
}
fn verify_sms_code(&mut self, code: &str, cx: &mut Context<Self>) {
self.is_verifying = true;
// API call to verify SMS code
println!("Verifying SMS code: {}", code);
cx.notify();
}
}
impl Render for SmsVerification {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.gap_4()
.child(format!("Enter the 6-digit code sent to {}", self.phone_number))
.child(OtpInput::new(&self.otp_state))
.when(self.is_verifying, |this| {
this.child("Verifying...")
})
}
}
Two-Factor Authentication
rust
struct TwoFactorAuth {
otp_state: Entity<OtpState>,
is_masked: bool,
}
impl TwoFactorAuth {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let otp_state = cx.new(|cx|
OtpState::new(6, window, cx)
.masked(true)
);
Self {
otp_state,
is_masked: true,
}
}
fn toggle_visibility(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.is_masked = !self.is_masked;
self.otp_state.update(cx, |state, cx| {
state.set_masked(self.is_masked, window, cx);
});
}
}
impl Render for TwoFactorAuth {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.gap_4()
.child("Enter your authenticator code")
.child(OtpInput::new(&self.otp_state))
.child(
Button::new("toggle-visibility")
.label(if self.is_masked { "Show" } else { "Hide" })
.on_click(cx.listener(Self::toggle_visibility))
)
}
}
PIN Entry
rust
struct PinEntry {
pin_state: Entity<OtpState>,
attempts: usize,
max_attempts: usize,
}
impl PinEntry {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let pin_state = cx.new(|cx|
OtpState::new(4, window, cx)
.masked(true)
);
cx.subscribe(&pin_state, |this, state, event: &InputEvent, cx| {
if let InputEvent::Change = event {
let pin = state.read(cx).value();
this.verify_pin(&pin, cx);
}
});
Self {
pin_state,
attempts: 0,
max_attempts: 3,
}
}
fn verify_pin(&mut self, pin: &str, cx: &mut Context<Self>) {
self.attempts += 1;
// Simulate PIN verification
if pin == "1234" {
println!("PIN verified successfully!");
} else {
println!("Incorrect PIN. Attempts: {}/{}", self.attempts, self.max_attempts);
// Clear PIN on incorrect attempt
self.pin_state.update(cx, |state, cx| {
state.set_value("", window, cx);
});
}
cx.notify();
}
}
impl Render for PinEntry {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let is_locked = self.attempts >= self.max_attempts;
v_flex()
.gap_4()
.child("Enter your 4-digit PIN")
.child(
OtpInput::new(&self.pin_state)
.groups(1)
.disabled(is_locked)
)
.when(is_locked, |this| {
this.child("Too many attempts. Please try again later.")
})
.when(self.attempts > 0 && !is_locked, |this| {
this.child(format!(
"Incorrect PIN. {} attempts remaining.",
self.max_attempts - self.attempts
))
})
}
}
Behavior
Input Handling
- Numeric Only: Accepts only digits (0-9)
- Auto-Focus: Automatically moves to next field when digit is entered
- Backspace: Removes current digit and moves to previous field
- Length Limit: Prevents input beyond specified length
- Auto-Complete: Emits
Change
event when all fields are filled
Visual Feedback
- Focus Indicator: Blue border and blinking cursor on active field
- Masking: Shows asterisk icons instead of numbers when enabled
- Grouping: Visual separation of fields into groups for better readability
- Disabled State: Grayed out appearance when disabled
Keyboard Navigation
- Arrow Keys: Navigate between fields
- Tab: Move to next focusable element
- Shift+Tab: Move to previous focusable element
- Backspace: Delete current digit and move backward
- Delete: Clear current field
Accessibility
- Keyboard Navigation: Full keyboard support for all interactions
- Focus Management: Clear focus indicators and logical tab order
- Screen Reader Support: Proper labeling and state announcements
- High Contrast: Clear visual distinctions in all themes
- Error States: Clear indication of validation failures
- Auto-completion: Automatic progression reduces cognitive load
Common Patterns
Auto-Submit on Complete
rust
cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| {
if let InputEvent::Change = event {
let code = state.read(cx).value();
if code.len() == 6 {
// Auto-submit when complete
this.submit_verification_code(&code, cx);
}
}
});
Clear on Focus
rust
cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| {
if let InputEvent::Focus = event {
// Clear previous value when user starts entering new code
state.update(cx, |state, cx| {
state.set_value("", window, cx);
});
}
});
Resend Code Timer
rust
struct OtpWithResend {
otp_state: Entity<OtpState>,
resend_timer: Option<Timer>,
can_resend: bool,
}
// Implementation would include timer logic for resend functionality