Przejdź do treści

Textarea

Wielowierszowe pole tekstowe, które rośnie razem z treścią — na wiadomości, opisy i opinie.

Kod źródłowyRejestr
import { Field, FieldLabel } from "@/components/ui/field"
import { Textarea } from "@/components/ui/textarea"

export default function TextareaDemo() {
  return (
    <Field className="w-80">
      <FieldLabel htmlFor="textarea-demo">Co możemy poprawić?</FieldLabel>
      <Textarea
        id="textarea-demo"
        placeholder="Przycisk eksportu trudno znaleźć na telefonie…"
      />
    </Field>
  )
}

Instalacja

$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/textarea.json

Użycie

import { Field, FieldLabel } from "@/components/ui/field"
import { Textarea } from "@/components/ui/textarea"
<Field>
  <FieldLabel htmlFor="message">Message</FieldLabel>
  <Textarea id="message" placeholder="Tell us what happened…" />
</Field>

Textarea ma to samo obramowanie, wypełnienie, poświatę fokusu i styl błędu co Input, więc oba komponenty pasują do siebie w każdym formularzu — bez żadnych wyjątków w kodzie.

Przykłady

Rośnie razem z treścią

Textarea korzysta z field-sizing: content: zaczyna od wygodnych trzech wierszy i rośnie w trakcie pisania, więc nikt nie musi pisać przez dziurkę od klucza. Ogranicz wzrost klasą max-h-* — powyżej tej wysokości pole zaczyna się przewijać. W przeglądarkach bez obsługi field-sizing pole zachowuje minimalną wysokość, a jego rozmiar można zmienić przeciąganiem.

import { Field, FieldDescription, FieldLabel } from "@/components/ui/field"
import { Textarea } from "@/components/ui/textarea"

export default function TextareaAutoResize() {
  return (
    <Field className="w-full max-w-md">
      <FieldLabel htmlFor="textarea-auto">Informacje o wersji</FieldLabel>
      <Textarea
        id="textarea-auto"
        className="max-h-60"
        defaultValue={
          "Szybszy eksport: pliki CSV i XLSX są generowane w tle.\nNowość: skróty klawiszowe dla każdej akcji w tabeli.\nPoprawka: filtry dat uwzględniają teraz strefę czasową przestrzeni roboczej."
        }
      />
      <FieldDescription>
        Rośnie wraz z treścią do 240 px, a potem się przewija.
      </FieldDescription>
    </Field>
  )
}

Limit znaków

Pokaż limit, zanim ktoś do niego dojdzie, odliczaj znaki w trakcie pisania i przełącz się na błąd po jego przekroczeniu — zamiast po cichu ucinać tekst. Licznik jest powiązany przez aria-describedby; osobny region na żywo o niskim priorytecie (polite) odzywa się dopiero blisko limitu, żeby nie przerywać osobom korzystającym z czytnika ekranu przy każdym naciśnięciu klawisza.

"use client"

import * as React from "react"

import {
  Field,
  FieldDescription,
  FieldError,
  FieldLabel,
} from "@/components/ui/field"
import { Textarea } from "@/components/ui/textarea"

const LIMIT = 160

export default function TextareaCharacterLimit() {
  const [bio, setBio] = React.useState(
    "Design engineer w prfct. Zależy mi na typografii, animacjach i interfejsach, które nie wchodzą w drogę."
  )
  const remaining = LIMIT - bio.length
  const over = remaining < 0

  return (
    <Field data-invalid={over || undefined} className="w-full max-w-md">
      <FieldLabel htmlFor="textarea-limit">Opis</FieldLabel>
      <Textarea
        id="textarea-limit"
        value={bio}
        onChange={(event) => setBio(event.target.value)}
        aria-invalid={over || undefined}
        aria-describedby="textarea-limit-count"
      />
      {/* Announce only near the limit — not on every keystroke. */}
      <span className="sr-only" aria-live="polite">
        {remaining <= 20
          ? over
            ? `Znaków ponad limit: ${-remaining}`
            : `Pozostało znaków: ${remaining}`
          : ""}
      </span>
      <div className="flex items-start justify-between gap-4">
        {over ? (
          <FieldError>{`Opis nie może przekraczać ${LIMIT} znaków.`}</FieldError>
        ) : (
          <FieldDescription>Widoczny na twoim profilu publicznym.</FieldDescription>
        )}
        <FieldDescription
          id="textarea-limit-count"
          className={
            over
              ? "shrink-0 text-danger-11 tabular-nums"
              : "shrink-0 tabular-nums"
          }
        >
          {bio.length}/{LIMIT}
        </FieldDescription>
      </div>
    </Field>
  )
}

Wyłączone i tylko do odczytu

Wyłączone pole nie przyjmuje fokusu i nie trafia do danych formularza. Pole tylko do odczytu nadal przyjmuje fokus, a jego treść można zaznaczać i przewijać — używaj go do tekstu, który trzeba przeczytać lub skopiować, na przykład wpisu z dziennika audytu.

import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"
import { Textarea } from "@/components/ui/textarea"

