suiss/uim · React Native Component
Textarea
A multi-line text field for longer input, with focus, invalid and disabled states.
Textarea is an open-source mobile component from suiss/uim for React Native and Expo, with SwiftUI and Jetpack Compose versions built from the same design tokens. The @esuiss/uim CLI copies its source into your project, so you own the code.


import { Textarea } from "@/components/ui/textarea"
export function TextareaExample() {
return (
<Textarea placeholder="Type your message here." />
)
}import EsuissUI
import SwiftUI
struct TextareaDemo: View {
let placeholder: LocalizedStringKey
@State private var text: String
init(initial: String = "", placeholder: LocalizedStringKey = "Type your message here.") {
self.placeholder = placeholder
_text = State(initialValue: initial)
}
var body: some View {
TextField(placeholder, text: $text, axis: .vertical)
.textFieldStyle(.textarea)
.frame(maxWidth: .infinity)
}
}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.Textarea
@Composable
fun TextareaDemo(
initial: String = "",
placeholder: String = "Type your message here.",
enabled: Boolean = true,
) {
var value by rememberSaveable { mutableStateOf(initial) }
Textarea(value, { value = it }, Modifier.fillMaxWidth(), enabled = enabled, placeholder = placeholder)
}Textarea is the multi-line version of Input. It wraps a TextInput through the same field control primitive, defaults to multiline with numberOfLines={8}, aligns text to the top and has a minimum height. It shows a focus border while editing, a danger border and tint when invalid, and muted colors when disabled.
Installation
$ pnpm dlx @esuiss/uim@latest add textarea$ pnpm dlx @esuiss/uim@latest add textarea --platform swift$ pnpm dlx @esuiss/uim@latest add textarea --platform kotlinManual
- Install the dependencies:
npx expo install cn-
Add the pieces it builds on: the primitives
components/primitives/input.tsx,components/primitives/use-render.tsx. -
Copy the source into
components/ui/textarea.tsx:
import { cn } from "cn"
import * as InputPrimitive from "@/components/primitives/input"
import { resolveClassName } from "@/components/primitives/use-render"
function Textarea({
className,
multiline = true,
numberOfLines = 8,
...props
}: React.ComponentProps<typeof InputPrimitive.Root>) {
return (
<InputPrimitive.Root
className={(state) =>
cn(
"min-h-16 w-full flex-row rounded-xl border border-transparent bg-control px-3 py-3 text-base text-label",
state.focused && "border-focus",- Update the import paths to match your project.
- Install the dependencies:
npx expo install cn nativewind-
Add the pieces it builds on: the primitives
components/primitives/input.tsx,components/primitives/use-render.tsx. -
Copy the source into
components/ui/textarea.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: {- Update the import paths to match your project.
-
Add the pieces it builds on: the primitives
components/primitives/input.tsx,components/primitives/use-render.tsx. -
Copy the source into
components/ui/textarea.tsx:
import { StyleSheet } from "react-native"
import * as InputPrimitive from "@/components/primitives/input"
import { fontSize, radius, useTheme } from "@/components/ui/theme"
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: {
...fontSize.base,
minHeight: 64,- Update the import paths to match your project.
-
Target iOS 17 or later.
-
Add the Swift package to your app:
.package(url: "https://github.com/phosphor-icons/swift", from: "2.1.0")- Copy the theme files every view uses:
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
}import SwiftUI
extension Color {
public static let surface = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x000000))
public static let surfaceSecondary = Color.adaptive(light: .init(0xF5F5F7), dark: .init(0x161617))
public static let surfaceTertiary = Color.adaptive(light: .init(0xE8E8ED), dark: .init(0x2C2C2E))
public static let surfaceRaised = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
public static let label = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
public static let labelSecondary = Color.adaptive(light: .init(0x6E6E73), dark: .init(0xA1A1A6))
public static let labelTertiary = Color.adaptive(light: .init(0x86868B), dark: .init(0x86868B))
public static let labelQuaternary = Color.adaptive(light: .init(0xD2D2D7), dark: .init(0x424245))
public static let control = Color.adaptive(light: .init(0x000000, 0.04), dark: .init(0xFFFFFF, 0.1))
public static let controlPressed = Color.adaptive(light: .init(0x000000, 0.12), dark: .init(0xFFFFFF, 0.22))
public static let controlSelected = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
public static let controlDisabled = Color.adaptive(light: .init(0x000000, 0.03), dark: .init(0xFFFFFF, 0.05))
public static let onControlSelected = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x000000))- Copy the source and every file it uses:
import SwiftUI
public struct TextareaStyle: TextFieldStyle {
public init() {}
public func _body(configuration: TextField<Self._Label>) -> some View {
configuration
.lineLimit(1...8)
.modifier(InputField(
radius: Radius.xl,
horizontalPadding: 12,
verticalPadding: 12,
minHeight: 64,
textStyle: .base,
alignment: .topLeading
))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: 44import 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
}import PhosphorSwift
import SwiftUI
extension Shadow {
static let control = Shadow(color: .black.opacity(0.08), radius: 0.5, y: 0.5)
static let thumb = Shadow(color: .black.opacity(0.12), radius: 1.5, y: 1)
}
struct SelectionIndicator<Mark: View>: View {
let checked: Bool
let pressed: Bool
let cornerRadius: CGFloat
let mark: Mark
@Environment(\.isEnabled) private var isEnabled
@Environment(\.isInvalid) private var invalidimport 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 {import SwiftUI
extension EnvironmentValues {
@Entry public var hapticsEnabled = true
}
extension View {
public func haptics(_ enabled: Bool) -> some View {
environment(\.hapticsEnabled, enabled)
}
func hapticFeedback<Trigger: Equatable>(_ feedback: SensoryFeedback, trigger: Trigger) -> some View {
modifier(HapticFeedbackModifier(feedback: feedback, trigger: trigger) { _, _ in true })
}
func hapticFeedback<Trigger: Equatable>(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 : 12import SwiftUI
public struct Separator: View {
let orientation: Axis
public init(_ orientation: Axis = .horizontal) {
self.orientation = orientation
}
public var body: some View {
Rectangle()
.fill(Color.separator)
.frame(width: orientation == .vertical ? 1 : nil, height: orientation == .horizontal ? 1 : nil)
.frame(maxWidth: orientation == .horizontal ? .infinity : nil, maxHeight: orientation == .vertical ? .infinity : nil)
.accessibilityHidden(true)
}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
}
}- Add the files to your app target.
- Set
minSdkto 24 or higher and add the dependencies:
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")- Copy the theme files every view uses:
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.DpOffsetpackage 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 surface: Color,
val surfaceSecondary: Color,
val surfaceTertiary: Color,
val surfaceRaised: Color,
val label: Color,
val labelSecondary: Color,
val labelTertiary: Color,
val labelQuaternary: Color,- Copy the source and every file it uses:
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.heightIn
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.CompositionLocalProviderpackage 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 }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.BasicTextpackage 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.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Sizepackage 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.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography
@Composablepackage 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(- Update the
com.esuiss.uipackage names to match your project.
Usage
import { Textarea } from "@/components/ui/textarea"<Textarea placeholder="Type your message here." />Use it for comments, messages and other free-form text. Use Input for single-line values.
The value works controlled with value and onValueChange(value, details), or uncontrolled with defaultValue. Inside a Field, the textarea picks up the field's label, description, invalid and disabled state.
Override numberOfLines or set a height through className to change the visible size.
Examples
With value
A textarea that starts with a defaultValue.


import { Textarea } from "@/components/ui/textarea"
export function TextareaWithValueExample() {
return (
<Textarea placeholder="Type your message here." defaultValue="The quick brown fox jumps over the lazy dog. This text spans a few lines to show how the textarea handles longer content." />
)
}import EsuissUI
import SwiftUI
struct TextareaWithValueExample: View {
var body: some View {
TextareaDemo(initial: "The quick brown fox jumps over the lazy dog. This text spans a few lines to show how the textarea handles longer content.")
}
}
struct TextareaDemo: View {
let placeholder: LocalizedStringKey
@State private var text: String
init(initial: String = "", placeholder: LocalizedStringKey = "Type your message here.") {
self.placeholder = placeholder
_text = State(initialValue: initial)
}
var body: some View {
TextField(placeholder, text: $text, axis: .vertical)
.textFieldStyle(.textarea)
.frame(maxWidth: .infinity)
}
}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.Textarea
@Composable
fun TextareaWithValueExample() {
TextareaDemo(
initial = "The quick brown fox jumps over the lazy dog. This text spans a few lines to show how the textarea handles longer content.",
)
}
@Composable
fun TextareaDemo(
initial: String = "",
placeholder: String = "Type your message here.",
enabled: Boolean = true,
) {
var value by rememberSaveable { mutableStateOf(initial) }
Textarea(value, { value = it }, Modifier.fillMaxWidth(), enabled = enabled, placeholder = placeholder)
}With label
A textarea inside a Field with a label and description.


import { Field, FieldDescription, FieldLabel } from "@/components/ui/field"
import { Textarea } from "@/components/ui/textarea"
export function TextareaWithLabelExample() {
return (
<Field>
<FieldLabel>Your message</FieldLabel>
<Textarea placeholder="Type your message here." />
<FieldDescription>
Your message will be copied to the team.
</FieldDescription>
</Field>
)
}import EsuissUI
import SwiftUI
struct TextareaWithLabelExample: View {
var body: some View {
Field {
FieldLabel("Your message")
TextareaDemo()
FieldDescription("Your message will be copied to the team.")
}
}
}
struct TextareaDemo: View {
let placeholder: LocalizedStringKey
@State private var text: String
init(initial: String = "", placeholder: LocalizedStringKey = "Type your message here.") {
self.placeholder = placeholder
_text = State(initialValue: initial)
}
var body: some View {
TextField(placeholder, text: $text, axis: .vertical)
.textFieldStyle(.textarea)
.frame(maxWidth: .infinity)
}
}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.Field
import com.esuiss.ui.components.FieldDescription
import com.esuiss.ui.components.FieldLabel
import com.esuiss.ui.components.Textarea
@Composable
fun TextareaDemo(
initial: String = "",
placeholder: String = "Type your message here.",
enabled: Boolean = true,
) {
var value by rememberSaveable { mutableStateOf(initial) }
Textarea(value, { value = it }, Modifier.fillMaxWidth(), enabled = enabled, placeholder = placeholder)
}
@Composable
fun TextareaWithLabel() {
Field(Modifier.fillMaxWidth()) {
FieldLabel("Your message")
TextareaDemo()
FieldDescription("Your message will be copied to the team.")
}
}Disabled
A disabled textarea.


import { Textarea } from "@/components/ui/textarea"
export function TextareaDisabledExample() {
return (
<Textarea placeholder="Disabled" />
)
}import EsuissUI
import SwiftUI
struct TextareaDisabledExample: View {
var body: some View {
TextareaDemo(placeholder: "Disabled")
.disabled(true)
}
}
struct TextareaDemo: View {
let placeholder: LocalizedStringKey
@State private var text: String
init(initial: String = "", placeholder: LocalizedStringKey = "Type your message here.") {
self.placeholder = placeholder
_text = State(initialValue: initial)
}
var body: some View {
TextField(placeholder, text: $text, axis: .vertical)
.textFieldStyle(.textarea)
.frame(maxWidth: .infinity)
}
}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.Textarea
@Composable
fun TextareaDisabledExample() {
TextareaDemo(placeholder = "Disabled", enabled = false)
}
@Composable
fun TextareaDemo(
initial: String = "",
placeholder: String = "Type your message here.",
enabled: Boolean = true,
) {
var value by rememberSaveable { mutableStateOf(initial) }
Textarea(value, { value = it }, Modifier.fillMaxWidth(), enabled = enabled, placeholder = placeholder)
}Invalid
A textarea inside an invalid Field with an error message.


import { Field, FieldError, FieldLabel } from "@/components/ui/field"
import { Textarea } from "@/components/ui/textarea"
export function TextareaInvalidExample() {
return (
<Field invalid>
<FieldLabel>Message</FieldLabel>
<Textarea defaultValue="Too short" />
<FieldError>Message must be at least 20 characters.</FieldError>
</Field>
)
}import EsuissUI
import SwiftUI
struct TextareaInvalidExample: View {
var body: some View {
Field(invalid: true) {
FieldLabel("Message")
TextareaDemo(initial: "Too short")
FieldError("Message must be at least 20 characters.")
}
}
}
struct TextareaDemo: View {
let placeholder: LocalizedStringKey
@State private var text: String
init(initial: String = "", placeholder: LocalizedStringKey = "Type your message here.") {
self.placeholder = placeholder
_text = State(initialValue: initial)
}
var body: some View {
TextField(placeholder, text: $text, axis: .vertical)
.textFieldStyle(.textarea)
.frame(maxWidth: .infinity)
}
}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.Field
import com.esuiss.ui.components.FieldError
import com.esuiss.ui.components.FieldLabel
import com.esuiss.ui.components.Textarea
@Composable
fun TextareaDemo(
initial: String = "",
placeholder: String = "Type your message here.",
enabled: Boolean = true,
) {
var value by rememberSaveable { mutableStateOf(initial) }
Textarea(value, { value = it }, Modifier.fillMaxWidth(), enabled = enabled, placeholder = placeholder)
}
@Composable
fun TextareaInvalid() {
Field(Modifier.fillMaxWidth(), invalid = true) {
FieldLabel("Message")
TextareaDemo(initial = "Too short")
FieldError("Message must be at least 20 characters.")
}
}API Reference
Textarea
React.ComponentProps<typeof InputPrimitive.Root>Accessibility
- Inside a
Field, the textarea gets the field label asaccessibilityLabeland the description asaccessibilityHint. disabledis set inaccessibilityStateand makes the textarea non-editable.- Outside a
Field, give it anaccessibilityLabel.
Notes
- Uses the
bg-control,border-focus,border-dangerandbg-control-disabledtokens. - Placeholder, cursor and selection colors come from the
label-secondaryandaccenttokens. - Light and dark colors follow the system appearance.