suiss/ui
  • Docs
  • Components
  • Patterns
  • Interactions
  • Charts
  • Blocks
GitHubGet started
DocsComponentsPatternsInteractionsChartsBlocks
Components82
  • Overview
  • Accordion
  • Alert
  • Alert Dialog
  • Alert Sheet
  • Amount Input
  • Aspect Ratio
  • Attachment
  • Avatar
  • Badge
  • Bottom Bar
  • Breadcrumb
  • Bubble
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Chip
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Dialog
  • Direction
  • Drawer
  • Dropdown Menu
  • Empty
  • FAB
  • Field
  • Fullscreen Menu
  • Haptics
  • Header
  • Hover Card
  • Icon
  • Input
  • Input Accessory Bar
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Keyboard Scroll View
  • Keypad
  • Label
  • Marker
  • Menubar
  • Message
  • Message Scroller
  • Native Select
  • Navigation Menu
  • Page Indicator
  • Pagination
  • Pin Pad
  • Popover
  • Progress
  • QR Scanner
  • Questionnaire
  • Radio Group
  • Resizable
  • Screen
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Signature Pad
  • Skeleton
  • Slider
  • Spinner
  • Switch
  • Tab Bar
  • Table
  • Tabs
  • Text
  • Textarea
  • Toast
  • Toggle
  • Toggle Group
  • Tooltip
  • Wheel Picker
  • Widget

suiss/ui · Component

Input OTP

A segmented field for entering one-time passcodes, one character per slot.

Input OTP is an open-source component from suiss/ui for React, React Native, SwiftUI and Jetpack Compose. The @esuiss/ui CLI copies its source into your project, so you own the code.

import {
  InputOTP,
  InputOTPGroup,
  InputOTPSeparator,
  InputOTPSlot,
} from "@/components/ui/input-otp"

export default function InputOtpDemo() {
  return (
    <InputOTP maxLength={6} aria-label="Verification code">
      <InputOTPGroup>
        <InputOTPSlot index={0} />
        <InputOTPSlot index={1} />
        <InputOTPSlot index={2} />
      </InputOTPGroup>
      <InputOTPSeparator />
      <InputOTPGroup>
        <InputOTPSlot index={3} />
        <InputOTPSlot index={4} />
        <InputOTPSlot index={5} />
      </InputOTPGroup>
    </InputOTP>
  )
}

Input OTP is built on the input-otp library. InputOTP renders a single hidden text input that holds the whole code, and displays it through InputOTPSlot elements, each showing the character at its index. Group slots with InputOTPGroup, and place an InputOTPSeparator between groups to split a code, for example into two sets of three.

Use it for verification and two-factor codes. For other short values, use Input.

Installation

CLI

$ pnpm dlx shadcn@latest add @esuiss/input-otp

Works in any project with a components.json. See Installation for the one-line registry setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add input-otp

Manual

  1. Install the dependencies and add the cn helper:
$ pnpm add @phosphor-icons/react input-otp
  1. Copy the source into components/ui/input-otp.tsx:
components/ui/input-otp.tsx
"use client"

import { MinusIcon } from "@phosphor-icons/react"
import { OTPInput, OTPInputContext } from "input-otp"
import * as React from "react"
import { cn } from "@/lib/utils"

