Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
Components
Buttons & Actions
ButtonButton GroupCopy ButtonDelete ButtonTheme ToggleToggleToggle Group
Inputs
Avatar UploadCalendarCheckboxColor PickerComboboxCountry SelectCron PickerCurrency InputDate PickerDate Range PickerDate Time PickerDuration PickerEmail InputEmoji PickerFile UploadFilter BuilderInputInput GroupListboxMasked InputMention InputMonth PickerMulti SelectNumber InputOTP InputPassword InputPhone InputRadio GroupRatingSearch InputSegmented ControlSelectSignature PadSliderSwitchTag InputTextareaTime PickerTimezone SelectTree Select
Forms
Address InputBooking WidgetCSV ImportFieldForm WizardInline EditInvoice line itemsRich Text EditorSchema Builder
Navigation
Back To TopBounce SidebarBreadcrumbCommand MenuGlobal SearchGooey NavHook SidebarNav MenuNav TabsPaginationProximity SidebarScroll ProgressSidebarStepsTable Of ContentsTabsTopbarUser MenuWorkspace Switcher
Overlays
Alert DialogContext MenuDialogDrawerDropdown MenuFamily DrawerHover CardPopoverShare DialogSheetShortcuts DialogTooltipTour
Feedback
AlertBannerCookie ConsentEmoji ReactionEmpty StateNotification BellNotification CenterProgressRoute ProgressSetup ChecklistSkeletonSpinnerToast
Data Display
Approval FlowAvatarBadgeCalendar ViewCode BlockComment ThreadContact cardCountdownDescription ListDiff ViewerFile ListFile managerFolderGanttJSON ViewerKbdLog ViewerMarkdownPresenceSchedulerTagTimelineTree View
Tables & Lists
Audit LogData TableDeal PipelineEditable GridKanbanListMembers tablePermissions MatrixTableTask ListVirtual List
Charts & Stats
Animated CounterArea chartBar chartDonut chartFunnel chartGaugeGitHub ActivityHeatmap calendarLeaderboardLine chartRadar chartRadial barSparklineStat CardTrackerTrend badge
Layout
AccordionApp ShellCardCollapsibleDetail LayoutEntity HeaderPage HeaderScroll AreaSeparatorSettings LayoutSplit ViewToolbar
Media
Audio PlayerCarouselCSV ViewerDOCX EditorDOCX ViewerExcel ViewerFile SystemImage compareImage CropperImage GalleryImage LightboxLocation MapPDF ViewerPowerPoint ViewerQR CodeStep PlayerVideo Player
AI Kit
Agent Status CardChat MessageChat ThreadChat WidgetConversation ListFluid OrbGrid RevealMatrix OrbModel PickerPrompt InputReasoning IndicatorRule BuilderSuggestion ChipsTool Call TimelineUsage MeterVoice InputWorkflow Canvas
Text & Effects
Background PatternGravity LettersMarqueeTerminalText AnimateText Rotate
Mobile
Action SheetBottom Tab BarFABHaptic ButtonHero HeaderInstall PromptLarge Title HeaderNotification BannerOnboarding CarouselPicker WheelPull To RefreshSlide To ConfirmSticky CTA BarSwipe DeckSwipeable Tabs
Commerce
Cart drawerCategory pickerCheckout stepsCoupon inputFavorite buttonFilter panelListing cardListing gridOrder trackerPricePrice rangePricing tableProduct galleryQuantity stepperReviewsSeller card
Marketing Sections
App Download SectionBlog Cards SectionCTA SectionFAQ SectionFeature GridFeature ShowcaseHero SectionIntegrations SectionLogo CloudMarketing FooterMarketing HeaderMarketing SectionNewsletter SectionPricing SectionStats SectionSteps SectionTeam SectionTestimonials SectionTimeline SectionVideo Section
Blocks
Admin Dashboard PageAdmin ShellAI Chat PageAPI Keys PageAutomation Builder PageCheckout PageCRM Contact PageCRM Pipeline PageError PagesInbox PageInvoice PageKPI DashboardListing Detail PageLogin PageMarketplace Home PageMarketplace Search PageMarketplace ShellMobile App HomeOnboarding FlowOrder History PagePost Listing WizardPricing PageProfile PageRegister PageSettings PageUsers Admin Page
Templates
About PageAcademy PageAccessibility PageAffiliate PageAgency PageAI Product PageAppointment Booking PageBlog PageBlog Post PageBug Bounty PageCareers PageChangelog PageCommunity PageCompare PageContact PageCustomer Story PageCustomers PageDemo Booking PageDevelopers PageDocs Home PageDownload PageEnterprise PageEvents PageExperts PageFAQ PageFeature PageGlossary PageHelp Article PageHelp Center PageHome Services PageImpact PageIntegration Detail PageIntegrations PageJob Detail PageLanding PageLaunch PageLegal PageManifesto PagePortfolio PagePress PageProduct Tour PagePromo PageReal Estate PageReferral PageResearch Report PageRestaurant PageRoadmap PageROI Calculator PageSecurity PageSolutions PageStartups PageStatus PageStorefront PageSwitch PageTemplate Gallery PageTravel PageWaitlist PageYear in Review Page
Hooks & Utilities
AI ChatChart kitCommerceDate utilsFile exportFile typesFiltersForm validationHapticsInvoicingMotion presetsNavigationNumber formatRelative timeRomanian locationsSpreadsheetText matchTonesUIProvideruseClipboarduseConsentuseControllableStateuseCooldownuseDebounceuseKeyboardInsetuseLocaleuseLongPressuseMediaControlsuseMediaQueryuseOverflowItemsusePersistedStateuseScrollEdgesuseScrollStateuseSelectionuseShortcutuseStickToBottom
GalleryTheme

