<script setup lang="ts">
import { FieldControl, FieldError, FieldLabel, FieldRoot, FormRoot } from 'akar';
import { ref } from 'vue';
import './styles.css';
const serverErrors = ref<Record<string, string>>({});
const submittedUsername = ref('');
function onFormSubmit(values: Record<string, unknown>) {
submittedUsername.value = '';
const username = values.username;
// Pretend this came back from an API call.
if (username === 'taken') {
serverErrors.value = { username: 'That username is already taken.' };
} else {
serverErrors.value = {};
submittedUsername.value = String(username);
}
}
</script>
<template>
<div class="akar-demo akar-demo-Form">
<FormRoot
class="FormRoot"
:errors="serverErrors"
@form-submit="onFormSubmit"
>
<FieldRoot
class="FieldRoot"
name="username"
required
validation-mode="onBlur"
>
<FieldLabel class="FieldLabel">
Username
</FieldLabel>
<FieldControl
placeholder="Try 'taken'"
class="FieldControl"
/>
<FieldError class="FieldError" />
</FieldRoot>
<button
type="submit"
class="SubmitButton"
>
Submit
</button>
<p
v-if="submittedUsername"
class="SuccessMessage"
>
Submitted as "{{ submittedUsername }}".
</p>
</FormRoot>
</div>
</template>@scope (.akar-demo-Form) {
.FormRoot {
display: flex;
width: 256px;
flex-direction: column;
gap: 12px;
}
.FieldRoot {
display: flex;
flex-direction: column;
gap: 6px;
}
.FieldLabel {
font-size: 14px;
font-weight: 500;
color: white;
}
.FieldControl {
border-radius: 6px;
border: 1px solid #404040;
background-color: #171717;
padding: 8px 12px;
font-size: 14px;
color: white;
outline: none;
}
.FieldControl:focus {
box-shadow: 0 0 0 2px #10b981;
}
.FieldControl[data-invalid] {
border-color: #ef4444;
}
.FieldError {
font-size: 12px;
color: #ef4444;
}
.SubmitButton {
border-radius: 6px;
background-color: #047857;
padding: 8px 12px;
font-size: 14px;
font-weight: 500;
color: white;
border: none;
cursor: pointer;
}
.SubmitButton:hover {
background-color: #065f46;
}
.SuccessMessage {
font-size: 12px;
color: #10b981;
}
}Features
- Runs every Field's validation on submit and focuses the first invalid control instead of submitting.
- Displays server-side errors by field name, cleared automatically once the user edits that field.
- Hands you the value of every named field through
formSubmit, or leaves a valid form to submit natively. - Sets a default
validationModefor every Field inside it. - Native
resetclears every Field's touched/dirty/error state.
Installation
Install the component from your command line.
pnpm add akar
yarn add akar
npm install akar
bun add akar
Anatomy
Import all parts and piece them together. See the Field docs for FieldRoot and its parts.
<script setup>
import { FieldControl, FieldError, FieldLabel, FieldRoot, FormRoot } from 'akar';
</script>
<template>
<FormRoot
:errors="serverErrors"
@form-submit="onFormSubmit"
>
<FieldRoot
name="email"
required
>
<FieldLabel>Email</FieldLabel>
<FieldControl type="email" />
<FieldError />
</FieldRoot>
</FormRoot>
</template>
API Reference
Root
Extends the native form element. Provides context that every descendant FieldRoot optionally reads for server errors, submit-time validation, and reset handling. Renders with novalidate, so the browser's own validation popups don't pre-empt Field errors.
Exposes a validate(name?) method through a template ref, which validates every field (or only the named one) and returns whether they're valid.
| Prop | Default | Type |
|---|---|---|
as | "form" | AsTag | ComponentThe element or component this component should render as. Can be overwritten by |
asChild | booleanChange the default rendered element for the one passed as a child, merging their props and behavior. Read our Composition guide for more details. | |
errors | Record<string, string | string[]>A map of field | |
validationMode | "onSubmit" | "onBlur" | "onChange" | "onSubmit"When fields validate. The
|
| Event | Payload |
|---|---|
formSubmit | values: Record<string, unknown>, event: SubmitEventEmitted after |
submit | event: SubmitEventEmitted with the native submit event once every field has validated
successfully. When any field is invalid, the native submission is
prevented and this isn't emitted. Otherwise the form submits natively
(e.g. to its |
| Method | Type |
|---|---|
validate | (fieldName?: string | undefined) => booleanValidates every field, or only the field with the given |
Examples
Server-side errors
Pass an errors map keyed by field name. The matching FieldRoot's FieldError displays it until the user edits that field, or a new value for that key is provided. When errors arrive in response to a submit, focus moves to the first field they invalidate.
<script setup>
import { FieldControl, FieldError, FieldLabel, FieldRoot, FormRoot } from 'akar'
import { ref } from 'vue'
const serverErrors = ref({})
async function onFormSubmit(values) {
const result = await api.createAccount(values)
if (result.error)
serverErrors.value = { email: result.error }
}
</script>
<template>
<FormRoot :errors="serverErrors" @form-submit="onFormSubmit">
<FieldRoot name="email">
<FieldLabel>Email</FieldLabel>
<FieldControl type="email" />
<FieldError v-slot="{ errors }">
{{ errors[0] }}
</FieldError>
</FieldRoot>
</FormRoot>
</template>
Submit gating
On submit, FormRoot runs every field's validation. If any field is invalid, it prevents the native submission and focuses the first invalid field's control, in document order. Otherwise it emits submit with the native event, then:
- With a
formSubmitlistener, it prevents the native submission and emitsformSubmitwith the value of every named field. - Without one, the form submits natively (for example to its
action), unless you callevent.preventDefault()in yoursubmithandler.
Only synchronous results gate the submission. An async validate runs, and shows its errors once it resolves, but doesn't hold the submission back in onSubmit mode.
<template>
<FormRoot @form-submit="onFormSubmit">
<FieldRoot name="email" required>
<FieldLabel>Email</FieldLabel>
<FieldControl type="email" />
<FieldError match="valueMissing">
Email is required
</FieldError>
</FieldRoot>
</FormRoot>
</template>
Validation timing
Set validation-mode on FormRoot to change when every field inside it validates. A FieldRoot's own validation-mode takes precedence.
<template>
<FormRoot validation-mode="onBlur">
<!-- ... -->
</FormRoot>
</template>
Accessibility
Renders a native form element. Focus moves to the first invalid field's control when a submit is blocked, so keyboard and screen-reader users land directly on the field that needs attention.