Input
Jednowierszowe pole tekstowe na imiona, adresy e-mail, liczby i wyszukiwane frazy — zawsze w parze z widoczną etykietą.
Na ten adres wyślemy zaproszenie.
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.jsonUż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.
Aby zmienić plan, skontaktuj się z administratorem.
Pole tylko do odczytu nadal przyjmuje fokus, a tekst można zaznaczyć.
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.
PNG lub JPG, do 2 MB.
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.
prfct.dev/planowanie-kwartalne20/32
"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.
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.
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 nadajaria-label. - Opisuj i wyjaśniaj. Powiąż opisy i błędy przez
aria-describedby; ustawiajaria-invalid, dopóki wartość jest błędna. - Określ cel pola. Ustaw
autoCompleteprzy danych osobowych, takich jakname,emailitel(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żywamuted-foregroundo kontraście co najmniej 4,5:1. - Bez powiększania przy fokusie. Poniżej breakpointu
mdtekst ma 16 px, żeby Safari na iOS nie powiększało strony, gdy pole dostaje fokus; na większych ekranach ma 14 px.
| Klawisz | Dział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.
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>