Inputs

Textarea

Inputs

Textarea React component for Tailwind CSS v4

Tag InputTime Picker
Live props
Field
label
description
error
required
invalid
disabled
readOnly
Content
placeholder
defaultValue
Appearance
size
variant
radius
Layout
minRows3
maxRows8
autosize
resizable
Behavior
showCount
maxLengthNo limit
submitShortcut
Code
import { Textarea } from "@/components/ui/textarea"

<Textarea />
  1. Home
  2. Components
  3. Inputs
  4. Textarea
Free

Textarea React component for Tailwind CSS v4

v1.0.0·Added in 1.0.0

An auto-growing React textarea with a character counter, an optional resize handle, a toolbar slot and mod+enter to submit.

Type and the box grows line by line from minRows up to maxRows, then scrolls inside; growth is instant, so typing never lags, and the caret stays in view because the box grows, not a scroller. Enter makes a new line and Tab moves on, like a native textarea. Press ⌘ Enter (Ctrl Enter elsewhere) to submit: the playground's Post checks for at least 40 characters, slides the error in under the box, and sends once it is long enough; submitShortcut="enter" makes it chat style, with Shift Enter for a new line. Use an example (Example on a phone) fills a long description so you can watch it grow to maxRows and scroll; Clear clears it. With a limit and showCount the counter turns amber at 90% and red at the limit. With resizable, a grip in the corner lets a mouse drag the box taller and that height sticks as the new floor. A click anywhere in the box, even in the toolbar's empty space, lands in the text. On a phone the text is 16px so iOS never zooms, the return key makes new lines, toolbar buttons keep 44px targets and the grip hides.

Textarea is a free React component in the React input components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add textarea, together with Field, useControllableState, useShortcut and UIProvider.

Add to a project

Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup

npx wingo-ui@latest add textarea

Usage

Import Textarea from @/components/ui/textarea.

import { useState } from "react"
import { Paperclip, SendHorizontal } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Textarea } from "@/components/ui/textarea"

export function ListingDescription() {
  return (
    <form action="/listings" method="post">
      <Textarea
        name="description"
        label="Listing description"
        description="At least 40 characters."
        placeholder="Describe the item: condition, size, why you are selling."
        required
        minLength={40}
        showCount
        maxLength={9000}
        fieldProps={{ counterLabel: (left) => `${left} characters left` }}
      />
    </form>
  )
}

