Skip to main content
PolyUI/docs

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&apos;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>
  )
}

前置文字附加

https://
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>
  )
}

末端文字附加

.com
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>
  )
}

双侧附加

https://
.com
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>
  )
}

字符限制

0/50
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>
  )
}

搜索

⌘k
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"输入框的类型,决定键盘类型与内容格式校验。
placeholderstring输入框为空时显示的占位文本。
disabledbooleanfalse禁用输入框,阻止所有交互并降低视觉透明度。
aria-invalid"true" | "false" | booleanfalse标记输入框处于错误状态,触发错误样式并向屏幕阅读器传达校验失败信息。
valuestring受控模式下输入框的当前值。
onChange(e: React.ChangeEvent<HTMLInputElement>) => void输入内容变化时的回调函数。