Input 输入框
单行文本输入框,支持多种 type、禁用状态与错误状态,可与 Label 组合使用。
安装
bash
npx polyui add input基础
tsx
import { Input } from "@polyui/react/input"
export function InputBasic() {
return <Input type="email" placeholder="Email address" className="max-w-xs" />
}带标签
tsx
import { useId } from "react"
import { Input } from "@polyui/react/input"
import { Label } from "@polyui/react/label"
export function InputWithLabel() {
const id = useId()
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>Input with label</Label>
<Input id={id} type="email" placeholder="Email address" />
</div>
)
}必填
tsx
import { useId } from "react"
import { Input } from "@polyui/react/input"
import { Label } from "@polyui/react/label"
export function InputRequired() {
const id = useId()
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id} className="gap-1">
Required input <span className="text-destructive">*</span>
</Label>
<Input id={id} type="email" placeholder="Email address" required />
</div>
)
}禁用
tsx
import { useId } from "react"
import { Input } from "@polyui/react/input"
import { Label } from "@polyui/react/label"
export function InputDisabled() {
const id = useId()
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>Disabled input</Label>
<Input id={id} type="email" placeholder="Email address" disabled />
</div>
)
}只读
tsx
import { useId } from "react"
import { Input } from "@polyui/react/input"
import { Label } from "@polyui/react/label"
export function InputReadOnly() {
const id = useId()
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>Read-only input</Label>
<Input
id={id}
type="email"
placeholder="Email address"
defaultValue="example@xyz.com"
className="read-only:bg-muted"
readOnly
/>
</div>
)
}尺寸
tsx
import { Input } from "@polyui/react/input"
export function InputSizes() {
return (
<div className="w-full max-w-xs space-y-2">
<Input type="text" placeholder="Small input" className="h-7" />
<Input type="text" placeholder="Default input" />
<Input type="text" placeholder="Large input" className="h-10" />
</div>
)
}辅助文字
We'll never share your email with anyone else.
tsx
import { useId } from "react"
import { Input } from "@polyui/react/input"
import { Label } from "@polyui/react/label"
export function InputHelperText() {
const id = useId()
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>With helper text</Label>
<Input id={id} type="email" placeholder="Email address" />
<p className="text-muted-foreground text-xs">We'll never share your email with anyone else.</p>
</div>
)
}提示文字
Optional field
tsx
import { useId } from "react"
import { Input } from "@polyui/react/input"
import { Label } from "@polyui/react/label"
export function InputHintText() {
const id = useId()
return (
<div className="w-full max-w-xs space-y-2">
<div className="flex items-center justify-between gap-1">
<Label htmlFor={id}>With hint text</Label>
<span className="text-muted-foreground text-xs">Optional field</span>
</div>
<Input id={id} type="email" placeholder="Email address" />
</div>
)
}错误状态
This email is invalid.
tsx
import { useId } from "react"
import { Input } from "@polyui/react/input"
import { Label } from "@polyui/react/label"
export function InputError() {
const id = useId()
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>Input with error</Label>
<Input
id={id}
type="email"
placeholder="Email address"
className="peer"
defaultValue="invalid@email.com"
aria-invalid
/>
<p className="text-muted-foreground peer-aria-invalid:text-destructive text-xs">This email is invalid.</p>
</div>
)
}前置图标
tsx
import { useId } from "react"
import { Label } from "@polyui/react/label"
import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from "@polyui/react/input-group"
import { UserIcon } from "lucide-react"
export function InputStartIcon() {
const id = useId()
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>With start icon</Label>
<InputGroup>
<InputGroupAddon align="inline-start">
<InputGroupText>
<UserIcon className="size-4" />
</InputGroupText>
</InputGroupAddon>
<InputGroupInput id={id} type="text" placeholder="Username" />
</InputGroup>
</div>
)
}末端图标
tsx
import { useId } from "react"
import { Label } from "@polyui/react/label"
import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from "@polyui/react/input-group"
import { MailIcon } from "lucide-react"
export function InputEndIcon() {
const id = useId()
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>With end icon</Label>
<InputGroup>
<InputGroupInput id={id} type="email" placeholder="Email address" />
<InputGroupAddon align="inline-end">
<InputGroupText>
<MailIcon className="size-4" />
</InputGroupText>
</InputGroupAddon>
</InputGroup>
</div>
)
}前置文字附加
tsx
import { useId } from "react"
import { Label } from "@polyui/react/label"
import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from "@polyui/react/input-group"
export function InputStartTextAddon() {
const id = useId()
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>With start text add-on</Label>
<InputGroup>
<InputGroupAddon align="inline-start">
<InputGroupText>https://</InputGroupText>
</InputGroupAddon>
<InputGroupInput id={id} type="text" placeholder="shadcnstudio.com" />
</InputGroup>
</div>
)
}末端文字附加
tsx
import { useId } from "react"
import { Label } from "@polyui/react/label"
import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from "@polyui/react/input-group"
export function InputEndTextAddon() {
const id = useId()
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>With end text add-on</Label>
<InputGroup>
<InputGroupInput id={id} type="text" placeholder="shadcnstudio" />
<InputGroupAddon align="inline-end">
<InputGroupText>.com</InputGroupText>
</InputGroupAddon>
</InputGroup>
</div>
)
}双侧附加
tsx
import { useId } from "react"
import { Label } from "@polyui/react/label"
import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from "@polyui/react/input-group"
export function InputBothAddons() {
const id = useId()
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>With both add-ons</Label>
<InputGroup>
<InputGroupAddon align="inline-start">
<InputGroupText>https://</InputGroupText>
</InputGroupAddon>
<InputGroupInput id={id} type="text" placeholder="shadcnstudio" />
<InputGroupAddon align="inline-end">
<InputGroupText>.com</InputGroupText>
</InputGroupAddon>
</InputGroup>
</div>
)
}带按钮
tsx
import { useId } from "react"
import { Input } from "@polyui/react/input"
import { Label } from "@polyui/react/label"
import { Button } from "@polyui/react/button"
export function InputWithButton() {
const id = useId()
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>With button</Label>
<div className="flex gap-2">
<Input id={id} type="email" placeholder="Email address" />
<Button type="submit">Subscribe</Button>
</div>
</div>
)
}末端内联按钮
tsx
import { useId } from "react"
import { Label } from "@polyui/react/label"
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@polyui/react/input-group"
import { SendHorizonalIcon } from "lucide-react"
export function InputEndInlineButton() {
const id = useId()
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>With end inline button</Label>
<InputGroup>
<InputGroupInput id={id} type="email" placeholder="Email address" />
<InputGroupAddon align="inline-end">
<InputGroupButton size="icon-xs" variant="ghost">
<SendHorizonalIcon className="size-4" />
<span className="sr-only">Subscribe</span>
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
)
}图标按钮
tsx
import { useId } from "react"
import { Label } from "@polyui/react/label"
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@polyui/react/input-group"
import { DownloadIcon } from "lucide-react"
export function InputIconButton() {
const id = useId()
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>With icon button (end)</Label>
<InputGroup>
<InputGroupInput id={id} type="email" placeholder="Email address" />
<InputGroupAddon align="inline-end">
<InputGroupButton size="icon-xs" variant="ghost">
<DownloadIcon className="size-4" />
<span className="sr-only">Download</span>
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
)
}密码
tsx
import { useId, useState } from "react"
import { Label } from "@polyui/react/label"
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@polyui/react/input-group"
import { EyeIcon, EyeOffIcon } from "lucide-react"
export function InputPassword() {
const [isVisible, setIsVisible] = useState(false)
const id = useId()
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>Password</Label>
<InputGroup>
<InputGroupInput id={id} type={isVisible ? "text" : "password"} placeholder="Password" />
<InputGroupAddon align="inline-end">
<InputGroupButton
size="icon-xs"
variant="ghost"
onClick={() => setIsVisible((prev) => !prev)}
aria-label={isVisible ? "Hide password" : "Show password"}
>
{isVisible ? <EyeOffIcon className="size-4" /> : <EyeIcon className="size-4" />}
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
)
}清除
tsx
import { useId, useRef, useState } from "react"
import { Label } from "@polyui/react/label"
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@polyui/react/input-group"
import { CircleXIcon } from "lucide-react"
export function InputClear() {
const [value, setValue] = useState("Click to clear")
const inputRef = useRef<HTMLInputElement>(null)
const id = useId()
const handleClear = () => {
setValue("")
inputRef.current?.focus()
}
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>With clear button</Label>
<InputGroup>
<InputGroupInput
ref={inputRef}
id={id}
type="text"
placeholder="Type something..."
value={value}
onChange={(e) => setValue(e.target.value)}
/>
{value && (
<InputGroupAddon align="inline-end">
<InputGroupButton size="icon-xs" variant="ghost" onClick={handleClear}>
<CircleXIcon className="size-4" />
<span className="sr-only">Clear input</span>
</InputGroupButton>
</InputGroupAddon>
)}
</InputGroup>
</div>
)
}字符限制
tsx
import { useId, useState } from "react"
import { Label } from "@polyui/react/label"
import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from "@polyui/react/input-group"
export function InputCharacterLimit() {
const maxLength = 50
const [value, setValue] = useState("")
const [characterCount, setCharacterCount] = useState(0)
const id = useId()
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.value.length <= maxLength) {
setValue(e.target.value)
setCharacterCount(e.target.value.length)
}
}
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>Character limit</Label>
<InputGroup>
<InputGroupInput
id={id}
type="text"
placeholder="Username"
value={value}
maxLength={maxLength}
onChange={handleChange}
/>
<InputGroupAddon align="inline-end">
<InputGroupText className="text-xs tabular-nums">
{characterCount}/{maxLength}
</InputGroupText>
</InputGroupAddon>
</InputGroup>
</div>
)
}搜索
tsx
import { useId } from "react"
import { Label } from "@polyui/react/label"
import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from "@polyui/react/input-group"
import { SearchIcon } from "lucide-react"
export function InputSearch() {
const id = useId()
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>Search with keyboard shortcut</Label>
<InputGroup>
<InputGroupAddon align="inline-start">
<InputGroupText>
<SearchIcon className="size-4" />
</InputGroupText>
</InputGroupAddon>
<InputGroupInput
id={id}
type="search"
placeholder="Search..."
className="[&::-webkit-search-cancel-button]:appearance-none"
/>
<InputGroupAddon align="inline-end">
<kbd className="text-muted-foreground bg-accent inline-flex h-5 max-h-full items-center rounded border px-1 font-[inherit] text-[0.625rem] font-medium">
⌘k
</kbd>
</InputGroupAddon>
</InputGroup>
</div>
)
}带麦克风搜索
tsx
import { useId } from "react"
import { Label } from "@polyui/react/label"
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText } from "@polyui/react/input-group"
import { SearchIcon, MicIcon } from "lucide-react"
export function InputSearchWithMic() {
const id = useId()
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>Search with icon and mic</Label>
<InputGroup>
<InputGroupAddon align="inline-start">
<InputGroupText>
<SearchIcon className="size-4" />
</InputGroupText>
</InputGroupAddon>
<InputGroupInput
id={id}
type="search"
placeholder="Search..."
className="[&::-webkit-search-cancel-button]:appearance-none"
/>
<InputGroupAddon align="inline-end">
<InputGroupButton size="icon-xs" variant="ghost">
<MicIcon className="size-4" />
<span className="sr-only">Press to speak</span>
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
)
}搜索加载
tsx
import { useId, useState } from "react"
import { Label } from "@polyui/react/label"
import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from "@polyui/react/input-group"
import { SearchIcon, LoaderCircleIcon } from "lucide-react"
export function InputSearchLoader() {
const [value, setValue] = useState("")
const [isLoading, setIsLoading] = useState(false)
const id = useId()
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value)
if (e.target.value) {
setIsLoading(true)
setTimeout(() => setIsLoading(false), 500)
} else {
setIsLoading(false)
}
}
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>Search with loader</Label>
<InputGroup>
<InputGroupAddon align="inline-start">
<InputGroupText>
<SearchIcon className="size-4" />
</InputGroupText>
</InputGroupAddon>
<InputGroupInput
id={id}
type="search"
placeholder="Search..."
value={value}
onChange={handleChange}
className="[&::-webkit-search-cancel-button]:appearance-none"
/>
{isLoading && (
<InputGroupAddon align="inline-end">
<InputGroupText>
<LoaderCircleIcon className="size-4 animate-spin" />
</InputGroupText>
</InputGroupAddon>
)}
</InputGroup>
</div>
)
}文件
tsx
import { useId } from "react"
import { Input } from "@polyui/react/input"
import { Label } from "@polyui/react/label"
export function InputFile() {
const id = useId()
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>File input</Label>
<Input
id={id}
type="file"
className="text-muted-foreground file:border-input file:text-foreground p-0 pr-3 italic file:mr-3 file:h-full file:border-0 file:border-r file:border-solid file:bg-transparent file:px-3 file:text-sm file:font-medium file:not-italic"
/>
</div>
)
}日期
tsx
import { useId } from "react"
import { Input } from "@polyui/react/input"
import { Label } from "@polyui/react/label"
export function InputDate() {
const id = useId()
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>Date input</Label>
<Input id={id} type="date" />
</div>
)
}时间
tsx
import { useId } from "react"
import { Input } from "@polyui/react/input"
import { Label } from "@polyui/react/label"
export function InputTime() {
const id = useId()
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>Time input</Label>
<Input id={id} type="time" />
</div>
)
}颜色选择器
tsx
import { useId } from "react"
import { Input } from "@polyui/react/input"
import { Label } from "@polyui/react/label"
export function InputColor() {
const id = useId()
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>Color input</Label>
<Input id={id} type="color" className="h-8 w-full cursor-pointer px-2" />
</div>
)
}悬浮标签
tsx
import { useId } from "react"
import { Input } from "@polyui/react/input"
import { Label } from "@polyui/react/label"
export function InputOverlappingLabel() {
const id = useId()
return (
<div className="group relative w-full max-w-xs">
<Label
htmlFor={id}
className="absolute top-0 left-2.5 z-10 block -translate-y-1/2 rounded-sm bg-card px-1.5 text-xs font-medium text-muted-foreground"
>
Overlapping label
</Label>
<Input id={id} type="email" placeholder="Email address" className="h-9" />
</div>
)
}浮动标签
tsx
import { useId } from "react"
import { Input } from "@polyui/react/input"
export function InputFloatingLabel() {
const id = useId()
return (
<div className="group relative w-full max-w-xs">
<label
htmlFor={id}
className="origin-start absolute top-1/2 block -translate-y-1/2 cursor-text px-2 text-sm text-muted-foreground transition-all group-focus-within:pointer-events-none group-focus-within:top-0 group-focus-within:cursor-default group-focus-within:text-xs group-focus-within:font-medium group-focus-within:text-foreground has-[+input:not(:placeholder-shown)]:pointer-events-none has-[+input:not(:placeholder-shown)]:top-0 has-[+input:not(:placeholder-shown)]:cursor-default has-[+input:not(:placeholder-shown)]:text-xs has-[+input:not(:placeholder-shown)]:font-medium has-[+input:not(:placeholder-shown)]:text-foreground"
>
<span className="inline-flex rounded-sm bg-card px-1.5">Floating label</span>
</label>
<Input id={id} type="email" placeholder=" " />
</div>
)
}内嵌标签
tsx
import { useId } from "react"
export function InputInsetLabel() {
const id = useId()
return (
<div className="border-input bg-background focus-within:border-ring focus-within:ring-ring/50 relative w-full max-w-xs rounded-lg border shadow-xs transition-[color,box-shadow] outline-none focus-within:ring-3">
<label htmlFor={id} className="text-foreground block px-3 pt-1 text-xs font-medium">
Inset label
</label>
<input
id={id}
type="email"
placeholder="Email address"
className="text-foreground placeholder:text-muted-foreground flex h-8 w-full bg-transparent px-3 pb-1 text-sm focus-visible:outline-none"
/>
</div>
)
}密码强度
Enter a password
- ✗At least 12 characters
- ✗At least 1 lowercase letter
- ✗At least 1 uppercase letter
- ✗At least 1 number
tsx
import { useId, useState } from "react"
import { Label } from "@polyui/react/label"
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@polyui/react/input-group"
import { EyeIcon, EyeOffIcon } from "lucide-react"
export function InputPasswordStrength() {
const [password, setPassword] = useState("")
const [isVisible, setIsVisible] = useState(false)
const id = useId()
const strength = strengthRequirements.map((req) => ({
met: req.regex.test(password),
text: req.text,
}))
const strengthScore = strength.filter((r) => r.met).length
const getColor = (score: number) => {
if (score === 0) return "bg-border"
if (score <= 1) return "bg-destructive"
if (score <= 2) return "bg-orange-500"
if (score <= 3) return "bg-amber-500"
return "bg-green-500"
}
const getText = (score: number) => {
if (score === 0) return "Enter a password"
if (score <= 2) return "Weak password"
if (score <= 3) return "Medium password"
return "Strong password"
}
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>Password strength</Label>
<InputGroup>
<InputGroupInput
id={id}
type={isVisible ? "text" : "password"}
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<InputGroupAddon align="inline-end">
<InputGroupButton size="icon-xs" variant="ghost" onClick={() => setIsVisible((prev) => !prev)}>
{isVisible ? <EyeOffIcon className="size-4" /> : <EyeIcon className="size-4" />}
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<div className="flex h-1 w-full gap-1">
{Array.from({ length: 4 }).map((_, index) => (
<span
key={index}
className={`h-full flex-1 rounded-full transition-all duration-500 ${
index < strengthScore ? getColor(strengthScore) : "bg-border"
}`}
/>
))}
</div>
<p className="text-foreground text-sm font-medium">{getText(strengthScore)}</p>
<ul className="space-y-1">
{strength.map((req, index) => (
<li key={index} className="flex items-center gap-2">
<span className={req.met ? "text-green-600 dark:text-green-400" : "text-muted-foreground"}>
{req.met ? "✓" : "✗"}
</span>
<span className={`text-xs ${req.met ? "text-green-600 dark:text-green-400" : "text-muted-foreground"}`}>
{req.text}
</span>
</li>
))}
</ul>
</div>
)
}前端下拉
tsx
import { useId } from "react"
import { Input } from "@polyui/react/input"
import { Label } from "@polyui/react/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@polyui/react/select"
export function InputStartSelect() {
const id = useId()
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>With start select</Label>
<div className="flex rounded-lg shadow-xs">
<Select defaultValue="https://">
<SelectTrigger className="rounded-r-none shadow-none focus-visible:z-1 w-fit">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="https://">https://</SelectItem>
<SelectItem value="http://">http://</SelectItem>
<SelectItem value="ftp://">ftp://</SelectItem>
<SelectItem value="sftp://">sftp://</SelectItem>
</SelectContent>
</Select>
<Input id={id} type="text" placeholder="shadcnstudio.com" className="-ms-px rounded-l-none shadow-none" />
</div>
</div>
)
}末端下拉
tsx
import { useId } from "react"
import { Input } from "@polyui/react/input"
import { Label } from "@polyui/react/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@polyui/react/select"
export function InputEndSelect() {
const id = useId()
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>With end select</Label>
<div className="flex rounded-lg shadow-xs">
<Input
id={id}
type="text"
placeholder="shadcnstudio"
className="-me-px rounded-r-none shadow-none focus-visible:z-1"
/>
<Select defaultValue=".com">
<SelectTrigger className="rounded-l-none shadow-none w-fit">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value=".com">.com</SelectItem>
<SelectItem value=".org">.org</SelectItem>
<SelectItem value=".net">.net</SelectItem>
</SelectContent>
</Select>
</div>
</div>
)
}末端连接按钮
tsx
import { useId } from "react"
import { Input } from "@polyui/react/input"
import { Label } from "@polyui/react/label"
import { Button } from "@polyui/react/button"
export function InputEndButtonJoined() {
const id = useId()
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>With end button (joined)</Label>
<div className="flex rounded-lg shadow-xs">
<Input
id={id}
type="email"
placeholder="Email address"
className="-me-px rounded-r-none shadow-none focus-visible:z-1"
/>
<Button className="rounded-l-none">Subscribe</Button>
</div>
</div>
)
}剩余字符
12 characters left
tsx
import { useId, useState } from "react"
import { Input } from "@polyui/react/input"
import { Label } from "@polyui/react/label"
export function InputCharactersLeft() {
const maxLength = 12
const [value, setValue] = useState("")
const id = useId()
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.value.length <= maxLength) setValue(e.target.value)
}
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>Characters left</Label>
<Input id={id} type="text" placeholder="Username" value={value} maxLength={maxLength} onChange={handleChange} />
<p className="text-muted-foreground text-xs">
<span className="tabular-nums">{maxLength - value.length}</span> characters left
</p>
</div>
)
}密码强度 v2
Enter a password. Must contain:
- At least 12 characters
- At least 1 lowercase letter
- At least 1 uppercase letter
- At least 1 number
- At least 1 special character
tsx
import { useId, useMemo, useState } from "react"
import { Label } from "@polyui/react/label"
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@polyui/react/input-group"
import { CheckIcon, EyeIcon, EyeOffIcon, XIcon } from "lucide-react"
export function InputPasswordStrengthV2() {
const [password, setPassword] = useState("")
const [isVisible, setIsVisible] = useState(false)
const id = useId()
const strength = strengthReqsV2.map((req) => ({ met: req.regex.test(password), text: req.text }))
const strengthScore = useMemo(() => strength.filter((r) => r.met).length, [strength])
const getColor = (score: number) => {
if (score === 0) return "bg-border"
if (score <= 1) return "bg-destructive"
if (score <= 2) return "bg-orange-500"
if (score <= 3) return "bg-amber-500"
if (score === 4) return "bg-yellow-400"
return "bg-green-500"
}
const getText = (score: number) => {
if (score === 0) return "Enter a password"
if (score <= 2) return "Weak password"
if (score <= 3) return "Medium password"
if (score === 4) return "Strong password"
return "Very strong password"
}
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>Password strength v2</Label>
<div className="relative mb-3">
<InputGroup>
<InputGroupInput
id={id}
type={isVisible ? "text" : "password"}
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<InputGroupAddon align="inline-end">
<InputGroupButton
size="icon-xs"
variant="ghost"
onClick={() => setIsVisible((prev) => !prev)}
aria-label={isVisible ? "Hide password" : "Show password"}
>
{isVisible ? <EyeOffIcon className="size-4" /> : <EyeIcon className="size-4" />}
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
<div className="flex h-1 w-full gap-1">
{Array.from({ length: 5 }).map((_, index) => (
<span
key={index}
className={`h-full flex-1 rounded-full transition-all duration-500 ease-out ${index < strengthScore ? getColor(strengthScore) : "bg-border"}`}
/>
))}
</div>
<p className="text-foreground text-sm font-medium">{getText(strengthScore)}. Must contain:</p>
<ul className="space-y-1.5">
{strength.map((req, index) => (
<li key={index} className="flex items-center gap-2">
{req.met ? (
<CheckIcon className="size-4 text-green-600 dark:text-green-400" />
) : (
<XIcon className="text-muted-foreground size-4" />
)}
<span className={`text-xs ${req.met ? "text-green-600 dark:text-green-400" : "text-muted-foreground"}`}>
{req.text}
</span>
</li>
))}
</ul>
</div>
)
}属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
type | "text" | "email" | "password" | "number" | "search" | "tel" | "url" | "text" | 输入框的类型,决定键盘类型与内容格式校验。 |
placeholder | string | — | 输入框为空时显示的占位文本。 |
disabled | boolean | false | 禁用输入框,阻止所有交互并降低视觉透明度。 |
aria-invalid | "true" | "false" | boolean | false | 标记输入框处于错误状态,触发错误样式并向屏幕阅读器传达校验失败信息。 |
value | string | — | 受控模式下输入框的当前值。 |
onChange | (e: React.ChangeEvent<HTMLInputElement>) => void | — | 输入内容变化时的回调函数。 |