export function Composer({ onSend }: { onSend: (text: string) => void }) {
  const [text, setText] = useState("")
  const send = () => {
    if (!text.trim()) return
    onSend(text.trim())
    setText("")
  }
  return (
    <Textarea
      aria-label="Message"
      placeholder="Write a message…"
      value={text}
      onValueChange={setText}
      minRows={1}
      maxRows={6}
      onSubmitShortcut={send}
      footer={
        <>
          <Button variant="ghost" size="icon-sm" radius="full" aria-label="Attach">
            <Paperclip />
          </Button>
          <Button size="sm" radius="full" className="ms-auto" rightIcon={<SendHorizontal />} onClick={send}>
            Send
          </Button>
        </>
      }
    />
  )
}

Props

  • labeldefault ""
    ReactNode

    The label, linked with htmlFor. Leave it empty for a bare box, e.g. inside a <Field> that has its own label.

  • descriptiondefault ""
    ReactNode

    Helper text under the box, linked with aria-describedby. The error takes its place while it shows.

  • errordefault ""
    ReactNode

    Replaces the description with an alert icon and the text, slides in and marks the box invalid (red ring, aria-invalid).

  • requireddefault false
    boolean

    The required mark after the label and required on the textarea.

  • invaliddefault false
    boolean

    The invalid look and aria-invalid without a message, for forms that show errors elsewhere.

  • disableddefault false
    boolean

    Dims the box and its label and blocks typing. Not submitted with the form, like any disabled field.

  • readOnlydefault false
    boolean

    The text stays visible, selectable and scrollable but cannot change; no hover tint and no resize grip. Good for terms or a sent message.

  • placeholderdefault ""
    string

    Shown while the box is empty, in the muted text color.

  • defaultValuedefault ""
    string

    The starting text while uncontrolled. Where the browser has CSS field-sizing the box fits it from the first paint, before any JavaScript runs.

  • value
    string

    The text, to control it from outside; pair it with onValueChange. A controlled value past maxLength shows as given (the counter turns red) and is never cut silently.

  • onValueChange
    (value: string) => void

    Called with the new text on every change, controlled or not.

  • onChange
    (event: ChangeEvent<HTMLTextAreaElement>) => void

    The native change event, untouched, so {...register("description")} from react-hook-form works as on a plain textarea.

  • sizedefault "md"
    "sm" | "md" | "lg"

    Text 13 / 14 / 15px (16px on phones, so iOS never zooms) with matching padding. One row is as tall as an input of the same size: 32 / 40 / 48px, 44px for md on touch; sm grows to 36px on phones, where its text is 16px.

  • variantdefault "soft"
    "soft" | "outline" | "underline" | "ghost"

    soft is a filled well that works on every surface, outline a bordered box, underline a bottom line that grows a bar from the center on focus, ghost is invisible until hovered (inline edits, toolbars).

  • radiusdefault "md"
    "none" | "sm" | "md" | "lg" | "full"

    Corner shape, scaled per size like the button's (md is 8 / 10 / 12px). full is half a one-row height (16 / 20 / 24px; 18px for sm on phones, 22px for md on touch), so a one-row composer is a pill and a taller box never turns into an oval. Ignored by underline.

  • minRowsdefault 3
    number

    The height when empty, in rows of text. 1 makes a composer that starts as tall as an input.

  • maxRowsdefault 8
    number

    Grows up to this many rows, then scrolls inside. 0 means no limit; a value below minRows counts as minRows.

  • autosizedefault true
    boolean

    Grow with the text. Uses CSS field-sizing where the browser has it and measures once per change elsewhere. false keeps the box at minRows and scrolls.

  • resizabledefault false
    boolean

    A vertical resize grip for mouse and trackpad (hidden on touch). With autosize the dragged height becomes the new floor and the box still grows past it when the text needs more.

  • showCountdefault false
    boolean

    The "count / max" counter at the end of the message row, amber from 90% and red at the limit. Needs maxLength.

  • maxLengthdefault 0
    number

    The native character limit; 0 means none. Screen readers hear how many characters are left once 10% or less remains.

  • submitShortcutdefault "mod+enter"
    string

    The combo that submits, in mod+k notation: ⌘ Enter on Apple devices, Ctrl Enter elsewhere. It calls onSubmitShortcut, else form.requestSubmit(). "enter" makes it chat style (Shift Enter for a new line; touch keyboards keep making new lines), "" turns it off. Announced with aria-keyshortcuts; shown nowhere by default (the demo shows a Kbd hint).

  • onSubmitShortcut
    (value: string, event: KeyboardEvent<HTMLTextAreaElement>) => void

    Runs on the shortcut with the current text instead of submitting the form. The chat example sends the message with it.

  • header
    ReactNode

    A row inside the box above the text: attachment chips, a quoted reply. A click on its empty space lands in the text.

  • footer
    ReactNode

    A toolbar inside the box under the text: attach, emoji, send. Buttons keep their own 44px hit areas on touch; a click on the empty space lands in the text.

  • id
    string

    The textarea's id. Defaults to useId(); inside a <Field> the Field's id wins. ref and every other native prop (name, autoComplete, spellCheck, autoFocus, onBlur, aria-*, data-*) land on the <textarea> too; inputMode defaults to text and enterKeyHint to enter.

  • fieldProps
    Partial<FieldProps>

    Anything else for the Field wrapper: optional, labelAction, orientation, labelWidth, hideLabel, counterLabel (what screen readers hear near the limit).

  • classNames
    Partial<Record<"field" | "label" | "description" | "error" | "counter" | "box" | "textarea" | "header" | "footer", string>>

    Extra classes for the inner parts.

  • className
    string

    Classes for the outermost element: the Field root when there is a label, description, error or counter, else the box.