export default function TextareaStates() {
  return (
    <FieldGroup className="grid w-full max-w-xl gap-4 sm:grid-cols-2">
      <Field data-disabled>
        <FieldLabel htmlFor="textarea-disabled">Notatki wewnętrzne</FieldLabel>
        <Textarea
          id="textarea-disabled"
          disabled
          defaultValue="Zablokowane na czas przetwarzania faktury."
        />
      </Field>
      <Field>
        <FieldLabel htmlFor="textarea-readonly">Wpis audytu</FieldLabel>
        <Textarea
          id="textarea-readonly"
          readOnly
          defaultValue="Plan zmieniony z Team na Enterprise przez ada@prfct.dev 12 września."
        />
      </Field>
    </FieldGroup>
  )
}

W formularzu komentarza

Pole tekstowe często ma własny, niewielki zestaw akcji. Akcję główną umieść na końcu, a pole zrób na tyle szerokie, żeby zmieściło prawdziwe zdanie.

GH
"use client"

import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Textarea } from "@/components/ui/textarea"

export default function TextareaCommentForm() {
  return (
    <form
      className="flex w-full max-w-lg gap-3"
      onSubmit={(event) => event.preventDefault()}
    >
      <Avatar size="lg">
        <AvatarFallback>GH</AvatarFallback>
      </Avatar>
      <div className="flex flex-1 flex-col gap-2">
        <Textarea
          name="comment"
          placeholder="Napisz komentarz…"
          aria-label="Dodaj komentarz"
          className="min-h-24"
        />
        <div className="flex justify-end gap-2">
          <Button type="button" variant="ghost" size="sm">
            Anuluj
          </Button>
          <Button type="submit" size="sm">
            Dodaj komentarz
          </Button>
        </div>
      </div>
    </form>
  )
}

Wytyczne

Kiedy używać

  • Odpowiedzi otwarte, dłuższe niż jeden wiersz: opinie, opisy, notki biograficzne, wiadomości.
  • Treści, w których liczą się podziały wierszy, na przykład informacje o wydaniu albo adresy.

Kiedy nie używać

  • Krótkie odpowiedzi — Input lepiej sygnalizuje „odpowiedz krótko”.
  • Rozbudowane formatowanie, takie jak pogrubienia, listy czy wzmianki — użyj edytora tekstu sformatowanego.
  • Pole wiadomości w czacie z załącznikami i przyciskami wysyłania — złóż je z Input Group i InputGroupTextarea.

Rozmiar na miarę oczekiwanej odpowiedzi

Początkowa wysokość buduje oczekiwania. Pole na jeden wiersz sugeruje krótką odpowiedź; trzy do pięciu wierszy zachęcają do przemyślanej. Nie wyłączaj zmiany rozmiaru, chyba że układ naprawdę na to nie pozwala — polegają na niej osoby, które ustawiły większy tekst.

DobrzeKilka wierszy miejsca zachęca do prawdziwej odpowiedzi, a limit widać od początku.
ŹleJednowierszowe pole przy pytaniu otwartym ukrywa, jak wiele chcesz usłyszeć.

Dostępność

Textarea to natywny <textarea>: czytniki ekranu przedstawiają go jako wielowierszowe pole edycji, a wszystkie funkcje przeglądarki działają bez dodatkowej pracy — w tym sprawdzanie pisowni, dyktowanie i wprowadzanie tekstu przez IME.

  • Nadaj etykietę. Powiąż widoczny FieldLabel przez htmlFor/id albo dodaj aria-label.
  • Opisz limity. Limity i wskazówki dotyczące formatu umieść w FieldDescription powiązanym przez aria-describedby, a nie w placeholderze.
  • Informuj oszczędnie. Licznik w regionie na żywo, który odzywa się przy każdym naciśnięciu klawisza, zagłusza to, co ktoś wpisuje. Komunikat powinien pojawić się dopiero wtedy, gdy tekst zbliża się do limitu lub go przekracza.
  • Kontrast i rozmiar. Obramowanie ma kontrast co najmniej 3:1 względem strony, a tekst na małych ekranach ma 16 px, żeby iOS nie powiększał widoku.
KlawiszDziałanie
Tab
Przenosi fokus do pola.
Enter
Wstawia podział wiersza. Nigdy nie wysyła formularza.
CtrlEnter⌘Enter
Popularna konwencja wysyłania z pola wielowierszowego — podłącz ją samodzielnie, jeśli pasuje do scenariusza.

Dokumentacja API

Textarea

Renderuje natywny element <textarea> i przyjmuje wszystkie jego atrybuty.

PropTypDomyślnie
value

Wartość kontrolowana. Używaj razem z onChange.

stringBrak wartości domyślnej
defaultValue

Wartość początkowa niekontrolowanego pola.

stringBrak wartości domyślnej
rows

Liczba wierszy widocznych na start tam, gdzie field-sizing nie jest obsługiwane. prfct ustawia dolną granicę min-h-20.

numberBrak wartości domyślnej
maxLength

Twardy limit znaków egzekwowany przez przeglądarkę. Przy miękkich limitach lepiej sprawdza się widoczny licznik.

numberBrak wartości domyślnej
disabled

Blokuje interakcję i wyklucza wartość z wysyłanego formularza.

booleanfalse
readOnly

Blokuje edycję, ale tekst nadal przyjmuje fokus, można go zaznaczyć i jest wysyłany z formularzem.

booleanfalse
aria-invalid

Pokazuje obramowanie i poświatę fokusu w stanie błędu. Używaj razem z FieldError.

booleanBrak wartości domyślnej