Input OTP
Segmentowane pole na kody jednorazowe, PIN-y i kody odzyskiwania — z obsługą wklejania, autouzupełniania z SMS-ów i menedżerów haseł.
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "@/components/ui/input-otp"
export default function InputOTPDemo() {
return (
<InputOTP maxLength={6} aria-label="Kod weryfikacyjny">
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
)
}Anatomia
- 1GrupaCiąg pól, które dzielą obramowania, dzięki czemu grupa wygląda jak jedno pole.
- 2Pole znakuJeden znak. Pole, w którym właśnie się pisze, przyjmuje kolor fokusu i pokazuje kursor.
- 3SeparatorPrzerwa między grupami, tak jak w kodzie zapisanym jako 123 456.
Instalacja
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/input-otp.jsonUżycie
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "@/components/ui/input-otp"<InputOTP maxLength={6}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>Input OTP jest zbudowany na input-otp: sloty to tylko obraz jednego prawdziwego pola, które leży pod spodem. Dlatego działają wklejanie, autouzupełnianie z SMS-ów, menedżery haseł, cofanie zmian i czytniki ekranu — do wypełnienia jest jedno pole, a nie sześć.
Przykłady
Tylko cyfry
Przekaż pattern, żeby odrzucać niepasujące znaki już podczas pisania. REGEXP_ONLY_DIGITS pasuje do PIN-ów i kodów z SMS-ów, a domyślny inputMode="numeric" wyświetla na telefonach klawiaturę numeryczną. Wzorce są reeksportowane z @/components/ui/input-otp; podobnie jak samego komponentu, używaj ich w komponentach klienckich.
Litery i symbole są pomijane podczas wpisywania.
"use client"
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field"
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
REGEXP_ONLY_DIGITS,
} from "@/components/ui/input-otp"
export default function InputOTPDigitsOnly() {
return (
<Field className="w-fit">
<FieldLabel htmlFor="input-otp-pin">PIN karty</FieldLabel>
<InputOTP
id="input-otp-pin"
maxLength={4}
pattern={REGEXP_ONLY_DIGITS}
inputMode="numeric"
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
</InputOTPGroup>
</InputOTP>
<FieldDescription>
Litery i symbole są pomijane podczas wpisywania.
</FieldDescription>
</Field>
)
}Litery i cyfry
W kodach odzyskiwania dopuść litery wzorcem REGEXP_ONLY_DIGITS_AND_CHARS i przełącz inputMode na text, żeby dało się je wpisać z klawiatury telefonu. pasteTransformer czyści wklejane wartości — tutaj usuwa myślniki i zamienia litery na wielkie.
Wklej kod, np. 7F2K-9QXA — myślniki zostaną usunięte.
"use client"
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field"
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
REGEXP_ONLY_DIGITS_AND_CHARS,
} from "@/components/ui/input-otp"
export default function InputOTPAlphanumeric() {
return (
<Field className="w-fit">
<FieldLabel htmlFor="input-otp-recovery">Kod odzyskiwania</FieldLabel>
<InputOTP
id="input-otp-recovery"
maxLength={8}
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
inputMode="text"
pasteTransformer={(pasted) => pasted.replaceAll("-", "").toUpperCase()}
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
<InputOTPSlot index={6} />
<InputOTPSlot index={7} />
</InputOTPGroup>
</InputOTP>
<FieldDescription>
Wklej kod, np. 7F2K-9QXA — myślniki zostaną usunięte.
</FieldDescription>
</Field>
)
}Tryb kontrolowany z automatycznym wysłaniem
Kontroluj wartość przez value i onChange, a weryfikuj ją w onComplete, który wywołuje się, gdy wszystkie sloty są wypełnione. Na czas weryfikacji wyłącz pole, po nieudanej próbie zachowaj to, co ktoś wpisał, i napisz, dokąd wysłano kod.
Wysłaliśmy go na adres ada@prfct.dev.
"use client"
import * as React from "react"
import { Button } from "@/components/ui/button"
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field"
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
REGEXP_ONLY_DIGITS,
} from "@/components/ui/input-otp"
export default function InputOTPControlled() {
const [code, setCode] = React.useState("")
const [status, setStatus] = React.useState<"idle" | "verifying" | "verified">(
"idle"
)
// Stands in for the request that checks the code; cleared on unmount.
React.useEffect(() => {
if (status !== "verifying") return
const timer = setTimeout(() => setStatus("verified"), 1200)
return () => clearTimeout(timer)
}, [status])
return (
<Field className="w-fit items-center">
<FieldLabel htmlFor="input-otp-controlled">
Wpisz 6-cyfrowy kod
</FieldLabel>
<InputOTP
id="input-otp-controlled"
maxLength={6}
pattern={REGEXP_ONLY_DIGITS}
value={code}
onChange={(value) => {
setCode(value)
setStatus("idle")
}}
onComplete={() => setStatus("verifying")}
disabled={status === "verifying"}
>
<InputOTPGroup>
{Array.from({ length: 6 }, (_, index) => (
<InputOTPSlot key={index} index={index} />
))}
</InputOTPGroup>
</InputOTP>
<FieldDescription aria-live="polite" className="text-center">
{status === "verifying" && "Weryfikowanie…"}
{status === "verified" && "Zweryfikowano. Zalogowano pomyślnie."}
{status === "idle" && "Wysłaliśmy go na adres ada@prfct.dev."}
</FieldDescription>
<Button
variant="link"
size="sm"
disabled={status === "verifying"}
onClick={() => {
setCode("")
setStatus("idle")
}}
>
Wyślij kod ponownie
</Button>
</Field>
)
}Stan błędu
Ustaw aria-invalid na InputOTP, a wszystkie sloty zmienią kolor na czerwony — nie trzeba oznaczać ich pojedynczo. Wyjaśnij problem w FieldError powiązanym przez aria-describedby.
"use client"
import * as React from "react"
import { Field, FieldError, FieldLabel } from "@/components/ui/field"
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp"
export default function InputOTPInvalid() {
const [code, setCode] = React.useState("481209")
return (
<Field data-invalid className="w-fit">
<FieldLabel htmlFor="input-otp-invalid">Kod weryfikacyjny</FieldLabel>
<InputOTP
id="input-otp-invalid"
maxLength={6}
value={code}
onChange={setCode}
aria-invalid
aria-describedby="input-otp-invalid-error"
>
<InputOTPGroup>
{Array.from({ length: 6 }, (_, index) => (
<InputOTPSlot key={index} index={index} />
))}
</InputOTPGroup>
</InputOTP>
<FieldError id="input-otp-invalid-error">
Ten kod wygasł. Poproś o nowy.
</FieldError>
</Field>
)
}Wytyczne
Kiedy używać
- Kody o stałej długości, które ktoś przepisuje z innego miejsca: kody uwierzytelniania dwuskładnikowego, kody weryfikacyjne z e-maili, PIN-y, kody odzyskiwania.
Kiedy nie używać
- Hasła i wszystko o zmiennej długości — użyj komponentu Input z
type="password". - Kody dłuższe niż mniej więcej 8 znaków — łatwiej wypełnić pojedyncze pole z czytelną wskazówką co do formatu.
Dziel długie kody
Pamięć krótkotrwała mieści naraz trzy, cztery znaki. Podziel kody sześciocyfrowe na dwie grupy po trzy, a ośmioznakowe na dwie grupy po cztery za pomocą InputOTPSeparator — i zachowaj taki sam podział jak w e-mailu lub SMS-ie, który dostarcza kod.
Wybaczaj błędy
Wysyłaj kod automatycznie po wypełnieniu pola, ale nigdy nie zostawiaj ludzi bez wyjścia: po nieudanej próbie zachowaj to, co wpisali, pozwól poprawić dowolny slot i zamiast ślepego zaułka zaproponuj Wyślij kod ponownie z widocznym czasem oczekiwania.
Dostępność
- Jedno pole. Czytniki ekranu ogłaszają jedno pole tekstowe i odczytują całą wartość; w kolejności tabulacji pole zajmuje jedno miejsce, a nie po jednym na każdy slot.
- Podpisz pole. Nadaj
InputOTPid, do którego odwoła sięFieldLabel, alboaria-label, np. Kod weryfikacyjny. - Autouzupełnianie.
autoComplete="one-time-code"jest ustawione domyślnie, więc iOS i Android podpowiadają kody z SMS-ów i e-maili. - Menedżery haseł. Przy
pushPasswordManagerStrategy="increase-width"(wartość domyślna) pole niewidocznie się poszerza, żeby ikony menedżerów haseł nie zasłaniały ostatniego slotu. - Kontrast. Obramowania slotów używają tokenu
inputo kontraście 3:1; aktywny slot dostaje dodatkowo poświatę fokusu w kolorze marki.
| Klawisz | Działanie |
|---|---|
Tab | Przenosi fokus do pola. Kursor trafia do pierwszego pustego slotu. |
0–9 / A–Z | Wypełnia aktywny slot i przechodzi do następnego. Znaki niepasujące do pattern są ignorowane. |
Backspace | Usuwa poprzedni znak. |
←→ | Przesuwa kursor między slotami. |
CtrlV⌘V | Wkleja kod i rozkłada go na sloty. |
Dokumentacja API
InputOTP
Renderuje kontener i jedno ukryte pole <input>. Przyjmuje wszystkie natywne propsy pola input oraz opcje biblioteki input-otp.
InputOTPGroup
Renderuje <div>, który łączy swoje sloty w jeden obramowany segment.
InputOTPSlot
Renderuje komórkę na jeden znak. Przyjmuje wszystkie propsy div.
InputOTPSeparator
Renderuje <div role="separator"> z kreską między grupami.