Examples

Counter and minimum length

showCount with a limit, a 40-character minimum checked on blur, and the counter turning amber near 120.

Chat composer

minRows 1 to maxRows 6 with attachment chips in header and a toolbar in footer. onSubmitShortcut sends; switch to Enter for chat style.

Autosize, fixed and resizable

Grows with the text, stays fixed and scrolls, or takes a dragged height as its new floor.

Variants and sizes

soft, outline, underline and ghost in sm, md and lg. One row is as tall as an input of the same size.

Read-only terms

readOnly at maxRows 5: selectable, scrollable, not editable.

States

Required with a hint, an error, invalid without a message, and disabled.

In a horizontal form

Inside a FieldGroup the label moves beside the box once the form is wide enough and stacks on a phone. Cancel resets the form, ⌘ Enter saves it.

Dependencies

fielduse-controllable-stateuse-shortcutui-config

Files

  • components/ui/textarea.tsx

FAQ

Is the React Textarea component free?

Yes. The source is free and needs no account: copy it from the Code tab or install it with the CLI.

How do I install Textarea?

Run npx wingo-ui@latest add textarea. It also adds the Wingo UI items it builds on (Field, useControllableState, useShortcut and UIProvider). With shadcn, run npx shadcn@latest add https://wingo-ui.com/r/textarea.json.

How does Textarea behave on phones?

Use an example (Example on a phone) fills a long description so you can watch it grow to maxRows and scroll; Clear clears it. On a phone the text is 16px so iOS never zooms, the return key makes new lines, toolbar buttons keep 44px targets and the grip hides.

See also

FieldButtonKbduseShortcuthook

Used in

KanbanWorkflow CanvasCheckout stepsDeal PipelineBooking WidgetInline EditSchema BuilderShare DialogApproval FlowComment ThreadMembers tableChat Message

and 32 more.

More React input components

Time PickerTimezone SelectTree SelectDuration PickerOTP InputAvatar Upload
All 40 React input components

Articles using this component

  • Nextjs Contact Form That Sends Email and Blocks Spam

    Build it: screens and apps · Nov 5, 2026 · 10 min read

  • React Form Components: Accessible Fields, Zod and Mobile

    Component guides · Oct 9, 2026 · 26 min read

PreviousTag InputNext Time Picker

Post a listing

Item details

Step 2 of 3

At least 40 characters.

CtrlEnterControl Enterposts it·0 of at least 40 characters