HextaUI

BêtaConstruit sur l’UI SDK 0.1.0-beta.2 de Vercel. Son API peut changer.

Password input

Un champ de mot de passe sur l’UI SDK de Vercel avec un bouton pour afficher ou masquer, un avertissement Verr. Maj, une jauge de robustesse et le bon autocomplete pour les gestionnaires de mots de passe.

Dépend de
  • @tabler/icons-react
  • @ui-sdk/core
  • @ui-sdk/react
import { Label } from "@/components/ui/label"
import {
  PasswordInput,
  PasswordInputCapsLock,
  PasswordInputControl,
  PasswordInputGroup,
  PasswordInputToggle,
} from "@/components/ui/ui-sdk-password-input"

function UiSdkPasswordInputDemo() {
  return (
    <div className="flex w-full max-w-64 flex-col gap-2">
      <Label htmlFor="demo-password">Password</Label>
      <PasswordInput>
        <PasswordInputGroup>
          <PasswordInputControl id="demo-password" />
          <PasswordInputToggle />
        </PasswordInputGroup>
        <PasswordInputCapsLock />
      </PasswordInput>
    </div>
  )
}

export { UiSdkPasswordInputDemo }
pnpm dlx shadcn@latest add https://hextaui.com/r/ui-sdk-password-input.json

Ajoute le composant, les tokens de thème HextaUI et les composants HextaUI dont il dépend.

UI SDK ajoute un peu de CSS dans une couche de cascade nommée ui-sdk. Déclarez cette couche en haut de votre feuille de style globale, au-dessus de l’import Tailwind, pour que vos classes l’emportent.

@layer ui-sdk;

@import "tailwindcss";
import {
  PasswordInput,
  PasswordInputControl,
  PasswordInputGroup,
  PasswordInputToggle,
} from "@/components/ui/ui-sdk-password-input"
<PasswordInput name="password">
  <PasswordInputGroup>
    <PasswordInputControl aria-label="Password" />
    <PasswordInputToggle />
  </PasswordInputGroup>
</PasswordInput>

Le contrôle reste un champ de mot de passe natif : gestionnaires de mots de passe, remplissage automatique et envoi de formulaire fonctionnent comme partout. Placez les options de formulaire comme name, required et autoComplete sur PasswordInput, et les attributs d’input comme id et placeholder sur PasswordInputControl.

autoComplete vaut current-password par défaut, pour se connecter. Utilisez new-password pour créer ou changer un mot de passe, afin que le navigateur en propose un solide, et off pour les secrets qui ne sont pas des mots de passe de compte.

PasswordInput
├── PasswordInputGroup
│   ├── PasswordInputControl
│   └── PasswordInputToggle
├── PasswordInputCapsLock
└── PasswordInputStrength

PasswordInputStrength note le mot de passe de 0 à 4 pendant la saisie. La liste lit la même valeur avec usePasswordInputValue(). Les règles appartiennent à l’exemple, le composant n’imposant aucune politique de mot de passe.

"use client"

import { IconCheck, IconCircleDashed } from "@tabler/icons-react"

import { Label } from "@/components/ui/label"
import {
  PasswordInput,
  PasswordInputCapsLock,
  PasswordInputControl,
  PasswordInputGroup,
  PasswordInputStrength,
  PasswordInputToggle,
  usePasswordInputValue,
} from "@/components/ui/ui-sdk-password-input"

const rules = [
  {
    label: "At least 12 characters",
    test: (password: string) => password.length >= 12,
  },
  { label: "A number", test: (password: string) => /\d/.test(password) },
  {
    label: "An uppercase and a lowercase letter",
    test: (password: string) =>
      /[a-z]/.test(password) && /[A-Z]/.test(password),
  },
]

