Password Input
The password input component provides a secure way for users to enter passwords with built-in visibility controls and clear validation feedback.
import {PasswordInput} from "@qualcomm-ui/react/password-input"Examples
Simple
The simple API bundles all subcomponents together into a single component.
<PasswordInput
className="w-72"
clearable
hint="Optional hint"
label="Password"
placeholder="Create password"
/>
Icons (Simple)
Add a startIcon to the simple <PasswordInput> using the prop at the root. Unlike the <TextInput>, the endIcon is not supported because its slot is reserved for the password visibility trigger.
import type {ReactElement} from "react"
import {KeyRound} from "lucide-react"
import {PasswordInput} from "@qualcomm-ui/react/password-input"
export function PasswordInputIconsDemo(): ReactElement {
return (
<PasswordInput
className="w-72"
label="Password"
placeholder="Create password"
startIcon={KeyRound}
/>
)
}Composite
The composite pattern provides full control over the component's elements by composing individual subcomponents.
<PasswordInput.Root className="w-72">
<PasswordInput.Label>Password</PasswordInput.Label>
<PasswordInput.InputGroup>
<PasswordInput.Input placeholder="Placeholder text" />
<PasswordInput.VisibilityTrigger />
</PasswordInput.InputGroup>
<PasswordInput.Hint>Optional hint</PasswordInput.Hint>
</PasswordInput.Root>
Controlled Value
Set the initial value using the defaultValue prop, or use value and onValueChange to control the value manually. These props follow our controlled state pattern.
import {type ReactElement, useState} from "react"
import {Button} from "@qualcomm-ui/react/button"
import {PasswordInput} from "@qualcomm-ui/react/password-input"
export function PasswordInputControlledValueDemo(): ReactElement {
const [value, setValue] = useState<string>("passw0rd")
return (
<div className="flex items-end gap-4">
<PasswordInput
className="w-72"
label="Password"
onValueChange={(updatedValue) => {
console.debug("Value changed:", updatedValue)
setValue(updatedValue)
}}
placeholder="Enter your password"
value={value}
/>
<Button emphasis="primary" onClick={() => setValue("")} variant="outline">
Reset
</Button>
</div>
)
}Controlled Visibility
Set the initial visibility using the defaultVisible prop, or use visible and onVisibleChange to control the visibility manually. These props follow our controlled state pattern.
import {type ReactElement, useState} from "react"
import {Button} from "@qualcomm-ui/react/button"
import {PasswordInput} from "@qualcomm-ui/react/password-input"
export function PasswordInputControlledVisibilityDemo(): ReactElement {
const [visible, setVisible] = useState<boolean>(false)
return (
<div className="flex flex-col gap-4">
<PasswordInput
className="w-72"
defaultValue="passw0rd"
label="Password"
onVisibleChange={setVisible}
placeholder="Enter your password"
visible={visible}
/>
<Button
emphasis="primary"
onClick={() => setVisible(!visible)}
variant="outline"
>
{visible ? "Hide Password" : "Show Password"}
</Button>
</div>
)
}Forms
React Hook Form
import {arktypeResolver} from "@hookform/resolvers/arktype"
import {type} from "arktype"
import {Controller, useForm} from "react-hook-form"
import {Button} from "@qualcomm-ui/react/button"
import {PasswordInput} from "@qualcomm-ui/react/password-input"
interface PasswordFormData {
confirmPassword: string
password: string
}
// Arktype schema
const passwordSchema = type({
confirmPassword: "string",
password: "string",
}).narrow(({confirmPassword, password}, ctx) => {
let passwordError: string | undefined
if (!password || password.trim().length === 0) {
passwordError = "Please enter your password"
} else if (password.length < 8) {
passwordError = "Must be at least 8 characters long"
} else if (!/(?=.*[a-z])/.test(password)) {
passwordError = "Must contain at least one lowercase letter"
} else if (!/(?=.*[A-Z])/.test(password)) {
passwordError = "Must contain at least one uppercase letter"
} else if (!/(?=.*\d)/.test(password)) {
passwordError = "Must contain at least one number"
} else if (!/(?=.*[@$!%*?&])/.test(password)) {
passwordError = "Must contain at least one special character (@$!%*?&)"
}
let valid = true
if (passwordError) {
valid = false
ctx.reject({message: passwordError, path: ["password"]})
}
if (!confirmPassword) {
valid = false
ctx.reject({
message: "Please confirm your password",
path: ["confirmPassword"],
})
} else if (password !== confirmPassword) {
valid = false
ctx.reject({
// don't display the password in the error message
actual: "",
message: "Passwords do not match",
path: ["confirmPassword"],
})
}
return valid
})
export function PasswordInputReactHookFormDemo() {
const {
control,
formState: {errors, isSubmitting, submitCount, touchedFields},
handleSubmit,
} = useForm<PasswordFormData>({
defaultValues: {
confirmPassword: "",
password: "",
},
mode: "onChange",
resolver: arktypeResolver(passwordSchema),
})
const onSubmit = (data: PasswordFormData) => {
console.log("Form submitted:", data)
}
const shouldShowError = (fieldName: keyof PasswordFormData) => {
return (submitCount > 0 || touchedFields[fieldName]) && errors[fieldName]
}
return (
<form
className="mx-auto flex w-full max-w-xs flex-col gap-3"
onSubmit={(e) => {
void handleSubmit(onSubmit)(e)
}}
>
<div className="grid grid-cols-1 gap-4">
<div>
<Controller
control={control}
name="password"
render={({field: {onChange, ...fieldProps}}) => {
return (
<PasswordInput
errorText={errors.password?.message}
hint="must be 8+ characters with at least 1 number, lowercase, uppercase, and special character."
invalid={!!shouldShowError("password")}
label="Password"
onValueChange={onChange}
placeholder="Create password"
{...fieldProps}
/>
)
}}
/>
</div>
<div>
<Controller
control={control}
name="confirmPassword"
render={({field: {onChange, ...fieldProps}}) => {
return (
<PasswordInput
errorText={errors.confirmPassword?.message}
invalid={!!errors.confirmPassword}
label="Confirm password"
onValueChange={onChange}
placeholder="Confirm password"
{...fieldProps}
/>
)
}}
/>
</div>
</div>
<div className="mt-2 flex w-full justify-end">
<Button
disabled={isSubmitting}
emphasis="primary"
type="submit"
variant="fill"
>
Submit
</Button>
</div>
</form>
)
}TanStack Form
import {useForm} from "@tanstack/react-form"
import {Button} from "@qualcomm-ui/react/button"
import {PasswordInput} from "@qualcomm-ui/react/password-input"
interface PasswordFormData {
confirmPassword: string
password: string
}
// Password validation utility
const validatePassword = (password: string): string | undefined => {
if (!password || password.trim().length === 0) {
return "Please enter your password"
}
if (password.length < 8) {
return "Must be at least 8 characters long"
}
if (!/(?=.*[a-z])/.test(password)) {
return "Must contain at least one lowercase letter"
}
if (!/(?=.*[A-Z])/.test(password)) {
return "Must contain at least one uppercase letter"
}
if (!/(?=.*\d)/.test(password)) {
return "Must contain at least one number"
}
if (!/(?=.*[@$!%*?&])/.test(password)) {
return "Must contain at least one special character (@$!%*?&)"
}
return undefined
}
export function PasswordInputTanstackFormDemo() {
const form = useForm({
canSubmitWhenInvalid: true,
defaultValues: {
confirmPassword: "",
password: "",
} satisfies PasswordFormData,
onSubmit: ({value}) => {
console.log("Form submitted:", value)
},
})
return (
<form
className="mx-auto flex w-full max-w-xs flex-col gap-3"
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<div className="grid grid-cols-1 gap-4">
<form.Field
name="password"
validators={{
onBlur: ({value}) => validatePassword(value),
onChange: ({fieldApi, value}) => {
const error = validatePassword(value)
if (!error) {
fieldApi.setErrorMap({onBlur: undefined})
}
return error
},
}}
>
{(field) => {
// only show the error if the user has attempted to submit the form, or
// this field has been blurred. This prevents the error from showing
// immediately as the user types.
const isInvalid =
(form.state.submissionAttempts > 0 ||
field.getMeta().isBlurred) &&
!!field.state.meta.errors?.at(0)
return (
<PasswordInput
errorText={field.state.meta.errors?.at(0)}
hint="must be 8+ characters with at least 1 number, lowercase, uppercase, and special character."
invalid={isInvalid}
label="Password"
name={field.name}
onBlur={field.handleBlur}
onValueChange={field.handleChange}
placeholder="Create password"
value={field.state.value}
/>
)
}}
</form.Field>
<form.Field
name="confirmPassword"
validators={{
onBlur: ({fieldApi, value}) => {
if (!value || value.trim().length === 0) {
return "Please confirm your password"
}
const passwordValue = fieldApi.form.getFieldValue("password")
if (value !== passwordValue) {
return "Passwords do not match"
}
return undefined
},
onChange: ({fieldApi, value}) => {
if (!value || value.trim().length === 0) {
return "Please confirm your password"
}
const passwordValue = fieldApi.form.getFieldValue("password")
if (value !== passwordValue) {
return "Passwords do not match"
}
// clear onBlur error
fieldApi.setErrorMap({onBlur: undefined})
return undefined
},
}}
>
{(field) => (
<PasswordInput
errorText={field.state.meta.errors?.at(0)}
invalid={field.state.meta.errors.length > 0}
label="Confirm password"
name={field.name}
onBlur={field.handleBlur}
onValueChange={field.handleChange}
placeholder="Confirm password"
value={field.state.value}
/>
)}
</form.Field>
</div>
<div className="mt-2 flex w-full justify-end">
<Button
disabled={form.state.isSubmitting}
emphasis="primary"
type="submit"
variant="fill"
>
Submit
</Button>
</div>
</form>
)
}API
<PasswordInput>
The PasswordInput extends the PasswordInput.Root with the following props:
| Prop | Type | Default |
|---|---|---|
aria-label
attribute, forwarded to the input element. | string | |
aria-labelledby
attribute, forwarded to the input element. If you provide a label,
omit this prop. | string | |
When true, renders a clear button that resets the input value on click.
The button only appears when the input has a value. | boolean | false |
Props applied to the error indicator element. | ||
string | ||
Props applied to the error text element. | ||
Optional hint describing the element. This element is automatically
associated with the component's input element for accessibility. | ||
Props applied to the label element. | ||
Props applied to the input group element. | ||
Props applied to the input element. | ||
Optional label describing the element. Recommended. This element is
automatically associated with the component's input element for
accessibility. | ||
Props applied to the label element. | ||
HTML placeholder attribute,
passed to the underlying input element. | string | |
Props applied to the visibility trigger element. |
stringstringbooleantrue, renders a clear button that resets the input value on click.
The button only appears when the input has a value.stringComposite API
<PasswordInput.Root>
| Prop | Type | Default |
|---|---|---|
The autocomplete
attribute for the password input. | | 'current-password' | "current-password" |
The initial value of the input when rendered.
Use when you don't need to control the value of the input. | string | |
The default visibility of the password input. | boolean | |
The document's text/writing direction. | 'ltr' | 'rtl' | 'ltr' |
Whether the input is disabled. When true, prevents user interaction and
applies visual styling to indicate the disabled state. | boolean | |
The id of the form that the input belongs to. | string | |
A root node to correctly resolve document in custom environments. i.e.,
Iframes, Electron. | () => | |
The ids of the elements that are associated with the input. These will be
automatically generated if omitted. | Partial<{ | |
Controls the visual error state of the input. When true, applies semantic error
styling to indicate validation failure. | boolean | |
The name of the input field. Useful for form submission. | string | |
The callback invoked when the field is focused or blurred. | ( | |
The callback invoked when the value changes. | ( | |
Function called when the visibility changes. | ( | |
Whether the input is read-only. When true, prevents user interaction while
keeping the input focusable and visible. | boolean | |
Allows you to replace the component's HTML element with a different tag or component. Learn more | | ReactElement | |
Whether the input is required. When true, the input must have a value for form
validation to pass. | boolean | |
The size of the input field and its elements. Governs properties like font size,
item padding, and icon sizes. | | 'sm' | 'md' |
| LucideIcon | ||
Localized messages to use for element labels. | { | |
The controlled value of the input | string | |
Whether the password input is visible. | boolean |
| 'current-password'
| 'new-password'
stringboolean'ltr' | 'rtl'
booleanstring() =>
| Node
| ShadowRoot
| Document
Partial<{
errorText: string
hint: string
input: string
label: string
visibilityTrigger: string
}>
booleanstring(
focused: boolean,
) => void
(
value: string,
) => void
(
visible: boolean,
) => void
boolean| ReactElement
| ((
props: object,
) => ReactElement)
boolean| 'sm'
| 'md'
| 'lg'
| LucideIcon
| ReactNode
{
visibilityTrigger?: (
visible: boolean,
) => string
}
stringboolean| Attribute / Property | Value |
|---|---|
className | 'qui-input__root' |
data-disabled | |
data-focus | |
data-invalid | |
data-part | 'root' |
data-scope | 'password-input' |
data-size | | 'sm' |
className'qui-input__root'data-disableddata-focusdata-invaliddata-part'root'data-scope'password-input'data-size| 'sm'
| 'md'
| 'lg'
<PasswordInput.InputGroup>
| Prop | Type |
|---|---|
Allows you to replace the component's HTML element with a different tag or component. Learn more | | ReactElement |
| ReactElement
| ((
props: object,
) => ReactElement)
| Attribute / Property | Value |
|---|---|
className | 'qui-input__input-group' |
data-disabled | |
data-focus | |
data-invalid | |
data-part | 'input-group' |
data-readonly | |
data-scope | 'password-input' |
data-size | | 'sm' |
className'qui-input__input-group'data-disableddata-focusdata-invaliddata-part'input-group'data-readonlydata-scope'password-input'data-size| 'sm'
| 'md'
| 'lg'
<PasswordInput.Label>
| Attribute / Property | Value |
|---|---|
className | 'qui-input__label' |
data-disabled | |
data-focus | |
data-invalid | |
data-part | 'label' |
data-scope | 'password-input' |
data-size | | 'sm' |
className'qui-input__label'data-disableddata-focusdata-invaliddata-part'label'data-scope'password-input'data-size| 'sm'
| 'md'
| 'lg'
<PasswordInput.Hint>
| Prop | Type |
|---|---|
Allows you to replace the component's HTML element with a different tag or component. Learn more | | ReactElement |
| ReactElement
| ((
props: object,
) => ReactElement)
| Attribute / Property | Value |
|---|---|
className | 'qui-input__hint' |
data-disabled | |
data-part | 'hint' |
data-scope | 'password-input' |
hidden | boolean |
className'qui-input__hint'data-disableddata-part'hint'data-scope'password-input'hiddenboolean<PasswordInput.ClearTrigger>
| Prop | Type |
|---|---|
Allows you to replace the component's HTML element with a different tag or component. Learn more | | ReactElement |
| ReactElement
| ((
props: object,
) => ReactElement)
| Attribute / Property | Value |
|---|---|
className | 'qui-input__clear-trigger' |
data-disabled | |
data-part | 'clear-trigger' |
data-scope | 'password-input' |
data-size | | 'sm' |
className'qui-input__clear-trigger'data-disableddata-part'clear-trigger'data-scope'password-input'data-size| 'sm'
| 'md'
| 'lg'
<PasswordInput.Input>
| Attribute / Property | Value |
|---|---|
className | 'qui-input__input' |
data-empty | |
data-focus | |
data-invalid | |
data-part | 'input' |
data-readonly | |
data-scope | 'password-input' |
data-size | | 'sm' |
data-state | | 'hidden' |
className'qui-input__input'data-emptydata-focusdata-invaliddata-part'input'data-readonlydata-scope'password-input'data-size| 'sm'
| 'md'
| 'lg'
data-state| 'hidden'
| 'visible'
<PasswordInput.ErrorText>
| Prop | Type |
|---|---|
An icon to display next to the error text. | | LucideIcon |
Allows you to replace the component's HTML element with a different tag or component. Learn more | | ReactElement |
| LucideIcon
| ReactNode
| ReactElement
| ((
props: object,
) => ReactElement)
| Attribute / Property | Value |
|---|---|
className | 'qui-input__error-text' |
data-part | 'error-text' |
data-scope | 'password-input' |
hidden | boolean |
className'qui-input__error-text'data-part'error-text'data-scope'password-input'hiddenboolean<PasswordInput.ErrorIndicator>
| Prop | Type | Default |
|---|---|---|
lucide-react icon or ReactNode. | | LucideIcon | CircleAlert |
Allows you to replace the component's HTML element with a different tag or component. Learn more | | ReactElement |
| LucideIcon
| ReactNode
| ReactElement
| ((
props: object,
) => ReactElement)
| Attribute / Property | Value |
|---|---|
className | 'qui-input__error-indicator' |
data-part | 'error-indicator' |
data-scope | 'password-input' |
data-size | | 'sm' |
hidden | boolean |
className'qui-input__error-indicator'data-part'error-indicator'data-scope'password-input'data-size| 'sm'
| 'md'
| 'lg'
hiddenboolean<PasswordInput.VisibilityTrigger>
| Prop | Type | Default |
|---|---|---|
The icon shown when the input text is visible. | | LucideIcon | EyeOff |
The icon shown when the input text is hidden. | | LucideIcon | Eye |
Allows you to replace the component's HTML element with a different tag or component. Learn more | | ReactElement |
| LucideIcon
| ReactNode
| LucideIcon
| ReactNode
| ReactElement
| ((
props: object,
) => ReactElement)
| Attribute / Property | Value |
|---|---|
className | 'qui-password-input__visibility-trigger' |
data-disabled | |
data-part | 'visibility-trigger' |
data-readonly | |
data-scope | 'password-input' |
data-state | | 'hidden' |
tabIndex | -1 |
className'qui-password-input__visibility-trigger'data-disableddata-part'visibility-trigger'data-readonlydata-scope'password-input'data-state| 'hidden'
| 'visible'
tabIndex-1