function InputOTP({
  className,
  containerClassName,
  value,
  defaultValue,
  onChange,
  ...props
}: React.ComponentProps<typeof OTPInput> & {
  containerClassName?: string
  1. Update the import paths to match your project.

Usage

import { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } from "@/components/ui/input-otp"
<InputOTP maxLength={6} aria-label="Verification code">
  <InputOTPGroup>
    <InputOTPSlot index={0} />
    <InputOTPSlot index={1} />
    <InputOTPSlot index={2} />
  </InputOTPGroup>
  <InputOTPSeparator />
  <InputOTPGroup>
    <InputOTPSlot index={3} />
    <InputOTPSlot index={4} />
    <InputOTPSlot index={5} />
  </InputOTPGroup>
</InputOTP>

Set maxLength to the length of the code and render one InputOTPSlot for each position, with index values from 0. The value is uncontrolled with defaultValue, or controlled with value and onChange, which receives the code as a string. Restrict accepted characters with pattern, for example with REGEXP_ONLY_DIGITS_AND_CHARS from input-otp.

Typing fills the slots in order and stops at maxLength; Backspace removes the last character. Set aria-invalid on InputOTP to show the error style on every slot.

Examples

Filled

A code that starts filled from defaultValue.

import {
  InputOTP,
  InputOTPGroup,
  InputOTPSeparator,
  InputOTPSlot,
} from "@/components/ui/input-otp"

export default function InputOtpFilled() {
  return (
    <InputOTP
      maxLength={6}
      aria-label="Verification code"
      defaultValue="482913"
    >
      <InputOTPGroup>
        <InputOTPSlot index={0} />
        <InputOTPSlot index={1} />
        <InputOTPSlot index={2} />
      </InputOTPGroup>
      <InputOTPSeparator />
      <InputOTPGroup>
        <InputOTPSlot index={3} />
        <InputOTPSlot index={4} />
        <InputOTPSlot index={5} />
      </InputOTPGroup>
    </InputOTP>
  )
}

Single group

Six slots in a single group with no separator.

import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
} from "@/components/ui/input-otp"

export default function InputOtpSingleGroup() {
  return (
    <InputOTP maxLength={6} aria-label="Verification code">
      <InputOTPGroup>
        {Array.from({ length: 6 }, (_, index) => (
          <InputOTPSlot key={index} index={index} />
        ))}
      </InputOTPGroup>
    </InputOTP>
  )
}

Four digits

A four-digit code with maxLength set to 4.

import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
} from "@/components/ui/input-otp"

export default function InputOtpFourDigits() {
  return (
    <InputOTP maxLength={4} aria-label="Verification code">
      <InputOTPGroup>
        <InputOTPSlot index={0} />
        <InputOTPSlot index={1} />
        <InputOTPSlot index={2} />
        <InputOTPSlot index={3} />
      </InputOTPGroup>
    </InputOTP>
  )
}

Alphanumeric

A code that accepts letters and digits using the REGEXP_ONLY_DIGITS_AND_CHARS pattern.

import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp"
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSeparator,
  InputOTPSlot,
} from "@/components/ui/input-otp"

export default function InputOtpAlphanumeric() {
  return (
    <InputOTP
      maxLength={6}
      aria-label="Verification code"
      pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
    >
      <InputOTPGroup>
        <InputOTPSlot index={0} />
        <InputOTPSlot index={1} />
        <InputOTPSlot index={2} />
      </InputOTPGroup>
      <InputOTPSeparator />
      <InputOTPGroup>
        <InputOTPSlot index={3} />
        <InputOTPSlot index={4} />
        <InputOTPSlot index={5} />
      </InputOTPGroup>
    </InputOTP>
  )
}

Invalid

An invalid code with every slot outlined in the error color.

import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
} from "@/components/ui/input-otp"

export default function InputOtpInvalid() {
  return (
    <InputOTP
      maxLength={6}
      aria-label="Verification code"
      defaultValue="123456"
      aria-invalid
    >
      <InputOTPGroup>
        {Array.from({ length: 6 }, (_, index) => (
          <InputOTPSlot key={index} index={index} />
        ))}
      </InputOTPGroup>
    </InputOTP>
  )
}

Disabled

A disabled code that keeps its value and dims its slots and separator.

import {
  InputOTP,
  InputOTPGroup,
  InputOTPSeparator,
  InputOTPSlot,
} from "@/components/ui/input-otp"

export default function InputOtpDisabled() {
  return (
    <InputOTP
      maxLength={6}
      disabled
      aria-label="Verification code"
      defaultValue="123456"
    >
      <InputOTPGroup>
        <InputOTPSlot index={0} />
        <InputOTPSlot index={1} />
        <InputOTPSlot index={2} />
      </InputOTPGroup>
      <InputOTPSeparator />
      <InputOTPGroup>
        <InputOTPSlot index={3} />
        <InputOTPSlot index={4} />
        <InputOTPSlot index={5} />
      </InputOTPGroup>
    </InputOTP>
  )
}

