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

Textarea

A multi-line text field that grows with its content, with an optional resize handle.

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

export default function TextareaDemo() {
  return (
    <div className="w-80">
      <Textarea placeholder="Type your message here." />
    </div>
  )
}

Textarea renders a native textarea styled to match the other form controls. By default it uses field-sizing: content, so it grows as people type, starting at a minimum height of 4rem.

Set resizable to give it a fixed height and a custom handle that people drag to resize it vertically. Use Textarea for free-form text such as messages or notes; for a single line, use Input.

Installation

CLI

$ pnpm dlx shadcn@latest add @esuiss/textarea

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 textarea

Manual

  1. Add the cn helper.

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

components/ui/textarea.tsx
"use client"

import type * as React from "react"
import { cn } from "@/lib/utils"

function TextareaHandle({ placement }: { placement: "inside" | "outside" }) {
  function startResize(event: React.PointerEvent<HTMLSpanElement>) {
    const handle = event.currentTarget
    const textarea = handle.parentElement?.querySelector<HTMLTextAreaElement>(
      "[data-slot=textarea]"
    )
    if (!textarea || event.button !== 0) return
    event.preventDefault()
    handle.setPointerCapture(event.pointerId)
    handle.dataset.dragging = ""
    const startY = event.clientY
  1. Update the import paths to match your project.

Usage

import { Textarea } from "@/components/ui/textarea"
<div className="w-80">
  <Textarea placeholder="Type your message here." />
</div>

Textarea passes its props to the native element, so use value with onChange to control it, or defaultValue for an uncontrolled field. Pair it with a Label through htmlFor and id, and link hint text with aria-describedby.

With resizable, the textarea and its handle are wrapped in a relative container. Double-click the handle to reset the height.

Examples

With value

Starts with a default value.

import { Textarea } from "@/components/ui/textarea"

export default function TextareaWithValue() {
  return (
    <div className="w-80">
      <Textarea
        placeholder="Type your message here."
        defaultValue="Thanks for the quick turnaround. The new layout looks great on mobile."
      />
    </div>
  )
}

With label

Names the textarea with a Label.

import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"

export default function TextareaWithLabel() {
  return (
    <div className="w-80">
      <div className="flex flex-col gap-2">
        <Label htmlFor="feedback">Feedback</Label>
        <Textarea id="feedback" placeholder="Tell us what you think." />
      </div>
    </div>
  )
}

Resizable

Adds a resize handle inside the bottom corner of the field.

import * as React from "react"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"

function ResizableExample({
  resizable,
  ...props
}: React.ComponentProps<typeof Textarea>) {
  const ref = React.useRef<HTMLTextAreaElement>(null)
  const [rows, setRows] = React.useState(0)

  React.useEffect(() => {
    const area = ref.current
    if (!area) return
    const measure = () => {
      const style = getComputedStyle(area)
      const padding =
        Number.parseFloat(style.paddingTop) +
        Number.parseFloat(style.paddingBottom)
      const line = Number.parseFloat(style.lineHeight)
      setRows(Math.floor((area.clientHeight - padding) / line))
    }
    measure()
    const observer = new ResizeObserver(measure)
    observer.observe(area)
    return () => observer.disconnect()
  }, [])

  return (
    <div className="flex flex-col gap-2">
      <div className="flex items-baseline justify-between">
        <Label htmlFor={`campaign-${resizable}`}>Campaign copy</Label>
        <span className="text-xs text-muted-foreground tabular-nums">
          {rows} rows
        </span>
      </div>
      <Textarea
        {...props}
        ref={ref}
        id={`campaign-${resizable}`}
        resizable={resizable}
        aria-describedby={`campaign-${resizable}-hint`}
      />
      <p
        id={`campaign-${resizable}-hint`}
        className="text-xs text-muted-foreground"
      >
        Drag the bottom corner to resize. Double-click it to reset.
      </p>
    </div>
  )
}

const campaign =
  "Weekend sale on every grill. 15% off Saturday and Sunday from 12:00 to 16:00, including delivery orders."

export default function TextareaResizable() {
  return (
    <div className="w-80">
      <ResizableExample
        placeholder="Type your message here."
        resizable="inside"
        rows={5}
        defaultValue={campaign}
      />
    </div>
  )
}

Resizable outside

Places the resize handle just outside the bottom corner of the field.

import * as React from "react"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"

function ResizableExample({
  resizable,
  ...props
}: React.ComponentProps<typeof Textarea>) {
  const ref = React.useRef<HTMLTextAreaElement>(null)
  const [rows, setRows] = React.useState(0)

  React.useEffect(() => {
    const area = ref.current
    if (!area) return
    const measure = () => {
      const style = getComputedStyle(area)
      const padding =
        Number.parseFloat(style.paddingTop) +
        Number.parseFloat(style.paddingBottom)
      const line = Number.parseFloat(style.lineHeight)
      setRows(Math.floor((area.clientHeight - padding) / line))
    }
    measure()
    const observer = new ResizeObserver(measure)
    observer.observe(area)
    return () => observer.disconnect()
  }, [])

  return (
    <div className="flex flex-col gap-2">
      <div className="flex items-baseline justify-between">
        <Label htmlFor={`campaign-${resizable}`}>Campaign copy</Label>
        <span className="text-xs text-muted-foreground tabular-nums">
          {rows} rows
        </span>
      </div>
      <Textarea
        {...props}
        ref={ref}
        id={`campaign-${resizable}`}
        resizable={resizable}
        aria-describedby={`campaign-${resizable}-hint`}
      />
      <p
        id={`campaign-${resizable}-hint`}
        className="text-xs text-muted-foreground"
      >
        Drag the bottom corner to resize. Double-click it to reset.
      </p>
    </div>
  )
}

const campaign =
  "Weekend sale on every grill. 15% off Saturday and Sunday from 12:00 to 16:00, including delivery orders."

export default function TextareaResizableOutside() {
  return (
    <div className="w-80">
      <ResizableExample
        placeholder="Type your message here."
        resizable="outside"
        rows={5}
        defaultValue={campaign}
      />
    </div>
  )
}

Disabled

Disables the textarea so it can't be edited.

import { Textarea } from "@/components/ui/textarea"

export default function TextareaDisabled() {
  return (
    <div className="w-80">
      <Textarea placeholder="Type your message here." disabled />
    </div>
  )
}

Invalid

Marks the textarea with aria-invalid to show an error state.

import { Textarea } from "@/components/ui/textarea"

export default function TextareaInvalid() {
  return (
    <div className="w-80">
      <Textarea
        placeholder="Type your message here."
        aria-invalid
        defaultValue="Too short"
      />
    </div>
  )
}

API Reference

01

Textarea

The multi-line text field, with an optional resize handle.

PropTypeDefaultDescription
resizable"inside" | "outside"-Adds a vertical resize handle, placed inside or outside the field's bottom corner, and turns off automatic growth.

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

Accessibility

  • Renders a native textarea with the textbox role. Name it with a Label or aria-label.
  • The resize handle is hidden from assistive technologies and works with a pointer only.
  • An invalid field is exposed through aria-invalid.

Notes

  • Uses data-slot="textarea" and sets data-resizable when resizable; the wrapper and handle use textarea-container and textarea-handle.
  • A resizable textarea starts at a minimum height of 6rem, and dragging never shrinks it below its minimum height.
  • The handle is hidden while the textarea is disabled, and mirrors in right-to-left layouts.
  • The caret uses the primary color; invalid fields use the destructive color.
iOS
Textarea on iOS in light modeTextarea on iOS in dark mode
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 destructive border and tint when invalid, and 50% opacity when disabled.

Installation

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

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

$ pnpm dlx @esuiss/ui@latest add textarea
$ pnpm dlx @esuiss/ui@latest add textarea --platform swift
$ pnpm dlx @esuiss/ui@latest add textarea --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/textarea.tsx:

uniwind/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-secondary px-3 py-3 text-base text-secondary-foreground",
          state.focused && "border-ring ring-3 ring-ring/50",
  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/textarea.tsx:

nativewind/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: {
  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/textarea.tsx:

stylesheet/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,
  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/Textarea.swift
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
      ))
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/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/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/Textarea.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.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.CompositionLocalProvider
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 { 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.

iOS
Textarea (with value) on iOS in light modeTextarea (with value) on iOS in dark mode
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.

iOS
Textarea (with label) on iOS in light modeTextarea (with label) on iOS in dark mode
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.

iOS
Textarea (disabled) on iOS in light modeTextarea (disabled) on iOS in dark mode
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.

iOS
Textarea (invalid) on iOS in light modeTextarea (invalid) on iOS in dark mode
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

01

Textarea

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

Accessibility

  • Inside a Field, the textarea gets the field label as accessibilityLabel and the description as accessibilityHint.
  • disabled is set in accessibilityState and makes the textarea non-editable.
  • Outside a Field, give it an accessibilityLabel.

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.
NextToastPreviousText

On this page

  1. 01Installation
    • CLI
    • Manual
  2. 02Usage
  3. 03Examples
    • With value
    • With label
    • Resizable
    • Resizable outside
    • Disabled
    • Invalid
  4. 04API Reference
    • Textarea
  5. 05Accessibility
  6. 06Notes
  7. 07Installation
    • Manual
  8. 08Usage
  9. 09Examples
    • With value
    • With label
    • Disabled
    • Invalid
  10. 10API Reference
    • Textarea
  11. 11Accessibility
  12. 12Notes