Przejdź do treści

Input

Jednowierszowe pole tekstowe na imiona, adresy e-mail, liczby i wyszukiwane frazy — zawsze w parze z widoczną etykietą.

Kod źródłowyBase UIRejestr
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"

export default function InputDemo() {
  return (
    <Field className="w-72">
      <FieldLabel htmlFor="input-demo-email">E-mail służbowy</FieldLabel>
      <Input
        id="input-demo-email"
        type="email"
        placeholder="ada@analytical.engine"
      />
      <FieldDescription>Na ten adres wyślemy zaproszenie.</FieldDescription>
    </Field>
  )
}

Instalacja

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

Użycie

import { Field, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
<Field>
  <FieldLabel htmlFor="email">Email</FieldLabel>
  <Input id="email" type="email" placeholder="you@company.com" />
</Field>

Samo pole nie ma widocznej nazwy. W formularzach zawsze łącz je z komponentem Field, który spójnie rozmieszcza etykietę, opis i komunikat o błędzie.

Przykłady

Rozmiary

sm, default i lg mają te same wysokości co Button i Select — 32, 36 i 40 px — więc pole i jego akcja zawsze wyrównują się w jednym wierszu.

import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"

const sizes = [
  { size: "sm", label: "Mały · 32px" },
  { size: "default", label: "Domyślny · 36px" },
  { size: "lg", label: "Duży · 40px" },
] as const

export default function InputSizes() {
  return (
    <div className="flex w-full max-w-md flex-col gap-4">
      {sizes.map(({ size, label }) => (
        <div key={size} className="flex items-center gap-2">
          <Input size={size} placeholder={label} aria-label={label} />
          <Button size={size} variant="outline">
            Zastosuj
          </Button>
        </div>
      ))}
    </div>
  )
}

Typy

prfct stylizuje każdy natywny typ pola. Dobierz właściwe type i inputMode, żeby klawiatura na telefonie pasowała do danych, i ustaw autoComplete, żeby przeglądarki i menedżery haseł mogły wypełnić pole.

import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"

export default function InputTypes() {
  return (
    <FieldGroup className="grid w-full max-w-lg gap-4 sm:grid-cols-2">
      <Field>
        <FieldLabel htmlFor="input-types-password">Hasło</FieldLabel>
        <Input
          id="input-types-password"
          type="password"
          defaultValue="correct-horse"
          autoComplete="new-password"
        />
      </Field>
      <Field>
        <FieldLabel htmlFor="input-types-amount">Kwota</FieldLabel>
        <Input
          id="input-types-amount"
          type="number"
          inputMode="decimal"
          min={0}
          step={0.01}
          defaultValue={249.0}
        />
      </Field>
      <Field>
        <FieldLabel htmlFor="input-types-date">Data rozpoczęcia</FieldLabel>
        <Input id="input-types-date" type="date" defaultValue="2026-10-01" />
      </Field>
      <Field>
        <FieldLabel htmlFor="input-types-url">Strona internetowa</FieldLabel>
        <Input id="input-types-url" type="url" placeholder="https://" />
      </Field>
    </FieldGroup>
  )
}

Stan błędu

Ustaw aria-invalid na polu i data-invalid na jego Field, a problem wyjaśnij w FieldError powiązanym przez aria-describedby. Obramowanie robi się czerwone, a poświata fokusu razem z nim, ale znaczenie niosą komunikat i jego ikona — nigdy sam kolor.

import { Field, FieldError, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"

export default function InputInvalid() {
  return (
    <Field data-invalid className="w-72">
      <FieldLabel htmlFor="input-invalid-handle">Nazwa użytkownika</FieldLabel>
      <Input
        id="input-invalid-handle"
        defaultValue="admin"
        aria-invalid
        aria-describedby="input-invalid-handle-error"
      />
      <FieldError id="input-invalid-handle-error">
        Nazwa „admin” jest zarezerwowana. Wybierz inną.
      </FieldError>
    </Field>
  )
}

Wyłączone i tylko do odczytu

Użyj disabled, gdy wartości nie da się w tej chwili zmienić: fokus klawiatury pomija takie pole, a jego wartość nie trafia do wysyłanego formularza. Użyj readOnly, gdy ktoś musi zobaczyć, zaznaczyć lub skopiować wartość, której nie może edytować — takie pole nadal przyjmuje fokus i jest wysyłane z formularzem.

import {
  Field,
  FieldDescription,
  FieldGroup,
  FieldLabel,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"

export default function InputStates() {
  return (
    <FieldGroup className="w-full max-w-sm">
      <Field data-disabled>
        <FieldLabel htmlFor="input-states-plan">Plan</FieldLabel>
        <Input id="input-states-plan" defaultValue="Enterprise" disabled />
        <FieldDescription>Aby zmienić plan, skontaktuj się z administratorem.</FieldDescription>
      </Field>
      <Field>
        <FieldLabel htmlFor="input-states-id">ID przestrzeni roboczej</FieldLabel>
        <Input
          id="input-states-id"
          defaultValue="ws_8f3k29d1"
          readOnly
          className="font-mono"
        />
        <FieldDescription>
          Pole tylko do odczytu nadal przyjmuje fokus, a tekst można zaznaczyć.
        </FieldDescription>
      </Field>
    </FieldGroup>
  )
}

Plik

Pola plików dostają stonowany, obramowany przycisk wyboru, który pasuje do pola. Przeciąganie i upuszczanie albo wiele plików z podglądem buduj na tej podstawie, zamiast zastępować natywne pole — obsługę klawiatury i technologii wspomagających dostajesz wtedy za darmo.

import { Field, FieldDescription, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"

export default function InputFile() {
  return (
    <Field className="w-80">
      <FieldLabel htmlFor="input-file-avatar">Zdjęcie profilowe</FieldLabel>
      <Input
        id="input-file-avatar"
        type="file"
        accept="image/png, image/jpeg"
      />
      <FieldDescription>PNG lub JPG, do 2 MB.</FieldDescription>
    </Field>
  )
}

Z przyciskiem

Wysokości są wspólne, więc pole i przycisk stoją obok siebie bez żadnych poprawek. Jeśli nie ma widocznej etykiety, nadaj polu aria-label.

"use client"

import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"

export default function InputInlineForm() {
  return (
    <form
      className="flex w-full max-w-sm items-center gap-2"
      onSubmit={(event) => event.preventDefault()}
    >
      <Input
        type="email"
        name="email"
        placeholder="you@company.com"
        aria-label="Adres e-mail"
        autoComplete="email"
        required
      />
      <Button type="submit">Zapisz się</Button>
    </form>
  )
}

Tryb kontrolowany

Przekaż value i onChange, żeby budować interfejs na podstawie tego, co ktoś wpisuje — tutaj licznik znaków i podgląd adresu URL na żywo. Jeśli wartość jest potrzebna dopiero przy wysyłaniu, wybierz pole niekontrolowane z defaultValue.

"use client"

import * as React from "react"

import { Field, FieldDescription, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"

const LIMIT = 32

export default function InputControlled() {
  const [name, setName] = React.useState("Planowanie kwartalne")
  // Accents become plain letters (é → e, ł → l) instead of disappearing.
  const slug = name
    .normalize("NFD")
    .replace(/\p{Diacritic}/gu, "")
    .replace(/ł/gi, "l")
    .toLowerCase()
    .trim()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-|-$/g, "")

  return (
    <Field className="w-80">
      <FieldLabel htmlFor="input-controlled-name">Nazwa projektu</FieldLabel>
      <Input
        id="input-controlled-name"
        value={name}
        maxLength={LIMIT}
        onChange={(event) => setName(event.target.value)}
      />
      <FieldDescription className="flex justify-between gap-4">
        <span className="truncate font-mono">prfct.dev/{slug || "…"}</span>
        <span className="tabular-nums">
          {name.length}/{LIMIT}
        </span>
      </FieldDescription>
    </Field>
  )
}

Wytyczne

Kiedy używać

  • Krótki, dowolny tekst w jednej linii: imiona i nazwiska, adresy e-mail, adresy URL, kwoty, wyszukiwane frazy.
  • Wartości natywnego typu, który przeglądarka już rozumie: email, number, date, url, password.

Kiedy nie używać

  • Akapity i tekst wielowierszowy — użyj komponentu Textarea.
  • Wybór ze znanej listy — sięgnij po Select, a przy długich listach po Combobox.
  • Kody o stałej długości, np. do 2FA — do tego służy Input OTP.
  • Ikony, jednostki lub przyciski wewnątrz pola — wybierz Input Group.

Etykiety, nie placeholdery

Placeholder znika, gdy tylko ktoś zacznie pisać, więc nie może przechowywać nazwy pola ani instrukcji. Nazwę umieść w widocznej etykiecie, instrukcje w opisie, a placeholdera używaj — jeśli w ogóle — do pokazania przykładu oczekiwanego formatu.

DobrzeStała etykieta nazywa pole; placeholder pokazuje tylko przykład.
ŹlePlaceholder jako jedyna etykieta znika, gdy tylko zaczniesz pisać.

Szerokość to wskazówka

Szerokość pola podpowiada, ile trzeba wpisać. Dopasuj pola do oczekiwanej treści — kod pocztowy powinien wyglądać na krótszy niż adres — zamiast rozciągać każde pole na całą szerokość kontenera.

DobrzeSzerokości odpowiadają oczekiwanej długości każdej wartości.
ŹleJednakowe pola na całą szerokość nie zdradzają, które odpowiedzi są krótkie.

Waliduj we właściwym momencie

Waliduj przy wysyłaniu formularza — nie przy każdym naciśnięciu klawisza i nie przy blur. Po pierwszym wysłaniu sprawdzaj pola ponownie w trakcie pisania, żeby błąd zniknął, gdy tylko zostanie poprawiony. Zobacz Formularze.

Dostępność

Input renderuje natywny <input> przez Base UI, więc zachowuje wszystko, co zapewnia przeglądarka — autouzupełnianie, sprawdzanie pisowni, kompozycję IME, wysyłanie formularza — i działa wewnątrz Field i Form z Base UI, gdy potrzebujesz wbudowanej walidacji.

  • Nazwij każde pole. Powiąż widoczną etykietę przez htmlFor/id, a polom bez widocznej etykiety nadaj aria-label.
  • Opisuj i wyjaśniaj. Powiąż opisy i błędy przez aria-describedby; ustawiaj aria-invalid, dopóki wartość jest błędna.
  • Określ cel pola. Ustaw autoComplete przy danych osobowych, takich jak name, email i tel (WCAG 1.3.5).
  • Kontrast. Obramowanie pola używa tokenu input, który spełnia wymóg kontrastu elementów nietekstowych — 3:1 względem strony (WCAG 1.4.11). Tekst placeholdera używa muted-foreground o kontraście co najmniej 4,5:1.
  • Bez powiększania przy fokusie. Poniżej breakpointu md tekst ma 16 px, żeby Safari na iOS nie powiększało strony, gdy pole dostaje fokus; na większych ekranach ma 14 px.
KlawiszDziałanie
Tab
Przenosi fokus do pola. Fokus widać jako obramowanie w kolorze marki z delikatną poświatą.
ShiftTab
Przenosi fokus do poprzedniego elementu, który może go przyjąć.
Enter
Wysyła formularz, w którym znajduje się pole.

Dokumentacja API

Input

Renderuje element <input> za pośrednictwem komponentu Input z Base UI. Przyjmuje wszystkie natywne atrybuty pola input oraz własne propsy Base UI, takie jak render i onValueChange; typ propsów jest eksportowany jako InputProps. Wariant size z prfct zastępuje natywny atrybut size.

PropTypDomyślnie
size

Wysokość i poziomy odstęp wewnętrzny: 32, 36 lub 40 px, tak jak w Button i Select.

"sm" | "default" | "lg""default"
type

Dowolny natywny typ pola: email, password, number, date, url, file, search…

string"text"
value

Wartość kontrolowana. Łącz z onChange.

string | numberBrak wartości domyślnej
defaultValue

Wartość początkowa pola niekontrolowanego.

string | numberBrak wartości domyślnej
disabled

Blokuje interakcję, usuwa pole z kolejności tabulacji i z wysyłanych danych formularza.

booleanfalse
readOnly

Blokuje edycję, ale pole nadal przyjmuje fokus, jego wartość da się zaznaczyć i trafia do wysyłanego formularza.

booleanfalse
aria-invalid

Oznacza wartość jako błędną: czerwone obramowanie i czerwona poświata fokusu. Łącz z FieldError.

booleanBrak wartości domyślnej

inputVariants

Generator klas jest eksportowany dla elementów, które mają wyglądać jak pole — na przykład wartości tylko do odczytu renderowanej jako <div>.

import { inputVariants } from "@/components/ui/input"
import { cn } from "@/lib/utils"

<div className={cn(inputVariants({ size: "sm" }), "flex items-center font-mono")}>
  ws_8f3k29d1
</div>