API Reference

Anatomy4 parts
<InputOTP>
<InputOTPGroup>
<InputOTPSlot />
</InputOTPGroup>
<InputOTPSeparator />
</InputOTP>
01

InputOTP

The root that holds the code in a single hidden input.

PropTypeDefaultDescription
containerClassNamestring-Classes applied to the element that wraps the slots.

Also accepts the props of React.ComponentProps<typeof OTPInput>.

02

InputOTPGroup

Groups adjacent slots.

Also accepts the props of React.ComponentProps<"div">.

03

InputOTPSlot

Displays one character of the code and a caret when active.

PropTypeDefaultDescription
indexnumberRequiredThe position of the character this slot displays.

Also accepts the props of React.ComponentProps<"div">.

04

InputOTPSeparator

A dash shown between groups of slots.

Also accepts the props of React.ComponentProps<"div">.

Accessibility

  • The code is entered into one native text input, so it has the textbox role. Give it an accessible name with aria-label or an associated label.
  • Slots are visual only; assistive technology reads the value of the input.
  • InputOTPSeparator has the separator role.
  • When disabled, the input cannot be edited and the slots are dimmed.

Notes

  • Styling hooks: data-slot values input-otp, input-otp-group, input-otp-slot and input-otp-separator. The active slot gets data-active="true" and a primary border.
  • Slots react to the invalid and disabled states of the input through the data-input-otp-container attribute on its container.
  • The blinking caret uses the animate-caret-blink animation.
  • Uses the secondary, primary and destructive colors, and opacity-50 when disabled.
iOS
Inputotp on iOS in light modeInputotp on iOS in dark mode
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "@/components/ui/input-otp"

export function InputOtpExample() {
  return (
    <InputOTP length={6}>
      <InputOTPGroup>
        <InputOTPSlot index={0} />
        <InputOTPSlot index={1} />
        <InputOTPSlot index={2} />
      </InputOTPGroup>
      <InputOTPSeparator />
      <InputOTPGroup>
        <InputOTPSlot index={3} />
        <InputOTPSlot index={4} />
        <InputOTPSlot index={5} />
      </InputOTPGroup>
    </InputOTP>
  )
}
import EsuissUI
import SwiftUI

struct InputOTPDefaultDemo: View {
  @State private var text = ""

  var body: some View {
    InputOTP(text: $text, length: 6) {
      InputOTPGroup {
        InputOTPSlot(index: 0)
        InputOTPSlot(index: 1)
        InputOTPSlot(index: 2)
      }
      InputOTPSeparator()
      InputOTPGroup {
        InputOTPSlot(index: 3)
        InputOTPSlot(index: 4)
        InputOTPSlot(index: 5)
      }
    }
  }
}
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import com.esuiss.ui.components.InputOTP
import com.esuiss.ui.components.InputOTPGroup
import com.esuiss.ui.components.InputOTPSeparator
import com.esuiss.ui.components.InputOTPSlot

@Composable
fun InputOTPDefault() {
    var value by rememberSaveable { mutableStateOf("") }
    Column {
        InputOTP(value, { value = it }, maxLength = 6) {
            InputOTPGroup {
                InputOTPSlot(0)
                InputOTPSlot(1)
                InputOTPSlot(2)
            }
            InputOTPSeparator()
            InputOTPGroup {
                InputOTPSlot(3)
                InputOTPSlot(4)
                InputOTPSlot(5)
            }
        }
    }
}

InputOTP is backed by one hidden TextInput and shows each character in an InputOTPSlot. The active slot gets a border-primary border and a blinking caret. The hidden input uses the one-time-code autofill hints, so codes from messages can be filled in automatically, and pasting fills every slot.

