Field
Groups a label, a control, a description and an error message, and owns the validity state they all read.
Wrap a control in Field and it picks up a label, a description, an error
message and the validity state behind them. Reach for it whenever a control
needs a name, a hint, or something to say when the value is wrong.
Three characters or more. Validated when you leave the field.
<template>
<Field.Root
class="group flex w-[17rem] flex-col gap-1.5"
:options="{ name: 'username', validate: validateUsername }"
>
<div :class="box">
<div :class="stack">
<span :class="line">
<Input
v-model="username"
:class="control"
placeholder=" "
autocomplete="off"
/>
</span>
<Field.Label :class="label">Username</Field.Label>
</div>
</div>
<Field.Description :class="hint">
Three characters or more. Validated when you leave the field.
</Field.Description>
<Field.Error v-slot="{ messages }" :class="error">
{{ messages.join(' ') }}
</Field.Error>
</Field.Root>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Field } from '@maas/mirror/vue/Field'
import { Input } from '@maas/mirror/vue/Input'
const username = ref('')
const box = [
'relative isolate flex h-12 w-full cursor-text items-center justify-between gap-1.5',
'rounded-component-lg border-surface border-2 px-[0.875rem]',
'transition-all duration-100 ease-linear [&_*]:transition-all [&_*]:duration-100 [&_*]:ease-linear',
'outline-4 outline-transparent focus-within:focus-ring',
'group-[[data-invalid=true]:not([data-focused=true])]:border-danger-subtle',
'group-[[data-invalid=true]:not([data-focused=true])]:bg-danger-subtle',
'group-data-[disabled=true]:border-disabled-subtle group-data-[disabled=true]:cursor-not-allowed',
].join(' ')
const stack = [
'group/stack flex h-full max-h-full w-full flex-col-reverse',
'items-center justify-center gap-0 px-1',
'focus-within:gap-1 has-[input:not(:placeholder-shown)]:gap-1',
].join(' ')
const line = [
'relative flex h-0 w-full items-end',
'group-focus-within/stack:h-[0.9375rem]',
'has-[input:not(:placeholder-shown)]:h-[0.9375rem]',
].join(' ')
const control = [
'type-component-lg leading-[normal]! text-surface',
'block box-content h-[1lh] w-full py-1.25 -my-1.25 appearance-none bg-transparent outline-none',
'placeholder:text-transparent',
'group-[[data-invalid=true]:not([data-focused=true])]:text-danger-on-muted',
'data-[disabled=true]:text-disabled-solid',
].join(' ')
const label = [
'type-component-lg leading-[normal]! text-surface-muted',
'flex h-full w-full items-center [--mirror-label-cursor:text]',
"before:absolute before:-inset-0.5 before:-z-10 before:content-['']",
'group-focus-within/stack:h-auto group-focus-within/stack:text-[0.6875rem] group-focus-within/stack:[--mirror-label-cursor:auto]',
'group-has-[input:not(:placeholder-shown)]/stack:h-auto group-has-[input:not(:placeholder-shown)]/stack:text-[0.6875rem] group-has-[input:not(:placeholder-shown)]/stack:[--mirror-label-cursor:auto]',
'group-[[data-invalid=true]:not([data-focused=true])]:text-danger-muted',
'group-data-[disabled=true]:text-disabled-muted',
].join(' ')
const hint = 'type-component-2xs text-surface-muted'
const error = 'type-component-2xs text-danger-muted'
function validateUsername(value: unknown) {
const entry = String(value ?? '')
if (entry.length === 0) {
return 'Enter a username.'
}
return entry.length < 3 ? 'Use at least three characters.' : null
}
</script>
Usage guidelines
- Only one control may register per field, since it owns
data-focusedand thearia-describedbychain, and a second one throwsduplicate_field_control.must - A control reports its initial value as it registers, which counts as a change, so
onChangevalidation leaves an empty required field invalid before the user has touched it. We default toonBlurfor that reason.should - Give a second
Field.DescriptionorField.Errorits ownid, or both take the field’s generated ID andaria-describedbylists it once.must - In
onSubmitmode the browser blocks its ownsubmitevent while a native control fails a constraint, so the field never hears it. Wrap the fields inForm, which setsnovalidatefor you, or setnovalidateon your own<form>.must