Password Input
password-input ui Password input with show/hide toggle (Eye/EyeOff) and optional strength meter (weak/fair/good/strong with colored bar). Supports min length display, disabled, placeholder, and size variants.
Also available for React ->Installation
$ pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/password-input.json $ npx shadcn-vue@latest add https://uipkge.dev/r/vue/password-input.json $ yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/password-input.json $ bunx shadcn-vue@latest add https://uipkge.dev/r/vue/password-input.json Named registry:
npx shadcn-vue@latest add @uipkge/password-input Installs to: app/components/ui/password-input/ Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
modelValue | string | — | optional |
defaultValue | string | — | optional |
placeholder | string | 'Enter password' | optional |
size | 'sm''default''lg' | 'default' | optional |
variant | 'outlined''filled''borderless' | 'outlined' | optional |
disabled | boolean | false | optional |
readonly | boolean | false | optional |
showStrength | boolean | false | optional |
showToggle | boolean | true | optional |
minLength | number | 0 | optional |
maxlength | number | — | optional |
id | string | — | optional |
name | string | — | optional |
autocomplete | string | 'current-password' | optional |
class | HTMLAttributes['class'] | — | optional |
Schema
Type aliases from this item's source — use them to shape the data you pass in.
StrengthResult interface StrengthResult {
score: number
label: 'weak' | 'fair' | 'good' | 'strong'
color: string
barColor: string
percent: number
} npm dependencies
Files installed (3)
-
app/components/ui/password-input/PasswordInput.vue 5.4 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { computed, ref } from 'vue' import { Eye, EyeOff } from 'lucide-vue-next' import { cn } from '@/lib/utils' import { passwordInputVariants } from './password-input.variants' interface Props { modelValue?: string defaultValue?: string placeholder?: string size?: 'sm' | 'default' | 'lg' variant?: 'outlined' | 'filled' | 'borderless' disabled?: boolean readonly?: boolean showStrength?: boolean showToggle?: boolean minLength?: number maxlength?: number id?: string name?: string autocomplete?: string class?: HTMLAttributes['class'] } const props = withDefaults(defineProps<Props>(), { placeholder: 'Enter password', size: 'default', variant: 'outlined', disabled: false, readonly: false, showStrength: false, showToggle: true, minLength: 0, autocomplete: 'current-password', }) const emits = defineEmits<{ 'update:modelValue': [value: string] focus: [event: FocusEvent] blur: [event: FocusEvent] }>() const passwordVisible = ref(false) const computedType = computed(() => (passwordVisible.value ? 'text' : 'password')) const inputValue = computed({ get: () => props.modelValue ?? '', set: (val: string) => emits('update:modelValue', val), }) interface StrengthResult { score: number label: 'weak' | 'fair' | 'good' | 'strong' color: string barColor: string percent: number } const strength = computed<StrengthResult>(() => { const pwd = inputValue.value if (!pwd) return { score: 0, label: 'weak', color: '', barColor: 'bg-transparent', percent: 0 } let score = 0 if (pwd.length >= 6) score++ if (pwd.length >= 10) score++ if (/[A-Z]/.test(pwd) && /[a-z]/.test(pwd)) score++ if (/\d/.test(pwd)) score++ if (/[^A-Za-z0-9]/.test(pwd)) score++ if (score <= 1) { return { score, label: 'weak', color: 'text-destructive', barColor: 'bg-destructive', percent: 25 } } if (score <= 2) { return { score, label: 'fair', color: 'text-[var(--warning)]', barColor: 'bg-[var(--warning)]', percent: 50, } } if (score <= 3) { return { score, label: 'good', color: 'text-[var(--info)]', barColor: 'bg-[var(--info)]', percent: 75, } } return { score, label: 'strong', color: 'text-[var(--success)]', barColor: 'bg-[var(--success)]', percent: 100, } }) const meetsMinLength = computed(() => inputValue.value.length >= props.minLength) function toggleVisibility() { if (props.disabled || props.readonly) return passwordVisible.value = !passwordVisible.value } const wrapperClasses = computed(() => cn( passwordInputVariants({ size: props.size, variant: props.variant }), 'focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px]', props.disabled && 'pointer-events-none opacity-50 cursor-not-allowed bg-muted/30', props.class, ), ) const inputPadding = computed(() => { if (props.size === 'sm') return 'px-2.5' if (props.size === 'lg') return 'px-4' return 'px-3' }) const togglePadding = computed(() => { if (props.size === 'sm') return 'pr-2' if (props.size === 'lg') return 'pr-3' return 'pr-2.5' }) </script> <template> <div class="flex w-full flex-col gap-2"> <div :class="wrapperClasses" data-uipkge data-slot="password-input" :data-size="size" :data-variant="variant"> <input :id="id" v-model="inputValue" :type="computedType" :disabled="disabled" :readonly="readonly" :maxlength="maxlength" :placeholder="placeholder" :name="name" :autocomplete="autocomplete" :class="cn('placeholder:text-muted-foreground w-full min-w-0 flex-1 bg-transparent outline-none', inputPadding)" @focus="emits('focus', $event)" @blur="emits('blur', $event)" /> <div v-if="showToggle" class="flex shrink-0 items-center" :class="togglePadding"> <button type="button" :aria-label="passwordVisible ? 'Hide password' : 'Show password'" :aria-pressed="passwordVisible" :disabled="disabled || readonly" class="text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 shrink-0 rounded p-0.5 transition-colors focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed" @mousedown.prevent @click="toggleVisibility" > <Eye v-if="passwordVisible" class="size-4" aria-hidden="true" /> <EyeOff v-else class="size-4" aria-hidden="true" /> </button> </div> </div> <div v-if="showStrength && inputValue" class="flex flex-col gap-1.5"> <div class="bg-muted h-1.5 w-full overflow-hidden rounded-full"> <div class="h-full rounded-full transition-all duration-300" :class="strength.barColor" :style="{ width: `${strength.percent}%` }" /> </div> <div class="flex items-center justify-between text-xs"> <span :class="strength.color" class="font-medium capitalize">{{ strength.label }}</span> <span v-if="minLength > 0" :class="meetsMinLength ? 'text-[var(--success)]' : 'text-muted-foreground'"> {{ inputValue.length }} / {{ minLength }} chars </span> </div> </div> <p v-if="minLength > 0 && !showStrength && inputValue" class="text-muted-foreground text-xs"> Minimum {{ minLength }} characters </p> </div> </template> -
app/components/ui/password-input/password-input.variants.ts 0.8 kB
import type { VariantProps } from 'class-variance-authority' import { cva } from 'class-variance-authority' export const passwordInputVariants = cva( 'flex w-full items-center gap-1.5 overflow-hidden border transition-[color,box-shadow] outline-none rounded-md', { variants: { size: { sm: 'h-8 text-xs', default: 'h-9 text-base md:text-sm', lg: 'h-11 text-base', }, variant: { outlined: 'border-input bg-transparent shadow-xs', filled: 'border-transparent bg-muted/50 shadow-none', borderless: 'border-transparent bg-transparent shadow-none', }, }, defaultVariants: { size: 'default', variant: 'outlined', }, }, ) export type PasswordInputVariants = VariantProps<typeof passwordInputVariants> -
app/components/ui/password-input/index.ts 0.2 kB
export { default as PasswordInput } from './PasswordInput.vue' export { passwordInputVariants } from './password-input.variants' export type { PasswordInputVariants } from './password-input.variants'
Raw manifest: https://uipkge.dev/r/vue/password-input.json