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

A single-line text field for entering and editing short values.

Input 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 { Input } from "@/components/ui/input"

export default function InputDemo() {
  return (
    <div className="w-80">
      <Input type="text" placeholder="Enter your name" />
    </div>
  )
}

Input renders a native input element with a filled control style. It supports every native input type, including email, password, number, search and file, and adds a size prop for three heights.

Use it for short, free-form values. For multi-line text use a textarea, for icons, buttons or text attached to the field use InputGroup, and for one-time codes use InputOTP.

Installation

CLI

$ pnpm dlx shadcn@latest add @esuiss/input

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

Manual

  1. Install the dependencies and add the cn helper:
$ pnpm add @base-ui/react
  1. Copy the source into components/ui/input.tsx:
  1. Update the import paths to match your project.

Usage

import { Input } 
<div

Use defaultValue for an uncontrolled field, or value with onChange to control it. Give every input a visible label, for example with FieldLabel inside a Field, or set aria-label when a visible label is not possible. Choose a size that matches neighboring controls such as a Button.

Examples

Sizes

The sm, md and default sizes, each next to a button of matching height.

Types

Inputs with email, password, number and search types.

With value

An input that starts with a defaultValue.

File

A file input with an accessible name that accepts a chosen file.

Invalid

An input marked with aria-invalid, showing the error style.

Disabled

A disabled input that ignores typing.

API Reference

01

Input

The text field.

PropTypeDefaultDescription
size"sm" | "md" | "default""default"Sets the height and text size: default is the tallest, md is medium and sm is the most compact with a smaller radius.

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

Accessibility

  • Renders a native input, so the role follows its type: textbox by default, spinbutton for number and searchbox for search.
  • Needs an accessible name from an associated label or aria-label.
  • Set aria-invalid to communicate a validation error.
  • A disabled input cannot be focused or edited.

Notes

  • Styling hooks: data-slot="input" and data-size.
  • Uses the secondary, primary (caret), muted-foreground (placeholder) and destructive colors, and opacity-50 when disabled.
  • The native size attribute is replaced by the size prop.

Input wraps a TextInput through the field control primitive. It tracks its own focused, filled, invalid and disabled state and styles itself from that state: a focus border while editing, a destructive border and tint when invalid, and 50% opacity when disabled.

It comes in three sizes: default (44 pt), md and sm. Use the default size for forms on a full screen, and the smaller sizes inside dense layouts such as toolbars or sheets.

Installation

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

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

$ pnpm dlx @esuiss/ui@latest add input
$ pnpm dlx @esuiss/ui@latest add input --platform swift
NextInput Accessory BarPreviousIcon

On this page

  1. 01Installation
    • CLI
    • Manual
  2. 02Usage
  3. 03Examples
    • Sizes
    • Types
    • With value
    • File
    • Invalid
    • Disabled
  4. 04API Reference
    • Input
  5. 05Accessibility
  6. 06Notes
  7. 07Installation
    • Manual
  8. 08Usage
  9. 09Examples
    • Types
    • Sizes
    • With value
    • With label
    • Invalid
    • Controlled
    • Disabled
  10. 10API Reference
    • Input
  11. 11Accessibility
  12. 12Notes
from
"@/components/ui/input"
className
=
"w-80"
>
<Input type="text" placeholder="Enter your name" />
</div>
$ pnpm dlx @esuiss/ui@latest add input --platform kotlin

Manual

  1. Install the dependencies:
npx expo install cn
  1. Add the pieces it builds on: the primitives components/primitives/input.tsx, components/primitives/use-render.tsx.

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

uniwind/components/ui/input.tsx
import { cn } from "cn"
import * as InputPrimitive from "@/components/primitives/input"
import { resolveClassName } from "@/components/primitives/use-render"

type InputSize = "sm" | "md" | "default"