Group slots with InputOTPGroup and place an InputOTPSeparator between groups for codes such as 123-456.

Installation

$ pnpm dlx shadcn@latest add @esuiss-rn/input-otp

See Installation for the components.json setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add input-otp
$ pnpm dlx @esuiss/ui@latest add input-otp --platform swift
$ pnpm dlx @esuiss/ui@latest add input-otp --platform kotlin

The CLI also adds what it builds on: Icon, Text.

Manual

  1. Install the dependencies:
npx expo install cn phosphor-react-native react-native-reanimated react-native-svg react-native-worklets
  1. Add the pieces it builds on: Icon, Text, and the primitives components/primitives/otp-field.tsx, components/primitives/use-render.tsx.

  2. Copy the source into components/ui/input-otp.tsx:

uniwind/components/ui/input-otp.tsx
import { cn } from "cn"
import { MinusIcon } from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import Animated, {
  cancelAnimation,
  ReduceMotion,
  useAnimatedStyle,
  useSharedValue,
  withDelay,
  withRepeat,
  withSequence,
  withTiming,
} from "react-native-reanimated"
import * as OTPFieldPrimitive from "@/components/primitives/otp-field"
import { resolveClassName } from "@/components/primitives/use-render"
  1. Update the import paths to match your project.
  1. Install the dependencies:
npx expo install cn phosphor-react-native react-native-reanimated react-native-svg react-native-worklets
  1. Add the pieces it builds on: Icon, Text, and the primitives components/primitives/otp-field.tsx, components/primitives/use-render.tsx.

  2. Copy the source into components/ui/input-otp.tsx:

nativewind/components/ui/input-otp.tsx
import { cn } from "cn"
import { MinusIcon } from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import Animated, {
  cancelAnimation,
  ReduceMotion,
  useAnimatedStyle,
  useSharedValue,
  withDelay,
  withRepeat,
  withSequence,
  withTiming,
} from "react-native-reanimated"
import * as OTPFieldPrimitive from "@/components/primitives/otp-field"
import { resolveClassName } from "@/components/primitives/use-render"
  1. Update the import paths to match your project.
  1. Install the dependencies:
npx expo install phosphor-react-native react-native-reanimated react-native-svg react-native-worklets
  1. Add the pieces it builds on: Icon, Text, and the primitives components/primitives/otp-field.tsx, components/primitives/use-render.tsx.

  2. Copy the source into components/ui/input-otp.tsx:

stylesheet/components/ui/input-otp.tsx
import { MinusIcon } from "phosphor-react-native"
import * as React from "react"
import { StyleSheet, type TextStyle, View, type ViewStyle } from "react-native"
import Animated, {
  cancelAnimation,
  ReduceMotion,
  useAnimatedStyle,
  useSharedValue,
  withDelay,
  withRepeat,
  withSequence,
  withTiming,
} from "react-native-reanimated"
import * as OTPFieldPrimitive from "@/components/primitives/otp-field"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"
  1. Update the import paths to match your project.
  1. Target iOS 17 or later.

  2. Add the Swift package to your app:

Package.swift
.package(url: "https://github.com/phosphor-icons/swift", from: "2.1.0")
  1. Copy the theme files every view uses:
swift/Theme/Theme.swift
import SwiftUI
import UIKit

struct RGBA: Sendable {
  let red: CGFloat
  let green: CGFloat
  let blue: CGFloat
  let alpha: CGFloat