function Requirements() {
  const password = usePasswordInputValue()
  return (
    <ul
      aria-label="Password requirements"
      className="m-0 flex list-none flex-col gap-1 p-0 pt-3 text-xs"
    >
      {rules.map((rule) => {
        const done = rule.test(password)
        return (
          <li
            key={rule.label}
            className={
              done
                ? "flex items-center gap-1.5 text-foreground"
                : "flex items-center gap-1.5 text-muted-foreground"
            }
          >
            {done ? (
              <IconCheck aria-hidden="true" className="size-3.5 text-success" />
            ) : (
              <IconCircleDashed aria-hidden="true" className="size-3.5" />
            )}
            <span className="sr-only">{done ? "Done:" : "To do:"}</span>
            {rule.label}
          </li>
        )
      })}
    </ul>
  )
}

function UiSdkPasswordInputNewPassword() {
  return (
    <div className="flex w-full max-w-64 flex-col gap-2">
      <Label htmlFor="create-password">Create a password</Label>
      <PasswordInput autoComplete="new-password">
        <PasswordInputGroup>
          <PasswordInputControl id="create-password" />
          <PasswordInputToggle />
        </PasswordInputGroup>
        <PasswordInputCapsLock />
        <PasswordInputStrength />
        <Requirements />
      </PasswordInput>
    </div>
  )
}

export { UiSdkPasswordInputNewPassword }

Associez-le à un champ e-mail avec autoComplete="username". Entrée dans le mot de passe envoie le formulaire, et l’envoi masque à nouveau un mot de passe affiché pour que le navigateur propose de l’enregistrer.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
  PasswordInput,
  PasswordInputCapsLock,
  PasswordInputControl,
  PasswordInputGroup,
  PasswordInputToggle,
} from "@/components/ui/ui-sdk-password-input"

function UiSdkPasswordInputSignIn() {
  const [message, setMessage] = React.useState<string | null>(null)

  return (
    <form
      className="flex w-full max-w-64 flex-col gap-4"
      onSubmit={(event) => {
        event.preventDefault()
        const data = new FormData(event.currentTarget)
        const length = String(data.get("password")).length
        setMessage(
          `Signed in as ${data.get("email")}, with a password of ${length} characters.`
        )
      }}
      onReset={() => setMessage(null)}
    >
      <div className="flex flex-col gap-2">
        <Label htmlFor="sign-in-email">Email</Label>
        <Input
          id="sign-in-email"
          name="email"
          type="email"
          autoComplete="username"
          required
        />
      </div>
      <div className="flex flex-col gap-2">
        <Label htmlFor="sign-in-password">Password</Label>
        <PasswordInput name="password" required>
          <PasswordInputGroup>
            <PasswordInputControl id="sign-in-password" />
            <PasswordInputToggle />
          </PasswordInputGroup>
          <PasswordInputCapsLock />
        </PasswordInput>
      </div>
      <div className="flex items-center gap-3">
        <Button type="submit" size="sm">
          Sign in
        </Button>
        <Button type="reset" size="sm" variant="ghost">
          Reset
        </Button>
      </div>
      <p className="m-0 text-sm text-muted-foreground">
        {message ?? "Press Enter in the password to submit."}
      </p>
    </form>
  )
}

export { UiSdkPasswordInputSignIn }

defaultVisible démarre avec le texte affiché, pour un secret qu’il faut lire, comme un mot de passe Wi-Fi.

import { Label } from "@/components/ui/label"
import {
  PasswordInput,
  PasswordInputControl,
  PasswordInputGroup,
  PasswordInputToggle,
} from "@/components/ui/ui-sdk-password-input"

function UiSdkPasswordInputVisible() {
  return (
    <div className="flex w-full max-w-64 flex-col gap-2">
      <Label htmlFor="wifi-password">Wi-Fi password</Label>
      <PasswordInput
        defaultVisible
        defaultValue="maple-orbit-42"
        autoComplete="off"
      >
        <PasswordInputGroup>
          <PasswordInputControl id="wifi-password" />
          <PasswordInputToggle />
        </PasswordInputGroup>
      </PasswordInput>
      <p className="m-0 text-xs text-muted-foreground">
        Guests read it from here, so it starts shown.
      </p>
    </div>
  )
}

