Password input
A password field with an inline reveal toggle that stays out of the tab order.
<script setup lang="ts">
import SaPasswordInput from '@shared/components/common/SaPasswordInput.vue'
import { Button } from '@shared/components/ui/button'
import { Input } from '@shared/components/ui/input'
import { Label } from '@shared/components/ui/label'
import { ref } from 'vue'
const email = ref('')
const password = ref('')
</script>
<template>
<form class="max-w-sm space-y-4" @submit.prevent>
<div class="space-y-2">
<Label for="signin-email">Email</Label>
<Input id="signin-email" v-model="email" type="email" autocomplete="username" />
</div>
<div class="space-y-2">
<Label for="signin-password">Password</Label>
<SaPasswordInput
id="signin-password"
v-model="password"
placeholder="At least 6 characters"
autocomplete="current-password"
/>
</div>
<Button type="submit">
Sign in
</Button>
</form>
</template>Usage
v-model is the password string. The component declares only the five props below and forwards every other attribute to the inner input, so required, minlength, name, disabled and the rest work as they do on a plain field.
The eye is tabindex="-1". Tab from the field goes to the next field, not to the toggle, because the reveal is a convenience and tabbing into it on the way to Sign in would be a detour on every attempt.
Examples
Invalid
ariaInvalid puts aria-invalid on the field, which is what draws the error ring. Pair it with aria-describedby, which falls through to the field with every other attribute.
<script setup lang="ts">
import SaPasswordInput from '@shared/components/common/SaPasswordInput.vue'
import { Label } from '@shared/components/ui/label'
import { computed, ref } from 'vue'
const password = ref('short')
const tooShort = computed(() => password.value.length > 0 && password.value.length < 6)
</script>
<template>
<form class="max-w-sm space-y-2" @submit.prevent>
<Label for="new-password">New password</Label>
<SaPasswordInput
id="new-password"
v-model="password"
:aria-invalid="tooShort"
aria-describedby="new-password-error"
autocomplete="new-password"
/>
<p v-if="tooShort" id="new-password-error" class="text-sm text-destructive">
Use at least 6 characters.
</p>
</form>
</template>Disabled
disabled is not a declared prop. It falls through to the inner field like any other attribute.
<script setup lang="ts">
import SaPasswordInput from '@shared/components/common/SaPasswordInput.vue'
import { Label } from '@shared/components/ui/label'
import { ref } from 'vue'
const password = ref('managed-by-sso')
</script>
<template>
<form class="max-w-sm space-y-2" @submit.prevent>
<Label for="sso-password">Password</Label>
<SaPasswordInput id="sso-password" v-model="password" disabled autocomplete="current-password" />
</form>
</template>API
Source: src/shared/components/common/SaPasswordInput.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | — | Put on the field, so a <Label for> points at it. |
modelValue | string | — | The password. |
placeholder | string | — | The field's placeholder. |
ariaInvalid | boolean | — | Sets aria-invalid on the field. |
autocomplete | string | — | Passed to the field. Use current-password to sign in and new-password to set one. |
inheritAttrs is off and every other attribute is bound onto the inner field rather than the wrapper.
Emits
| Event | Payload | Description |
|---|---|---|
update:modelValue | value: string | Fires on input. |
Slots
None.
Accessibility
- The toggle is named "Show password" or "Hide password" as its state changes, and carries
aria-pressed. - The toggle is out of the tab order, so it never sits between the field and the submit button. A screen reader still reaches it in browse mode.
- The icon flips between an open and a struck-through eye, so the state does not rest on color.
aria-describedbyand the rest fall through to the field, which is where an error message has to be announced from.