  init(_ hex: UInt32, _ alpha: CGFloat = 1) {
    red = CGFloat((hex >> 16) & 0xFF) / 255
    green = CGFloat((hex >> 8) & 0xFF) / 255
    blue = CGFloat(hex & 0xFF) / 255
    self.alpha = alpha
  }
swift/Theme/Tokens.swift
import SwiftUI

public enum EsuissColor {
  public static let background = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x000000))
  public static let foreground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let card = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
  public static let cardForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let popover = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
  public static let popoverForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let primary = Color.adaptive(light: .init(0x0071E3), dark: .init(0x2997FF))
  public static let primaryForeground = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0xFFFFFF))
  public static let secondary = Color.adaptive(light: .init(0x000000, 0.04), dark: .init(0xFFFFFF, 0.1))
  public static let secondaryForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let muted = Color.adaptive(light: .init(0xF5F5F7), dark: .init(0x161617))
  public static let mutedForeground = Color.adaptive(light: .init(0x6E6E73), dark: .init(0xA1A1A6))
  public static let accent = Color.adaptive(light: .init(0x000000, 0.06), dark: .init(0xFFFFFF, 0.1))
  1. Copy the source and every file it uses:
swift/Components/InputOTP.swift
import PhosphorSwift
import SwiftUI

struct InputOTPContext {
  var characters: [Character]
  var length: Int
  var focused: Bool
}

struct InputOTPTone: Equatable {
  var danger = false
  var verified = false
  var dimmed = false
  var pops: [Int] = []
}
swift/Components/Field.swift
import SwiftUI

extension EnvironmentValues {
  @Entry public var isInvalid = false
  @Entry var fieldInset = false
  @Entry var fieldGroupInset = false
  @Entry var fieldSetInset = false
}

extension View {
  public func invalid(_ isInvalid: Bool = true) -> some View {
    environment(\.isInvalid, isInvalid)
  }
}

public enum FieldOrientation: CaseIterable, Sendable {
swift/Components/Separator.swift
import SwiftUI

public struct Separator: View {
  let orientation: Axis

  public init(_ orientation: Axis = .horizontal) {
    self.orientation = orientation
  }

  public var body: some View {
    Rectangle()
      .fill(EsuissColor.border)
      .frame(width: orientation == .vertical ? 1 : nil, height: orientation == .horizontal ? 1 : nil)
      .frame(maxWidth: orientation == .horizontal ? .infinity : nil, maxHeight: orientation == .vertical ? .infinity : nil)
      .accessibilityHidden(true)
  }
swift/Components/TextVariant.swift
import SwiftUI

public enum TextVariant: CaseIterable, Sendable {
  case `default`, h1, h2, h3, h4, p, blockquote, code, lead, large, small, muted