export { UiSdkPasswordInputVisible }

Un contrôleur issu de createPasswordInputController() remplit, affiche, masque, vide et cible le champ depuis n’importe quel bouton. Les écritures passent par onValueChange comme la saisie.

"use client"

import * as React from "react"
import { IconSparkles } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import {
  PasswordInput,
  PasswordInputControl,
  PasswordInputGroup,
  PasswordInputStrength,
  PasswordInputToggle,
  createPasswordInputController,
} from "@/components/ui/ui-sdk-password-input"

const alphabet =
  "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789-_!?"

function generatePassword(length = 20) {
  const bytes = crypto.getRandomValues(new Uint32Array(length))
  return Array.from(bytes, (byte) => alphabet[byte % alphabet.length]).join("")
}

function UiSdkPasswordInputController() {
  const [password] = React.useState(createPasswordInputController)

  return (
    <div className="flex w-full max-w-72 flex-col gap-2">
      <div className="flex items-center justify-between gap-2">
        <Label htmlFor="generated-password">New password</Label>
        <Button
          size="xs"
          variant="ghost"
          onClick={() => {
            password.setValue(generatePassword())
            password.setVisible(true)
            password.focus()
          }}
        >
          <IconSparkles aria-hidden="true" />
          Suggest
        </Button>
      </div>
      <PasswordInput controller={password} autoComplete="new-password">
        <PasswordInputGroup>
          <PasswordInputControl id="generated-password" />
          <PasswordInputToggle />
        </PasswordInputGroup>
        <PasswordInputStrength />
      </PasswordInput>
      <div className="flex gap-2 pt-1">
        <Button
          size="sm"
          variant="outline"
          onClick={() => password.toggleVisible()}
        >
          Toggle
        </Button>
        <Button size="sm" variant="ghost" onClick={() => password.clear()}>
          Clear
        </Button>
      </div>
    </div>
  )
}

export { UiSdkPasswordInputController }

Passez visible et onVisibleChange. La raison distingue un appui sur le bouton d’un envoi, d’une réinitialisation ou de la page masquée.

"use client"

import * as React from "react"

import { Label } from "@/components/ui/label"
import {
  PasswordInput,
  PasswordInputControl,
  PasswordInputGroup,
  PasswordInputToggle,
} from "@/components/ui/ui-sdk-password-input"

function UiSdkPasswordInputControlled() {
  const [visible, setVisible] = React.useState(false)
  const [reason, setReason] = React.useState<string | null>(null)

  return (
    <div className="flex w-full max-w-64 flex-col gap-2">
      <Label htmlFor="controlled-password">Password</Label>
      <PasswordInput
        visible={visible}
        onVisibleChange={(next, details) => {
          setVisible(next)
          setReason(details.reason)
        }}
      >
        <PasswordInputGroup>
          <PasswordInputControl id="controlled-password" />
          <PasswordInputToggle />
        </PasswordInputGroup>
      </PasswordInput>
      <p className="m-0 text-xs text-muted-foreground">
        {reason
          ? `${visible ? "Shown" : "Hidden"} by ${reason}.`
          : "Hidden. Press the eye to show it."}
      </p>
    </div>
  )
}

export { UiSdkPasswordInputControlled }

invalid marque le champ et pose aria-invalid sur l’input. Faites pointer son aria-describedby vers votre message. Un champ requis devient aussi rouge quand on le quitte vide.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import {
  PasswordInput,
  PasswordInputControl,
  PasswordInputGroup,
  PasswordInputToggle,
} from "@/components/ui/ui-sdk-password-input"

