HextaUI

BetaHecho sobre el UI SDK 0.1.0-beta.2 de Vercel. Su API puede cambiar.

Password input

Un campo de contraseña sobre el UI SDK de Vercel con botón para mostrar y ocultar, aviso de Bloq Mayús, medidor de seguridad y el autocompletado adecuado para gestores de contraseñas.

Depende 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

Añade el componente, los tokens del tema de HextaUI y los componentes de HextaUI de los que depende.

UI SDK añade un poco de CSS propio en una capa de cascada llamada ui-sdk. Declara esa capa al principio de tu hoja de estilos global, encima del import de Tailwind, para que tus clases ganen.

@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>

El control sigue siendo un campo de contraseña nativo, así que los gestores de contraseñas, el autocompletado y el envío de formularios funcionan como en cualquier otro sitio. Pon las opciones de formulario como name, required y autoComplete en PasswordInput, y los atributos del input como id y placeholder en PasswordInputControl.

autoComplete vale current-password por defecto, para iniciar sesión. Usa new-password al registrarse o cambiar la contraseña, para que el navegador ofrezca una segura, y off para secretos que no son contraseñas de cuenta.

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

PasswordInputStrength puntúa la contraseña de 0 a 4 mientras se escribe. La lista lee el mismo valor con usePasswordInputValue(). Las reglas son del ejemplo, porque el componente no impone ninguna política de contraseñas.

"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 }

Combínalo con un campo de email con autoComplete="username". Enter en la contraseña envía el formulario, y al enviar se vuelve a ocultar una contraseña visible para que el navegador ofrezca guardarla.

"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 empieza con el texto visible, para un secreto que hay que leer, como una contraseña de 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 controlador de createPasswordInputController() fija, muestra, oculta, vacía y enfoca el campo desde cualquier botón. Las escrituras pasan por onValueChange como al teclear.

"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 }

Pasa visible y onVisibleChange. El motivo distingue una pulsación del botón de un envío, un reinicio o la página al ocultarse.

"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 marca el campo y pone aria-invalid en el input. Apunta su aria-describedby a tu mensaje. Un campo obligatorio también se vuelve rojo si alguien lo deja vacío.

"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 campo de solo lectura se puede enfocar, mostrar y copiar, pero no cambiar, y aun así se envía.

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 se salta un campo desactivado y su botón, y el valor no se envía.

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 }

El botón queda a la izquierda y el medidor se llena desde la derecha. Pasa las etiquetas en tu idioma.

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 }
KeyAcción
TabPasa del campo al botón.
SpaceEnterMuestra u oculta la contraseña desde el botón y mantiene el foco en él.
EnterEnvía el formulario desde el campo. Nunca pulsa el botón.

El botón es un botón conmutador con un nombre fijo, “Show password”, y aria-pressed indica a los lectores de pantalla si la contraseña se ve. Cámbiale el nombre para otros secretos e idiomas. Una pulsación con puntero en el botón deja el cursor donde estaba en el campo.

PasswordInputCapsLock se abre bajo el campo mientras Bloq Mayús está activo y el campo tiene el foco, y lo dice mediante una región de estado. PasswordInputStrength es un medidor cuyo texto de valor es la etiqueta de seguridad, que se lee a demanda en vez de anunciarse en cada tecla. Las contraseñas visibles se saltan el corrector, la autocorrección y las mayúsculas automáticas.

Ninguna parte escribe la contraseña en un atributo data-*. La raíz y el control solo llevan booleanos como data-filled, y el medidor lleva la puntuación. Un value controlado sí hace que React mantenga un atributo value en el DOM, así que lee la contraseña con onValueChange o usePasswordInputValue().

Cada parte envuelve la parte de UI SDK del mismo nombre y acepta todas sus props, incluidas render y un className que puede ser una función del estado de la parte.

PropTipoPredeterminado
value
string–
defaultValue
string–
onValueChange
(value: string, details) => void–
visible
boolean–
defaultVisible
booleanfalse
onVisibleChangedetails.reason es toggle-press, form-submit, reset, page-hide o 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–
controllerDe createPasswordInputController().
PasswordInputController–
AtributoDescripción
data-slot="password-input"Apunta al campo de contraseña en CSS.
data-visiblePresente mientras se muestra la contraseña.
data-capslockPresente mientras Bloq Mayús está activo y el campo tiene el foco.
data-filledPresente mientras el campo tiene texto.
data-invalidPresente cuando no es válido.
data-disabledPresente cuando está deshabilitado.
data-readonlyPresente cuando es de solo lectura.
data-requiredPresente cuando se exige una contraseña.

La caja alrededor del campo y del botón. Dibuja el anillo de foco cuando el campo tiene el foco.

PropTipoPredeterminado
id
string–
placeholder
string–
render
ReactElement | (props, state) => ReactElement–
AtributoDescripción
data-slot="password-input-control"Apunta al input nativo en CSS.
data-visiblePresente mientras es un campo de texto.
PropTipoPredeterminado
aria-label
string"Show password"
childrenPor defecto, un ojo que se funde en un ojo cerrado.
ReactNode | (state) => ReactNode–
render
ReactElement | (props, state) => ReactElement–
AtributoDescripción
data-slot="password-input-toggle"Selecciona el interruptor en CSS.
aria-pressedtrue mientras la contraseña se ve.
PropTipoPredeterminado
children
ReactNode"Caps Lock is on"
AtributoDescripción
data-slot="password-input-capslock"Apunta al aviso en CSS.
data-activePresente mientras Bloq Mayús está activo.
PropTipoPredeterminado
getStrengthPuntúa la contraseña. La función por defecto pondera longitud, variedad de caracteres, repeticiones y contraseñas comunes. Usa un estimador más sólido como zxcvbn para políticas reales.
(password: string) => 0 | 1 | 2 | 3 | 4scorePassword
labels
[string, string, string, string, string]["", "Weak", "Fair", "Good", "Strong"]
label
string"Password strength"
AtributoDescripción
data-slot="password-input-strength"Apunta al medidor en CSS.
data-scoreLa puntuación, de 0 (vacío) a 4.