  var style: TextStyle {
    switch self {
    case .h1: .xl3
    case .h2: .xl2
    case .h3: .xl
    case .h4, .lead, .large: .lg
    case .code, .small, .muted: .sm
    default: .base
    }
  }
  1. Add the files to your app target.
  1. Set minSdk to 24 or higher and add the dependencies:
build.gradle.kts
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
implementation("com.adamglin:phosphor-icon-android:1.0.0")
  1. Copy the theme files every view uses:
kotlin/theme/Theme.kt
package com.esuiss.ui.theme

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.dropShadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.shadow.Shadow as DropShadow
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
kotlin/theme/Tokens.kt
package com.esuiss.ui.theme

import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Immutable
data class Colors(
    val background: Color,
    val foreground: Color,
    val card: Color,
    val cardForeground: Color,
    val popover: Color,
    val popoverForeground: Color,
    val primary: Color,
    val primaryForeground: Color,
  1. Copy the source and every file it uses:
kotlin/components/InputOTP.kt
package com.esuiss.ui.components

import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
kotlin/components/Content.kt
package com.esuiss.ui.components

import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

val LocalContentStyle = compositionLocalOf { TextStyle.Default }

val LocalContentColor = compositionLocalOf { Color.Unspecified }

val LocalIconSize = compositionLocalOf { 20.dp }
kotlin/components/Field.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.LayoutScopeMarker
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
kotlin/components/FormSupport.kt
package com.esuiss.ui.components

import android.provider.Settings
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
kotlin/components/Icon.kt
package com.esuiss.ui.components

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp
import com.esuiss.ui.theme.EsuissTheme

@Composable
fun Icon(
    imageVector: ImageVector,
kotlin/components/Label.kt
package com.esuiss.ui.components

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography
kotlin/components/Separator.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.theme.EsuissTheme

@Composable
fun Separator(
  1. Update the com.esuiss.ui package names to match your project.

Usage

import { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } from "@/components/ui/input-otp"
<InputOTP length={6}>
  <InputOTPGroup>
    <InputOTPSlot index={0} />
    <InputOTPSlot index={1} />
    <InputOTPSlot index={2} />
  </InputOTPGroup>
  <InputOTPSeparator />
  <InputOTPGroup>
    <InputOTPSlot index={3} />
    <InputOTPSlot index={4} />
    <InputOTPSlot index={5} />
  </InputOTPGroup>
</InputOTP>

Set length and render one InputOTPSlot per index. Use value with onValueChange for controlled state, or defaultValue. onValueComplete fires when every slot is filled.

validationType limits input to numeric (the default number pad), alpha, alphanumeric or none, and onValueInvalid reports rejected characters. Set mask to hide the characters.

Use InputOTPSlotClassContext to style every slot at once, or className on a single slot.

Examples

Controlled

Controlled with value and onValueChange.

iOS
Inputotp (controlled) on iOS in light modeInputotp (controlled) on iOS in dark mode
import * as React from "react"
import { View } from "react-native"
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp"
import { Text } from "@/components/ui/text"

function slotIndexes(length: number) {
  return Array.from({ length }, (_, index) => index)
}

function ControlledDemo() {
  const [value, setValue] = React.useState("")
  const [completed, setCompleted] = React.useState<string | null>(null)
  return (
    <View className="gap-3">
      <InputOTP
        length={4}
        value={value}
        onValueChange={setValue}
        onValueComplete={setCompleted}
      >
        <InputOTPGroup>
          {slotIndexes(4).map((index) => (
            <InputOTPSlot key={index} index={index} />
          ))}
        </InputOTPGroup>
      </InputOTP>
      <Text variant="muted">
        {completed ? `Submitted ${completed}` : "Enter the 4 digit code"}
      </Text>
    </View>
  )
}

export function InputOtpControlledExample() {
  return (
    <ControlledDemo />
  )
}
import EsuissUI
import SwiftUI

struct InputOTPControlledDemo: View {
  @State private var value = ""
  @State private var completed: String?

  var body: some View {
    VStack(alignment: .leading, spacing: 12) {
      InputOTP(text: $value, length: 4)
      Text(completed.map { "Submitted \($0)" } ?? "Enter the 4 digit code")
        .textVariant(.muted)
    }
    .onChange(of: value) { _, next in
      if next.count == 4 { completed = next }
    }
  }
}
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.InputOTP
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.TextVariant

@Composable
fun InputOTPControlled() {
    var value by rememberSaveable { mutableStateOf("") }
    var completed by rememberSaveable { mutableStateOf<String?>(null) }
    Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
        InputOTP(value, { value = it }, maxLength = 4, onComplete = { completed = it })
        Text(completed?.let { "Submitted $it" } ?: "Enter the 4 digit code", variant = TextVariant.Muted)
    }
}

Alphanumeric

Letters and numbers with validationType="alphanumeric".

iOS
Inputotp (alphanumeric) on iOS in light modeInputotp (alphanumeric) on iOS in dark mode
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp"

function slotIndexes(length: number) {
  return Array.from({ length }, (_, index) => index)
}

export function InputOtpAlphanumericExample() {
  return (
    <InputOTP
      length={6}
      validationType="alphanumeric"
      normalizeValue={(value) => value.toUpperCase()}
    >
      <InputOTPGroup>
        {slotIndexes(6).map((index) => (
          <InputOTPSlot key={index} index={index} />
        ))}
      </InputOTPGroup>
    </InputOTP>
  )
}
import EsuissUI
import SwiftUI

struct InputOTPAlphanumericDemo: View {
  @State private var value = ""

  var body: some View {
    InputOTP(
      text: Binding(get: { value }, set: { value = $0.uppercased() }),
      length: 6,
      allowed: .alphanumerics
    )
  }
}
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.text.input.KeyboardType
import com.esuiss.ui.components.InputOTP

@Composable
fun InputOTPAlphanumeric() {
    var value by rememberSaveable { mutableStateOf("") }
    Column {
        InputOTP(value, { value = it.uppercase() }, maxLength = 6, keyboardType = KeyboardType.Ascii)
    }
}

Masked

A four-digit code with hidden characters.

iOS
Inputotp (masked) on iOS in light modeInputotp (masked) on iOS in dark mode
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp"

function slotIndexes(length: number) {
  return Array.from({ length }, (_, index) => index)
}

export function InputOtpMaskedExample() {
  return (
    <InputOTP length={4} mask defaultValue="12">
      <InputOTPGroup>
        {slotIndexes(4).map((index) => (
          <InputOTPSlot key={index} index={index} />
        ))}
      </InputOTPGroup>
    </InputOTP>
  )
}

Disabled

A disabled field with a partial value.

iOS
Inputotp (disabled) on iOS in light modeInputotp (disabled) on iOS in dark mode
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp"

function slotIndexes(length: number) {
  return Array.from({ length }, (_, index) => index)
}

export function InputOtpDisabledExample() {
  return (
    <InputOTP length={6} disabled defaultValue="123">
      <InputOTPGroup>
        {slotIndexes(6).map((index) => (
          <InputOTPSlot key={index} index={index} />
        ))}
      </InputOTPGroup>
    </InputOTP>
  )
}
import EsuissUI
import SwiftUI

struct InputOTPDisabledExample: View {
  var body: some View {
    InputOTPDemo(length: 6, initial: "123")
      .disabled(true)
  }
}

struct InputOTPDemo: View {
  let length: Int

  @State private var text: String

  init(length: Int, initial: String = "") {
    self.length = length
    _text = State(initialValue: initial)
  }

  var body: some View {
    InputOTP(text: $text, length: length)
  }
}
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import com.esuiss.ui.components.InputOTP

@Composable
fun InputOTPDisabled() {
    var value by rememberSaveable { mutableStateOf("123") }
    Column {
        InputOTP(value, { value = it }, maxLength = 6, enabled = false)
    }
}

API Reference

01

InputOTP

Root that owns the hidden input and the value.

Also accepts the props of React.ComponentProps<typeof OTPFieldPrimitive.Root>.

02

InputOTPGroup

Row of adjacent slots.

Also accepts the props of React.ComponentProps<typeof View>.

03

InputOTPSlot

Box that shows one character.

Also accepts the props of React.ComponentProps<typeof OTPFieldPrimitive.Input>.

04

InputOTPSeparator

Dash between groups.

Also accepts the props of React.ComponentProps<typeof OTPFieldPrimitive.Separator>.

Accessibility

  • Screen readers interact with the hidden input, labelled "N character code" by default; pass inputProps with an accessibilityLabel to localize it.
  • The visual slots are hidden from VoiceOver and TalkBack.
  • disabled is exposed on the input.

Notes

  • The hidden input sets textContentType="oneTimeCode" for iOS and autoComplete="one-time-code" for Android autofill.
  • The caret blink respects the system Reduce Motion setting.
  • Uses bg-secondary, border-primary and bg-primary tokens; light and dark follow the system appearance.
NextItemPreviousInput Group

On this page

  1. 01Installation
    • CLI
    • Manual
  2. 02Usage
  3. 03Examples
    • Filled
    • Single group
    • Four digits
    • Alphanumeric
    • Invalid
    • Disabled
  4. 04API Reference
    • InputOTP
    • InputOTPGroup
    • InputOTPSlot
    • InputOTPSeparator
  5. 05Accessibility
  6. 06Notes
  7. 07Installation
    • Manual
  8. 08Usage
  9. 09Examples
    • Controlled
    • Alphanumeric
    • Masked
    • Disabled
  10. 10API Reference
    • InputOTP
    • InputOTPGroup
    • InputOTPSlot
    • InputOTPSeparator
  11. 11Accessibility
  12. 12Notes