function UiSdkPasswordInputInvalid() {
  const [invalid, setInvalid] = React.useState(true)

  return (
    <form
      className="flex w-full max-w-64 flex-col gap-2"
      onSubmit={(event) => {
        event.preventDefault()
        const password = new FormData(event.currentTarget).get("password")
        setInvalid(password !== "correct-horse")
      }}
    >
      <Label htmlFor="checked-password">Password</Label>
      <PasswordInput
        name="password"
        invalid={invalid}
        onValueChange={() => setInvalid(false)}
      >
        <PasswordInputGroup>
          <PasswordInputControl
            id="checked-password"
            aria-describedby={invalid ? "checked-password-error" : undefined}
          />
          <PasswordInputToggle />
        </PasswordInputGroup>
      </PasswordInput>
      {invalid ? (
        <p
          id="checked-password-error"
          className="m-0 text-xs text-destructive"
        >
          That password isn’t right. Try “correct-horse”.
        </p>
      ) : null}
      <Button type="submit" size="sm" className="mt-1 self-start">
        Unlock
      </Button>
    </form>
  )
}

export { UiSdkPasswordInputInvalid }

Un champ en lecture seule peut être ciblé, affiché et copié, mais pas modifié, et il est quand même envoyé.

import { Label } from "@/components/ui/label"
import {
  PasswordInput,
  PasswordInputControl,
  PasswordInputGroup,
  PasswordInputToggle,
} from "@/components/ui/ui-sdk-password-input"

function UiSdkPasswordInputReadOnly() {
  return (
    <div className="flex w-full max-w-64 flex-col gap-2">
      <Label htmlFor="recovery-key">Recovery key</Label>
      <PasswordInput readOnly defaultValue="7Hq2-vX9p-Lm4c" autoComplete="off">
        <PasswordInputGroup>
          <PasswordInputControl id="recovery-key" />
          <PasswordInputToggle aria-label="Show recovery key" />
        </PasswordInputGroup>
      </PasswordInput>
      <p className="m-0 text-xs text-muted-foreground">
        You can show it, but not change it.
      </p>
    </div>
  )
}

export { UiSdkPasswordInputReadOnly }

Tab saute un champ désactivé et son bouton, et la valeur n’est pas envoyée.

import { Label } from "@/components/ui/label"
import {
  PasswordInput,
  PasswordInputControl,
  PasswordInputGroup,
  PasswordInputToggle,
} from "@/components/ui/ui-sdk-password-input"

function UiSdkPasswordInputDisabled() {
  return (
    <div className="flex w-full max-w-64 flex-col gap-2">
      <Label htmlFor="managed-password">Password</Label>
      <PasswordInput disabled defaultValue="hunter22">
        <PasswordInputGroup>
          <PasswordInputControl id="managed-password" />
          <PasswordInputToggle />
        </PasswordInputGroup>
      </PasswordInput>
      <p className="m-0 text-xs text-muted-foreground">
        Your administrator manages this password.
      </p>
    </div>
  )
}

export { UiSdkPasswordInputDisabled }

Le bouton est à gauche, et la jauge se remplit depuis la droite. Passez les libellés dans votre langue.

import { Label } from "@/components/ui/label"
import {
  PasswordInput,
  PasswordInputCapsLock,
  PasswordInputControl,
  PasswordInputGroup,
  PasswordInputStrength,
  PasswordInputToggle,
} from "@/components/ui/ui-sdk-password-input"

function UiSdkPasswordInputRtl() {
  return (
    <div dir="rtl" className="flex w-full max-w-64 flex-col gap-2">
      <Label htmlFor="rtl-password">كلمة المرور</Label>
      <PasswordInput autoComplete="new-password">
        <PasswordInputGroup>
          <PasswordInputControl id="rtl-password" />
          <PasswordInputToggle aria-label="إظهار كلمة المرور" />
        </PasswordInputGroup>
        <PasswordInputCapsLock>مفتاح الأحرف الكبيرة مفعّل</PasswordInputCapsLock>
        <PasswordInputStrength
          label="قوة كلمة المرور"
          labels={["", "ضعيفة", "مقبولة", "جيدة", "قوية"]}
        />
      </PasswordInput>
    </div>
  )
}

