Examples
Basic
Prop | Default | Type | Description |
---|---|---|---|
label | - | string | Adds a label to the form field. |
name | - | string | Adds a name to the form field. |
Clicking the label focuses the input. We automatically add for
to the label and id
to the input. Override this by defining for
and id
manually.
Building form with vee-validate and zod using FormField component.
Preview
Code
<script setup lang="ts">
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
const formSchema = toTypedSchema(z.object({
username: z.string().min(2).max(50),
password: z.string().min(6).max(50),
}))
const { handleSubmit, validate, errors } = useForm({
validationSchema: formSchema,
})
const onSubmit = handleSubmit((values) => {
// eslint-disable-next-line no-alert
alert(JSON.stringify(values, null, 2))
})
async function onValidating() {
await validate()
const firstErrorField = Object.keys(errors.value)[0]
if (firstErrorField) {
const firstErrorFieldElement = document.querySelector(`[name=${firstErrorField}]`) as HTMLElement
if (firstErrorFieldElement) {
firstErrorFieldElement.focus()
firstErrorFieldElement?.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
}
onSubmit()
}
</script>
<template>
<form
class="space-y-4"
@submit.prevent="onValidating()"
>
<NFormField
name="username"
label="Username"
>
<NInput placeholder="username" />
</NFormField>
<NFormField
name="password"
label="Password"
>
<NInput type="password" placeholder="password" />
</NFormField>
<NButton type="submit">
Submit
</NButton>
</form>
</template>
Read more in Label component
Required
Prop | Default | Type | Description |
---|---|---|---|
required | false | boolean | Adds * to the label. |
Preview
Code
<template>
<div class="flex">
<NFormField
label="Email"
name="email"
required
>
<NInput
placeholder="phojrengel@gmail.com"
leading="i-heroicons-envelope-20-solid"
/>
</NFormField>
</div>
</template>
Description
Prop | Default | Type | Description |
---|---|---|---|
description | - | string | Adds a description to the form field. |
Preview
Code
We'll never share your email with anyone else.
<template>
<div class="flex">
<NFormField
label="Email"
required
name="email"
description="We'll never share your email with anyone else."
>
<NInput
placeholder="phojrengel@gmail.com"
leading="i-heroicons-envelope-20-solid"
/>
</NFormField>
</div>
</template>
Hint
Prop | Default | Type | Description |
---|---|---|---|
hint | - | string | Adds a hint to the form field. |
Preview
Code
Optional
<template>
<div class="flex">
<NFormGroup
label="Email"
name="email"
hint="Optional"
>
<NInput
placeholder="phojrengel@gmail.com"
leading="i-heroicons-envelope-20-solid"
/>
</NFormGroup>
</div>
</template>
Message
Prop | Default | Type | Description |
---|---|---|---|
message | - | string | Sets the form field's message. |
Preview
Code
We'll never share your email with anyone else.
<template>
<div class="flex">
<NFormGroup
label="Email"
name="email"
message="We'll never share your email with anyone else."
>
<NInput
placeholder="phojrengel@gmail.com"
leading="i-heroicons-envelope-20-solid"
/>
</NFormGroup>
</div>
</template>
Status
Prop | Default | Type | Description |
---|---|---|---|
status | - | info ,success , warning , error , undefined | Sets the form field's status. |
Notice that when you change the status
prop, the message
prop and the child component status
prop are automatically updated.
Preview
Code
Your username is available.
This information will be visible to other users.
Your email is invalid
Your password is weak.
<template>
<div class="flex flex-col gap-4">
<NFormField
label="Username"
name="username"
message="Your username is available."
status="success"
>
<NInput />
</NFormField>
<NFormField
label="About Me"
name="about"
status="info"
message="This information will be visible to other users."
>
<NInput type="textarea" />
</NFormField>
<NFormField
label="Email"
status="error"
name="email"
message="Your email is invalid"
>
<NInput />
</NFormField>
<NFormGroup
label="Password"
name="password"
message="Your password is weak."
status="warning"
>
<NInput
type="password"
/>
</NFormGroup>
</div>
</template>
Slots
Name | Props | Description |
---|---|---|
default | - | The default slot of the form field, refer Basic section. |
top | - | The top section of the form field. |
bottom | - | The bottom section of the form field. |
label | - | The label slot of the form field. |
description | - | The description slot of the form field. |
hint | - | The hint slot of the form field. |
message | - | The message slot of the form field. |
Props
types/form.ts
import type { HTMLAttributes } from 'vue'
import type { NLabelProps } from './label'
export interface NFormFieldProps extends NLabelProps {
class?: HTMLAttributes['class']
/**
* Update the form field status.
*
* @default null
*/
status?: 'info' | 'success' | 'warning' | 'error'
/**
* Add a required indicator to the form field.
*
* @default false
*/
required?: boolean
/**
* Add a name attribute to the form field.
*
*/
name: string
/**
* Manually set the id attribute.
*
* By default, the id attribute is generated randomly for accessibility reasons.
*
* @default randomId
* @example
* id="email"
*/
id?: string
/**
* Label for the form field.
*
* @example
* label="Email"
*/
label?: string
/**
* Display `hint` message for the form field.
*
* @example
* hint="Enter your email address"
*/
hint?: any
/**
* Display `Description` message for the form field.
*
* @example
* description="We will never share your email with anyone else."
*/
description?: any
/**
* Display `Message` for the form field.
* Useful for displaying validation errors.
*
* @example
* message="Email is required"
*/
message?: any
/**
* `UnaUI` preset configuration
*
* @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/form.ts
*/
una?: NFormUnaProps
}
export interface NFormMessageProps {
class?: HTMLAttributes['class']
una?: NFormUnaProps['formMessage']
}
export interface NFormLabelProps extends NLabelProps {
una?: NFormUnaProps['formLabel']
}
export interface NFormItemProps {
class?: HTMLAttributes['class']
una?: NFormUnaProps['formItem']
}
export interface NFormDescriptionProps {
class?: HTMLAttributes['class']
una?: NFormUnaProps['formDescription']
}
interface NFormUnaProps {
formField?: HTMLAttributes['class']
formFieldTopWrapper?: HTMLAttributes['class']
formFieldTopWrapperInner?: HTMLAttributes['class']
formFieldBottomWrapper?: HTMLAttributes['class']
formFieldCounterWrapper?: HTMLAttributes['class']
formFieldMessageWrapper?: HTMLAttributes['class']
formFieldLabelWrapper?: HTMLAttributes['class']
formFieldHint?: HTMLAttributes['class']
formMessage?: HTMLAttributes['class']
formLabel?: HTMLAttributes['class']
formItem?: HTMLAttributes['class']
formDescription?: HTMLAttributes['class']
formFieldLabelRequired?: HTMLAttributes['class']
}
Presets
shortcuts/form.ts
type FormPrefix = 'form'
export const staticForm: Record<`${FormPrefix}-${string}` | FormPrefix, string> = {
// base
'form': '',
'form-field': '',
'form-field-description': 'text-muted',
'form-field-hint': 'text-sm leading-none text-muted',
'form-field-message': '',
// wrappers
'form-field-top-wrapper': 'flex flex-col space-y-1.5 pb-0.5',
'form-field-top-wrapper-inner': 'flex justify-between items-end space-x-1.5',
'form-field-bottom-wrapper': 'flex space-x-1.5 justify-between items-start',
'form-field-message-wrapper': '',
// label
'form-field-label-wrapper': 'flex',
'form-field-label': 'block',
'form-field-label-required': 'after:content-[\'*\'] after:ms-0.5 after:text-error',
'form-message': 'text-0.8rem font-medium transition-all duration-1000 ease-in-out text-error',
'form-label': '',
'form-description': 'text-sm text-muted',
'form-item': 'space-y-2',
}
export const form = [
staticForm,
]
Components
FormField.vue
FormControl.vue
FormDescription.vue
FormHint.vue
FormLabel.vue
FormMessage.vue
<script setup lang="ts">
import type { NFormFieldProps } from '../../../types'
import { Field } from 'vee-validate'
import { computed } from 'vue'
import { cn } from '../../../utils'
import FormFieldDefaultSlot from '../../slots/FormFieldDefault'
import FormControl from './FormControl.vue'
import FormDescription from './FormDescription.vue'
import FormItem from './FormItem.vue'
import FormLabel from './FormLabel.vue'
import FormMessage from './FormMessage.vue'
defineOptions({
inheritAttrs: false,
})
const props = defineProps<NFormFieldProps>()
const statusClassVariants = computed(() => {
const text = {
info: 'text-info',
success: 'text-success',
warning: 'text-warning',
error: 'text-error',
default: 'text-muted',
}
return text[props.status ?? 'default']
})
</script>
<template>
<Field
v-slot="{ componentField, errorMessage }"
:name
>
<FormItem
:class="cn(
props.class,
)"
>
<slot name="top">
<div
:class="cn(
'form-field-top-wrapper',
una?.formFieldTopWrapper,
)"
>
<div
v-if="label || hint || description"
:class="cn(
'form-field-top-wrapper',
una?.formFieldTopWrapper,
)"
>
<div
v-if="label || hint"
:class="cn(
'form-field-top-wrapper-inner',
una?.formFieldTopWrapperInner,
)"
>
<div
:class="cn(
'form-field-label-wrapper',
una?.formFieldLabelWrapper,
)"
>
<slot name="label">
<FormLabel
:una
>
<span>
{{ label }}
</span>
<span
v-if="required"
:class="cn(
'form-field-label-required',
una?.formFieldLabelRequired,
)"
/>
</FormLabel>
</slot>
</div>
<slot name="hint">
<span
v-if="hint"
:class="cn(
'form-field-hint',
una?.formFieldHint,
)"
>
{{ hint }}
</span>
</slot>
</div>
<slot name="description">
<FormDescription
v-if="description"
:una
>
{{ description }}
</FormDescription>
</slot>
</div>
</div>
</slot>
<FormControl>
<FormFieldDefaultSlot
:status="!errorMessage ? status : 'error'"
v-bind="componentField"
>
<slot />
</FormFieldDefaultSlot>
</FormControl>
<slot name="bottom">
<div
:class="cn(
'form-field-bottom-wrapper',
una?.formFieldBottomWrapper,
)"
>
<slot name="message">
<div
v-if="message && !errorMessage"
:class="cn(
'form-field-message-wrapper',
una?.formFieldMessageWrapper,
)"
>
<FormDescription
:class="cn(
'form-field-message',
una?.formMessage,
statusClassVariants,
)"
>
{{ message }}
</FormDescription>
</div>
<FormMessage
v-else
/>
</slot>
</div>
</slot>
</FormItem>
</Field>
</template>
<script lang="ts" setup>
import { Slot } from 'radix-vue'
import { useFormField } from '../../../composables/useFormField'
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
</script>
<template>
<Slot
:id="formItemId"
:aria-describedby="!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`"
:aria-invalid="!!error"
>
<slot />
</Slot>
</template>
<script lang="ts" setup>
import type { NFormDescriptionProps } from '../../../types'
import { useFormField } from '../../../composables/useFormField'
import { cn } from '../../../utils'
const props = defineProps<NFormDescriptionProps>()
const { formDescriptionId } = useFormField()
</script>
<template>
<p
:id="formDescriptionId"
:class="cn(
'form-description',
props.una?.formDescription,
props.class,
)"
>
<slot />
</p>
</template>
<script lang="ts" setup>
import type { NFormMessageProps } from '../../../types'
import { ErrorMessage } from 'vee-validate'
import { toValue } from 'vue'
import { useFormField } from '../../../composables/useFormField'
import { cn } from '../../../utils'
const props = defineProps<NFormMessageProps>()
const { name, formMessageId } = useFormField()
</script>
<template>
<ErrorMessage
:id="formMessageId"
as="p"
:name="toValue(name)"
:class="cn(
'form-message',
props.una?.formMessage,
props.class,
)"
/>
</template>
<script lang="ts" setup>
import type { NFormLabelProps } from '../../../types'
import { useFormField } from '../../../composables/useFormField'
import { cn } from '../../../utils'
import Label from '../../elements/Label.vue'
const props = defineProps<NFormLabelProps>()
const { formItemId } = useFormField()
</script>
<template>
<Label
:class="cn(
'form-label',
props.una?.formLabel,
props.class,
)"
:for="formItemId"
>
<slot />
</Label>
</template>
<script lang="ts" setup>
import type { NFormItemProps } from '../../../types'
import { provide } from 'vue'
import { cn, randomId } from '../../../utils'
import { FORM_ITEM_INJECTION_KEY } from '../../../utils/injectionKeys'
const props = defineProps<NFormItemProps>()
const id = randomId('form')
provide(FORM_ITEM_INJECTION_KEY, id)
</script>
<template>
<div
:class="cn(
'form-item',
props.una?.formItem,
props.class,
)"
>
<slot />
</div>
</template>
Dropdown Menu
Displays a menu to the user — such as a set of actions or functions — triggered by a button.
Form Group
A versatile wrapper for various form components such as `Input`, `Textarea`, `Select`, and more. It offers a comprehensive set of features, including label, description, hint, message, status, and others.