UIPackage
Menu

Password Input

password-input ui
Edit on GitHub

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

$ npx 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
}

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