export { UiSdkPasswordInputRtl }
ToucheAction
TabPasse du champ au bouton.
SpaceEnterAffiche ou masque le mot de passe depuis le bouton, et garde le focus dessus.
EnterEnvoie le formulaire depuis le champ. N’actionne jamais le bouton.

Le bouton est un bouton bascule au nom fixe, « Show password », et aria-pressed indique aux lecteurs d’écran si le mot de passe est affiché. Renommez-le pour d’autres secrets et d’autres langues. Un appui au pointeur sur le bouton laisse le curseur où il était dans le champ.

PasswordInputCapsLock s’ouvre sous le champ tant que Verr. Maj est actif et que le champ a le focus, et le signale par une région de statut. PasswordInputStrength est une jauge dont le texte de valeur est le libellé de robustesse, lu à la demande plutôt qu’annoncé à chaque touche. Les mots de passe affichés échappent au correcteur, à la correction automatique et aux majuscules automatiques.

Aucune partie n’écrit le mot de passe dans un attribut data-*. La racine et le contrôle ne portent que des booléens comme data-filled, et la jauge porte le score. Un value contrôlé fait en revanche garder à React un attribut value dans le DOM, alors lisez le mot de passe avec onValueChange ou usePasswordInputValue().

Chaque partie enveloppe la partie UI SDK du même nom et accepte toutes ses props, y compris render et un className qui peut être une fonction de son état.

PropTypePar défaut
value
string–
defaultValue
string–
onValueChange
(value: string, details) => void–
visible
boolean–
defaultVisible
booleanfalse
onVisibleChangedetails.reason vaut toggle-press, form-submit, reset, page-hide ou imperative.
(visible: boolean, details) => void–
autoComplete
"current-password" | "new-password" | "off" | string"current-password"
invalid
booleanfalse
disabled
booleanfalse
readOnly
booleanfalse
required
booleanfalse
name
string–
form
string–
controllerIssu de createPasswordInputController().
PasswordInputController–
AttributDescription
data-slot="password-input"Cible le champ de mot de passe en CSS.
data-visiblePrésent tant que le mot de passe est affiché.
data-capslockPrésent tant que Verr. Maj est actif et que le champ a le focus.
data-filledPrésent tant que le champ contient du texte.
data-invalidPrésent quand il est invalide.
data-disabledPrésent lorsque l’élément est désactivé.
data-readonlyPrésent en lecture seule.
data-requiredPrésent quand un mot de passe est requis.

La boîte autour du champ et du bouton. Elle dessine l’anneau de focus quand le champ a le focus.

PropTypePar défaut
id
string–
placeholder
string–
render
ReactElement | (props, state) => ReactElement–
AttributDescription
data-slot="password-input-control"Cible l’input natif en CSS.
data-visiblePrésent tant que c’est un champ texte.
PropTypePar défaut
aria-label
string"Show password"
childrenPar défaut, un œil qui se fond en œil fermé.
ReactNode | (state) => ReactNode–
render
ReactElement | (props, state) => ReactElement–
AttributDescription
data-slot="password-input-toggle"Ciblez le bouton d’affichage en CSS.
aria-pressedtrue tant que le mot de passe est affiché.
PropTypePar défaut
children
ReactNode"Caps Lock is on"
AttributDescription
data-slot="password-input-capslock"Cible l’indication en CSS.
data-activePrésent tant que Verr. Maj est actif.
PropTypePar défaut
getStrengthNote le mot de passe. La fonction par défaut pèse la longueur, la variété des caractères, les répétitions et les mots de passe courants. Pour une vraie politique, utilisez un estimateur plus solide comme zxcvbn.
(password: string) => 0 | 1 | 2 | 3 | 4scorePassword
labels
[string, string, string, string, string]["", "Weak", "Fair", "Good", "Strong"]
label
string"Password strength"
AttributDescription
data-slot="password-input-strength"Cible la jauge en CSS.
data-scoreLe score, de 0 pour vide à 4.