Input Group
Pole z dołączonymi ikonami, tekstem, przyciskami lub podpowiedziami klawiszy — z jednym obramowaniem, fokusem i walidacją, jak pojedyncza kontrolka.
import { SearchIcon } from "lucide-react"
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/components/ui/input-group"
import { Kbd } from "@/components/ui/kbd"
export default function InputGroupDemo() {
return (
<InputGroup className="w-72">
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
<InputGroupInput
placeholder="Szukaj zgłoszeń…"
aria-label="Szukaj zgłoszeń"
/>
<InputGroupAddon align="inline-end">
<Kbd>⌘K</Kbd>
</InputGroupAddon>
</InputGroup>
)
}Anatomia
- 1GrupaJedno obramowane pole z własną poświatą fokusu, dzięki czemu dodatki i pole odbiera się jako jedną kontrolkę.
- 2Dodatek początkowyTekst lub ikona przed polem: protokół, waluta, ikona wyszukiwania. Kliknięcie w dodatek ustawia fokus w polu.
- 3KontrolkaSamo pole — jedno- lub wielowierszowe — bez obramowania wewnątrz grupy.
- 4Dodatek końcowyPrzycisk, jednostka lub podpowiedź klawiszowa za polem.
Instalacja
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/input-group.jsonUżycie
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
} from "@/components/ui/input-group"<InputGroup>
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
<InputGroupInput placeholder="Search…" />
</InputGroup>Wewnątrz grupy używaj InputGroupInput i InputGroupTextarea — nigdy zwykłego komponentu Input. Obramowanie, wypełnienie, poświata fokusu i stan błędu należą do grupy; wewnętrzna kontrolka jest celowo pozbawiona własnych stylów.
Przykłady
Dodatki tekstowe
Prefiksy i sufiksy niosą jednostki i stałe części wartości — walutę, domenę — dzięki czemu ludzie wpisują tylko to, co się zmienia. Owiń tekst w InputGroupText.
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
InputGroupText,
} from "@/components/ui/input-group"
export default function InputGroupTextAddons() {
return (
<FieldGroup className="w-full max-w-sm">
<Field>
<FieldLabel htmlFor="input-group-subdomain">Adres URL przestrzeni roboczej</FieldLabel>
<InputGroup>
<InputGroupInput
id="input-group-subdomain"
defaultValue="acme"
className="text-right"
/>
<InputGroupAddon align="inline-end">
<InputGroupText>.prfct.dev</InputGroupText>
</InputGroupAddon>
</InputGroup>
</Field>
<Field>
<FieldLabel htmlFor="input-group-price">Budżet miesięczny</FieldLabel>
<InputGroup>
<InputGroupAddon>
<InputGroupText>$</InputGroupText>
</InputGroupAddon>
<InputGroupInput
id="input-group-price"
inputMode="decimal"
defaultValue="2400,00"
className="tabular-nums"
/>
<InputGroupAddon align="inline-end">
<InputGroupText>USD</InputGroupText>
</InputGroupAddon>
</InputGroup>
</Field>
</FieldGroup>
)
}Przyciski
InputGroupButton to kompaktowy Button w wariancie ghost, dopasowany rozmiarem do wnętrza pola: skopiuj wartość, odsłoń hasło, wyczyść zapytanie. Przyciski z samą ikoną potrzebują aria-label; przełączniki takie jak Pokaż hasło zgłaszają też aria-pressed.
"use client"
import * as React from "react"
import { CheckIcon, CopyIcon, EyeIcon, EyeOffIcon } from "lucide-react"
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/components/ui/input-group"
export default function InputGroupButtons() {
const [visible, setVisible] = React.useState(false)
const [copied, setCopied] = React.useState(false)
const key = "sk_live_51Mz9aQ8dK2x"
React.useEffect(() => {
if (!copied) return
const timer = setTimeout(() => setCopied(false), 1500)
return () => clearTimeout(timer)
}, [copied])
async function copy() {
try {
await navigator.clipboard.writeText(key)
setCopied(true)
} catch {
// The clipboard can be denied; the key stays in the field to select.
}
}
return (
<FieldGroup className="w-full max-w-sm">
<Field>
<FieldLabel htmlFor="input-group-key">Klucz API</FieldLabel>
<InputGroup>
<InputGroupInput
id="input-group-key"
readOnly
value={key}
className="font-mono"
/>
<InputGroupAddon align="inline-end">
<InputGroupButton
size="icon-xs"
aria-label={copied ? "Skopiowano" : "Kopiuj klucz API"}
onClick={copy}
>
{copied ? (
<CheckIcon className="text-success-11" />
) : (
<CopyIcon />
)}
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</Field>
<Field>
<FieldLabel htmlFor="input-group-password">Hasło</FieldLabel>
<InputGroup>
<InputGroupInput
id="input-group-password"
type={visible ? "text" : "password"}
defaultValue="parallel-lines-meet"
autoComplete="current-password"
/>
<InputGroupAddon align="inline-end">
<InputGroupButton
size="icon-xs"
aria-label={visible ? "Ukryj hasło" : "Pokaż hasło"}
aria-pressed={visible}
onClick={() => setVisible((value) => !value)}
>
{visible ? <EyeOffIcon /> : <EyeIcon />}
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</Field>
</FieldGroup>
)
}Status
Dodatki mogą informować, co dzieje się z wartością: podczas sprawdzania dostępności pokazują Spinner, a potem potwierdzają wynik ikoną. Zawsze powtórz status tekstem pod polem — sama ikona nie wystarczy.
Sprawdzanie dostępności…
Nazwa ada.lovelace jest dostępna.
import { CircleCheckIcon } from "lucide-react"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
} from "@/components/ui/field"
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/components/ui/input-group"
import { Spinner } from "@/components/ui/spinner"
export default function InputGroupStatus() {
return (
<FieldGroup className="w-full max-w-sm">
<Field>
<FieldLabel htmlFor="input-group-checking">Nazwa użytkownika</FieldLabel>
<InputGroup>
<InputGroupAddon>
<span className="text-muted-foreground">@</span>
</InputGroupAddon>
<InputGroupInput id="input-group-checking" defaultValue="ada" />
<InputGroupAddon align="inline-end">
<Spinner label="Sprawdzanie dostępności" />
</InputGroupAddon>
</InputGroup>
<FieldDescription>Sprawdzanie dostępności…</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="input-group-available">Nazwa użytkownika</FieldLabel>
<InputGroup>
<InputGroupAddon>
<span className="text-muted-foreground">@</span>
</InputGroupAddon>
<InputGroupInput
id="input-group-available"
defaultValue="ada.lovelace"
/>
<InputGroupAddon align="inline-end">
<CircleCheckIcon className="text-success-11" aria-hidden="true" />
</InputGroupAddon>
</InputGroup>
<FieldDescription className="text-success-11">
Nazwa ada.lovelace jest dostępna.
</FieldDescription>
</Field>
</FieldGroup>
)
}Pole wiadomości
Z InputGroupTextarea i dodatkiem block-end grupa staje się polem do pisania wiadomości: pole rośnie razem z tekstem, a załączniki, podpowiedzi i przycisk wysyłania pozostają przypięte pod nim.
import { ArrowUpIcon, AtSignIcon, PaperclipIcon } from "lucide-react"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupTextarea,
} from "@/components/ui/input-group"
export default function InputGroupComposer() {
return (
<InputGroup className="w-full max-w-lg">
<InputGroupTextarea
placeholder="Odpowiedz w #design-reviews…"
aria-label="Wiadomość"
className="min-h-20"
/>
<InputGroupAddon align="block-end" className="gap-1">
<InputGroupButton size="icon-sm" aria-label="Załącz plik">
<PaperclipIcon />
</InputGroupButton>
<InputGroupButton size="icon-sm" aria-label="Oznacz kogoś">
<AtSignIcon />
</InputGroupButton>
<InputGroupText className="ml-auto text-xs">
Enter wysyła · Shift + Enter dodaje nowy wiersz
</InputGroupText>
<InputGroupButton
size="icon-sm"
variant="default"
aria-label="Wyślij wiadomość"
className="ml-2 rounded-full"
>
<ArrowUpIcon />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
)
}Dodatki blokowe
Dodatki block-start stoją nad kontrolką na całą szerokość — nazwa pliku nad edytorem kodu, pasek narzędzi nad notatką. Dodaj border-b, żeby je oddzielić.
import { CodeIcon } from "lucide-react"
import {
InputGroup,
InputGroupAddon,
InputGroupText,
InputGroupTextarea,
} from "@/components/ui/input-group"
export default function InputGroupBlockAddon() {
return (
<InputGroup className="w-full max-w-lg">
<InputGroupAddon align="block-start" className="border-b pb-2.5">
<CodeIcon />
<InputGroupText className="font-mono text-xs text-foreground">
webhook-payload.json
</InputGroupText>
</InputGroupAddon>
<InputGroupTextarea
aria-label="Treść webhooka"
className="min-h-28 font-mono text-code"
defaultValue={"{\n \"event\": \"invoice.paid\",\n \"amount\": 4900\n}"}
/>
</InputGroup>
)
}Wytyczne
Kiedy używać
- Jednostki, prefiksy i sufiksy, które są częścią wartości:
$,USD,.prfct.dev,https://. - Akcje działające na samej wartości: kopiowanie, odsłanianie, czyszczenie, wysłanie wyszukiwania.
- Sygnały dotyczące wartości: ikona wyszukiwania na początku, status walidacji, podpowiedź skrótu klawiszowego.
Kiedy nie używać
- Akcje, które nie dotyczą wartości, takie jak Zapisz czy Anuluj — umieść wtedy Button obok pola.
- Więcej niż dwa dodatki po jednej stronie. Zatłoczone pole trudno objąć wzrokiem, a na wpisywanie zostaje mało miejsca.
- Wielowierszowa edycja z formatowaniem — użyj dedykowanego edytora.
Dodatki nie mogą udawać
Ikona na początku pola ma wyjaśniać, do czego ono służy, a nie je zdobić. Jeśli jest interaktywna, musi być przyciskiem InputGroupButton z etykietą — ikona, która wygląda na klikalną, a nic nie robi, to pułapka.
Dostępność
- Jedna kontrolka, jedna nazwa. Nazwij
InputGroupInputprzezFieldLabelzhtmlFor/idalbo przezaria-label. Tekst dodatku, np.USD, nie wchodzi w skład nazwy — jeśli jednostka ma znaczenie, podaj ją w etykiecie lub opisie. - Grupowanie. Grupa i każdy dodatek renderują
role="group"; kliknięcie w puste miejsce dodatku ustawia fokus w polu. - Fokus i stan. Gdy kontrolka ma fokus — z klawiatury albo po kliknięciu — cała grupa pokazuje poświatę fokusu w kolorze marki, a gdy kontrolka ma
aria-invalid— robi się czerwona. - Przyciski.
InputGroupButtonto prawdziwy przycisk, domyślnie ztype="button", więc nigdy przypadkiem nie wyśle formularza. - Skróty. Podpowiedź
Kbdjest wyłącznie wizualna. Zadbaj, żeby skrót dało się odkryć także gdzie indziej, np. w podpowiedzi lub na stronie pomocy.
| Klawisz | Działanie |
|---|---|
Tab | Przenosi fokus do pola, a potem na kolejne przyciski w grupie. |
EnterSpacja | Aktywuje InputGroupButton, który ma fokus. |
Dokumentacja API
InputGroup
Renderuje <div role="group">, który rysuje obramowanie, wypełnienie i stan fokusu. Przyjmuje wszystkie propsy div.
InputGroupAddon
Renderuje <div role="group"> na ikony, tekst, przyciski lub podpowiedzi Kbd.
InputGroupButton
Renderuje Button o rozmiarze dopasowanym do grupy. Przyjmuje wszystkie propsy Button; size przyjmuje własne rozmiary grupy, wymienione poniżej.
InputGroupText
Renderuje <span> na tekst i ikony w kolorze muted-foreground.
InputGroupInput
Renderuje Input z Base UI bez obramowania. Przyjmuje wszystkie natywne propsy pola input.
InputGroupTextarea
Renderuje element <textarea> bez obramowania, który rośnie razem z treścią. Przyjmuje wszystkie natywne propsy textarea.