function Input({
  className,
  size = "default",
  ...props
}: React.ComponentProps<typeof InputPrimitive.Root> & {
  size?: InputSize
}) {
  return (
    <InputPrimitive.Root
      className={(state) =>
  1. Update the import paths to match your project.
  1. Install the dependencies:
npx expo install cn nativewind
  1. Add the pieces it builds on: the primitives components/primitives/input.tsx, components/primitives/use-render.tsx.

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

nativewind/components/ui/input.tsx
import { cn } from "cn"
import { cssInterop } from "nativewind"
import { TextInput } from "react-native"
import * as InputPrimitive from "@/components/primitives/input"
import { resolveClassName } from "@/components/primitives/use-render"

declare module "react-native" {
  interface TextInputProps {
    cursorColorClassName?: string
    selectionColorClassName?: string
  }
}

cssInterop(TextInput, {
  className: { target: "style", nativeStyleToProp: { textAlign: true } },
  placeholderClassName: {
  1. Update the import paths to match your project.
  1. Add the pieces it builds on: the primitives components/primitives/input.tsx, components/primitives/use-render.tsx.

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

  1. Update the import paths to match your project.
  1. Target iOS 17 or later.

  2. 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/Input.swift
import SwiftUI

public struct InputStyle: TextFieldStyle {
  public enum Size: CaseIterable, Sendable {
    case `default`, md, sm

    var height: CGFloat {
      switch self {
      case .default: 44
      case .md: 36
      case .sm: 32
      }
    }

    var horizontalPadding: CGFloat {
      self == .sm ? 10 : 12
swift/Components/Button.swift
import SwiftUI

public struct VariantButtonStyle: ButtonStyle {
  public enum Variant: CaseIterable, Sendable {
    case `default`, outline, secondary, tinted, ghost, plain, destructive, link
  }

  public enum Size: CaseIterable, Sendable {
    case `default`, xs, sm, lg, xl, icon, iconXs, iconSm, iconLg, iconXl

    var height: CGFloat {
      switch self {
      case .xs, .iconXs: 24
      case .sm, .iconSm: 32
      case .default, .icon: 36
      case .lg, .iconLg: 44
swift/Components/ButtonGroup.swift
import SwiftUI

extension EnvironmentValues {
  @Entry var buttonGroup: Axis? = nil
  @Entry var buttonGroupSize: CGSize = .zero
}

enum ButtonGroupSpace: Hashable {
  case group
}

struct ButtonGroupFrame {
  let bounds: Anchor<CGRect>
  let outlined: Bool
}
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")
  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/Input.kt
package com.esuiss.ui.components

import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
kotlin/components/ButtonGroup.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.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutable
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/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 { Input } from "@/components/ui/input"
<Input placeholder="Email" />

Pass any TextInput prop, such as placeholder, secureTextEntry, keyboardType or inputMode, to get the matching field type.

The value works controlled with value and onValueChange(value, details), or uncontrolled with defaultValue. Inside a Field, the input picks up the field's label, description, invalid and disabled state, so you usually set invalid on the Field instead of on the input.

className can be a function that receives the state (focused, invalid, disabled, filled, …) and returns classes.

Examples

Types

Text, email, password, number and search fields set through standard TextInput props.

iOS
Input (types) on iOS in light modeInput (types) on iOS in dark mode
import { View } from "react-native"
import { Input } from "@/components/ui/input"
import { Text } from "@/components/ui/text"

export function InputTypesExample() {
  return (
    <View className="gap-3">
      <Input placeholder="Text" />
      <Input
        placeholder="Email"
        keyboardType="email-address"
        autoCapitalize="none"
      />
      <Input placeholder="Password" secureTextEntry />
      <Input placeholder="Number" keyboardType="number-pad" />
      <Input placeholder="Search" inputMode="search" />
    </View>
  )
}
import EsuissUI
import SwiftUI

struct InputTypesDemo: View {
  @State private var text = ""
  @State private var email = ""
  @State private var password = ""
  @State private var number = ""
  @State private var search = ""

  var body: some View {
    VStack(alignment: .leading, spacing: 12) {
      TextField("Text", text: $text)
        .textFieldStyle(.input)
      TextField("Email", text: $email)
        .textFieldStyle(.input)
        .keyboardType(.emailAddress)
        .textInputAutocapitalization(.never)
      SecureField("Password", text: $password)
        .textFieldStyle(.input)
      TextField("Number", text: $number)
        .textFieldStyle(.input)
        .keyboardType(.numberPad)
      TextField("Search", text: $search)
        .textFieldStyle(.input)
        .submitLabel(.search)
    }
  }
}
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardOptions
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.Modifier
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Input
import com.esuiss.ui.components.Text

val InputEmailKeyboard = KeyboardOptions(
    capitalization = KeyboardCapitalization.None,
    keyboardType = KeyboardType.Email,
)

@Composable
fun InputTypes() {
    var text by rememberSaveable { mutableStateOf("") }
    var email by rememberSaveable { mutableStateOf("") }
    var password by rememberSaveable { mutableStateOf("") }
    var number by rememberSaveable { mutableStateOf("") }
    var search by rememberSaveable { mutableStateOf("") }
    Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp)) {
        Input(text, { text = it }, placeholder = "Text")
        Input(email, { email = it }, placeholder = "Email", keyboardOptions = InputEmailKeyboard)
        Input(
            password,
            { password = it },
            placeholder = "Password",
            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
            visualTransformation = PasswordVisualTransformation(),
        )
        Input(number, { number = it }, placeholder = "Number", keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number))
        Input(search, { search = it }, placeholder = "Search", keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search))
    }
}

Sizes

The sm, md and default sizes.

iOS
Input (sizes) on iOS in light modeInput (sizes) on iOS in dark mode
import { View } from "react-native"
import { Input } from "@/components/ui/input"

export function InputSizesExample() {
  return (
    <View className="gap-3">
      <Input size="sm" placeholder="Small" />
      <Input size="md" placeholder="Medium" />
      <Input placeholder="Default" />
    </View>
  )
}
import EsuissUI
import SwiftUI

struct InputSizesExample: View {
  var body: some View {
    VStack(alignment: .leading, spacing: 12) {
      InputDemo(placeholder: "Small", size: .sm)
      InputDemo(placeholder: "Medium", size: .md)
      InputDemo(placeholder: "Default")
    }
  }
}

struct InputDemo: View {
  let placeholder: LocalizedStringKey
  var size: InputStyle.Size = .default

  @State private var text: String

  init(placeholder: LocalizedStringKey, size: InputStyle.Size = .default, initial: String = "") {
    self.placeholder = placeholder
    self.size = size
    _text = State(initialValue: initial)
  }

  var body: some View {
    TextField(placeholder, text: $text)
      .textFieldStyle(.input(size: size))
  }
}
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
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.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Input
import com.esuiss.ui.components.InputSize

@Composable
fun InputSizes() {
    var small by rememberSaveable { mutableStateOf("") }
    var medium by rememberSaveable { mutableStateOf("") }
    var default by rememberSaveable { mutableStateOf("") }
    Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp)) {
        Input(small, { small = it }, size = InputSize.Sm, placeholder = "Small")
        Input(medium, { medium = it }, size = InputSize.Md, placeholder = "Medium")
        Input(default, { default = it }, placeholder = "Default")
    }
}

With value

An input that starts with a defaultValue.

iOS
Input (with value) on iOS in light modeInput (with value) on iOS in dark mode
import { Input } from "@/components/ui/input"

export function InputWithValueExample() {
  return (
    <Input placeholder="Email" defaultValue="[email protected]" />
  )
}
import EsuissUI
import SwiftUI

struct InputWithValueExample: View {
  var body: some View {
    InputDemo(placeholder: "Email", initial: "[email protected]")
  }
}

struct InputDemo: View {
  let placeholder: LocalizedStringKey
  var size: InputStyle.Size = .default

  @State private var text: String

  init(placeholder: LocalizedStringKey, size: InputStyle.Size = .default, initial: String = "") {
    self.placeholder = placeholder
    self.size = size
    _text = State(initialValue: initial)
  }

  var body: some View {
    TextField(placeholder, text: $text)
      .textFieldStyle(.input(size: size))
  }
}
import androidx.compose.foundation.layout.fillMaxWidth
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.Modifier
import com.esuiss.ui.components.Input

@Composable
fun InputWithValueExample() {
    InputSingle(initial = "[email protected]", placeholder = "Email")
}

@Composable
fun InputSingle(initial: String, placeholder: String, enabled: Boolean = true) {
    var value by rememberSaveable { mutableStateOf(initial) }
    Input(value, { value = it }, Modifier.fillMaxWidth(), enabled = enabled, placeholder = placeholder)
}

With label

An input inside a Field with a label and description.

iOS
Input (with label) on iOS in light modeInput (with label) on iOS in dark mode
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"

export function InputWithLabelExample() {
  return (
    <Field>
      <FieldLabel>Email</FieldLabel>
      <Input
        placeholder="[email protected]"
        keyboardType="email-address"
        autoCapitalize="none"
      />
      <FieldDescription>We will never share your email.</FieldDescription>
    </Field>
  )
}
import EsuissUI
import SwiftUI

struct InputWithLabelDemo: View {
  @State private var email = ""

  var body: some View {
    Field {
      FieldLabel("Email")
        .accessibilityHidden(true)
      TextField(text: $email, prompt: Text(verbatim: "[email protected]")) {
        Text("Email")
      }
        .textFieldStyle(.input)
        .keyboardType(.emailAddress)
        .textInputAutocapitalization(.never)
        .accessibilityLabel(Text("Email"))
      FieldDescription("We will never share your email.")
    }
  }
}
import androidx.compose.foundation.text.KeyboardOptions
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.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import com.esuiss.ui.components.Field
import com.esuiss.ui.components.FieldDescription
import com.esuiss.ui.components.FieldLabel
import com.esuiss.ui.components.Input

val InputEmailKeyboard = KeyboardOptions(
    capitalization = KeyboardCapitalization.None,
    keyboardType = KeyboardType.Email,
)

@Composable
fun InputWithLabel() {
    var value by rememberSaveable { mutableStateOf("") }
    Field {
        FieldLabel("Email")
        Input(value, { value = it }, placeholder = "[email protected]", keyboardOptions = InputEmailKeyboard)
        FieldDescription("We will never share your email.")
    }
}

Invalid

An input inside an invalid Field with an error message.

iOS
Input (invalid) on iOS in light modeInput (invalid) on iOS in dark mode
import { Field, FieldError, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"

export function InputInvalidExample() {
  return (
    <Field invalid>
      <FieldLabel>Username</FieldLabel>
      <Input defaultValue="x" />
      <FieldError>Username must be at least 3 characters.</FieldError>
    </Field>
  )
}
import EsuissUI
import SwiftUI

struct InputInvalidDemo: View {
  @State private var username = "x"

  var body: some View {
    Field(invalid: true) {
      FieldLabel("Username")
        .accessibilityHidden(true)
      TextField("", text: $username)
        .textFieldStyle(.input)
        .accessibilityLabel(Text("Username"))
      FieldError("Username must be at least 3 characters.")
    }
  }
}
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.Field
import com.esuiss.ui.components.FieldError
import com.esuiss.ui.components.FieldLabel
import com.esuiss.ui.components.Input

@Composable
fun InputInvalid() {
    var value by rememberSaveable { mutableStateOf("x") }
    Field(invalid = true) {
        FieldLabel("Username")
        Input(value, { value = it })
        FieldError("Username must be at least 3 characters.")
    }
}

Controlled

A controlled input that mirrors its value below.

iOS
Input (controlled) on iOS in light modeInput (controlled) on iOS in dark mode
import * as React from "react"
import { View } from "react-native"
import { Input } from "@/components/ui/input"
import { Text } from "@/components/ui/text"

function ControlledDemo() {
  const [value, setValue] = React.useState("")
  return (
    <View className="gap-2">
      <Input
        placeholder="Lowercase only"
        autoCapitalize="none"
        value={value}
        onValueChange={(next) => setValue(next.toLowerCase())}
      />
      <Text variant="muted">Value: {value}</Text>
    </View>
  )
}

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

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

  var body: some View {
    VStack(alignment: .leading, spacing: 8) {
      TextField(
        "Lowercase only",
        text: Binding(get: { value }, set: { value = $0.lowercased() })
      )
      .textFieldStyle(.input)
      .textInputAutocapitalization(.never)
      Text(verbatim: "Value: \(value)")
        .textVariant(.muted)
    }
  }
}
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardOptions
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.Modifier
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Input
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.TextVariant

@Composable
fun InputControlled() {
    var value by rememberSaveable { mutableStateOf("") }
    Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
        Input(
            value,
            { value = it.lowercase() },
            placeholder = "Lowercase only",
            keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.None),
        )
        Text("Value: $value", variant = TextVariant.Muted)
    }
}

Disabled

A disabled input.

iOS
Input (disabled) on iOS in light modeInput (disabled) on iOS in dark mode
import { Input } from "@/components/ui/input"

export function InputDisabledExample() {
  return (
    <Input placeholder="Disabled" />
  )
}
import EsuissUI
import SwiftUI

struct InputDisabledExample: View {
  var body: some View {
    InputDemo(placeholder: "Disabled")
      .disabled(true)
  }
}

struct InputDemo: View {
  let placeholder: LocalizedStringKey
  var size: InputStyle.Size = .default

  @State private var text: String

  init(placeholder: LocalizedStringKey, size: InputStyle.Size = .default, initial: String = "") {
    self.placeholder = placeholder
    self.size = size
    _text = State(initialValue: initial)
  }

  var body: some View {
    TextField(placeholder, text: $text)
      .textFieldStyle(.input(size: size))
  }
}
import androidx.compose.foundation.layout.fillMaxWidth
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.Modifier
import com.esuiss.ui.components.Input

@Composable
fun InputDisabledExample() {
    InputSingle(initial = "", placeholder = "Disabled", enabled = false)
}

@Composable
fun InputSingle(initial: String, placeholder: String, enabled: Boolean = true) {
    var value by rememberSaveable { mutableStateOf(initial) }
    Input(value, { value = it }, Modifier.fillMaxWidth(), enabled = enabled, placeholder = placeholder)
}

API Reference

01

Input

PropTypeDefaultDescription
sizeInputSize"default"Height and text size: default, md or sm.

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

Accessibility

  • Inside a Field, the input is linked to the field label through accessibilityLabelledBy and gets the label as accessibilityLabel and the description as accessibilityHint.
  • disabled is set in accessibilityState and makes the input non-editable.
  • Outside a Field, give the input an accessibilityLabel; a placeholder alone is not a label.

Notes

  • Uses the bg-secondary, border-ring and border-destructive tokens, with opacity-50 when disabled.
  • In Uniwind and NativeWind, a focused field also draws the ring-3 ring-ring/50 focus ring.
  • Placeholder, cursor and selection colors come from the muted-foreground and primary tokens.
  • Light and dark colors follow the system appearance.
iOS
Input on iOS in light modeInput on iOS in dark mode
import { Input } from "@/components/ui/input"

export function InputExample() {
  return (
    <Input placeholder="Email" />
  )
}
import EsuissUI
import SwiftUI

struct InputExample: View {
  var body: some View {
    InputDemo(placeholder: "Email")
  }
}

struct InputDemo: View {
  let placeholder: LocalizedStringKey
  var size: InputStyle.Size = .default

  @State private var text: String

  init(placeholder: LocalizedStringKey, size: InputStyle.Size = .default, initial: String = "") {
    self.placeholder = placeholder
    self.size = size
    _text = State(initialValue: initial)
  }

  var body: some View {
    TextField(placeholder, text: $text)
      .textFieldStyle(.input(size: size))
  }
}
import androidx.compose.foundation.layout.fillMaxWidth
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.Modifier
import com.esuiss.ui.components.Input

@Composable
fun InputExample() {
    InputSingle(initial = "", placeholder = "Email")
}

@Composable
fun InputSingle(initial: String, placeholder: String, enabled: Boolean = true) {
    var value by rememberSaveable { mutableStateOf(initial) }
    Input(value, { value = it }, Modifier.fillMaxWidth(), enabled = enabled, placeholder = placeholder)
}
components/ui/input.tsx
import { Input as InputPrimitive } from "@base-ui/react/input"
import type * as React from "react"
import { cn } from "@/lib/utils"

function Input({
  className,
  type,
  size = "default",
  ...props
}: Omit<React.ComponentProps<"input">, "size"> & {
  size?: "sm" | "md" | "default"
}) {
  return (
    <InputPrimitive
      type={type}
      data-slot="input"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"

export default function InputSizes() {
  return (
    <div className="w-80">
      <div className="flex flex-col gap-3">
        <div className="flex items-center gap-2">
          <Input type="text" size="sm" placeholder="Small" />
          <Button size="sm">Save</Button>
        </div>
        <div className="flex items-center gap-2">
          <Input type="text" size="md" placeholder="Medium" />
          <Button>Save</Button>
        </div>
        <div className="flex items-center gap-2">
          <Input type="text" size="default" placeholder="Default" />
          <Button size="lg">Save</Button>
        </div>
      </div>
    </div>
  )
}
import { Input } from "@/components/ui/input"

export default function InputTypes() {
  return (
    <div className="w-80">
      <div className="flex flex-col gap-3">
        <Input type="email" placeholder="[email protected]" />
        <Input type="password" placeholder="Password" />
        <Input type="number" placeholder="Quantity" />
        <Input type="search" placeholder="Search..." />
      </div>
    </div>
  )
}
import { Input } from "@/components/ui/input"

export default function InputWithValue() {
  return (
    <div className="w-80">
      <Input
        type="text"
        placeholder="Enter your name"
        defaultValue="Jane Doe"
      />
    </div>
  )
}
import { Input } from "@/components/ui/input"

export default function InputFile() {
  return (
    <div className="w-80">
      <Input type="file" placeholder={undefined} aria-label="Menu" />
    </div>
  )
}
import { Input } from "@/components/ui/input"

export default function InputInvalid() {
  return (
    <div className="w-80">
      <Input
        type="email"
        placeholder="Enter your name"
        defaultValue="jane@"
        aria-invalid
      />
    </div>
  )
}
import { Input } from "@/components/ui/input"

export default function InputDisabled() {
  return (
    <div className="w-80">
      <Input type="text" placeholder="Enter your name" disabled />
    </div>
  )
}
stylesheet/components/ui/input.tsx
import { StyleSheet } from "react-native"
import * as InputPrimitive from "@/components/primitives/input"
import { fontSize, radius, useTheme } from "@/components/ui/theme"

type InputSize = "sm" | "md" | "default"

function withAlpha(color: string, alpha: number) {
  if (!color.startsWith("#") || color.length !== 7) return color
  const r = Number.parseInt(color.slice(1, 3), 16)
  const g = Number.parseInt(color.slice(3, 5), 16)
  const b = Number.parseInt(color.slice(5, 7), 16)
  return `rgba(${r}, ${g}, ${b}, ${alpha})`
}

const styles = StyleSheet.create({
  root: {