Form Field
A wrapper component that provides a label, hint text, and error message around any form control. Instead of each input managing its own label and validation display, CoarFormField handles these concerns in one place.
import { CoarFormField } from '@cocoar/vue-ui';Basic Usage
Wrap any form control in CoarFormField and pass label, hint, or error props. The label is automatically associated with the input inside via generated IDs.
<template>
<div style="display: flex; flex-direction: column; gap: 16px; max-width: 320px;">
<CoarFormField label="Full Name" hint="Enter your first and last name">
<CoarTextInput v-model="name" placeholder="Jane Doe" />
</CoarFormField>
<CoarFormField label="Email" error="Please enter a valid email address">
<CoarTextInput value="not-an-email" />
</CoarFormField>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { CoarTextInput, CoarFormField } from '@cocoar/vue-ui';
const name = ref('');
</script>
Grouping Controls
Use CoarFormField to add a group label and shared error to a set of checkboxes or radio buttons. Each checkbox keeps its own inline label prop for the option text.
<template>
<div style="max-width: 400px;">
<CoarFormField
label="Permissions"
:error="permissionsError"
hint="Select at least one permission"
required
>
<div style="display: flex; flex-direction: column; gap: 8px;">
<CoarCheckbox v-model="read" label="Read — View content" />
<CoarCheckbox v-model="write" label="Write — Create and edit content" />
<CoarCheckbox v-model="admin" label="Admin — Manage users and settings" />
</div>
</CoarFormField>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { CoarCheckbox, CoarFormField } from '@cocoar/vue-ui';
const read = ref(false);
const write = ref(false);
const admin = ref(false);
const permissionsError = computed(() => {
if (!read.value && !write.value && !admin.value) {
return 'At least one permission is required';
}
return '';
});
</script>
Registration Form
A complete registration form with submit-on-click validation. Errors only appear after the user attempts to submit.
<template>
<form class="demo-form" @submit.prevent="onSubmit">
<CoarFormField label="Full Name" :error="errors.name" required>
<CoarTextInput v-model="form.name" placeholder="Jane Doe" required />
</CoarFormField>
<CoarFormField label="Email" :error="errors.email" hint="We'll never share your email" required>
<CoarTextInput v-model="form.email" placeholder="jane@example.com" required />
</CoarFormField>
<CoarFormField label="Password" :error="errors.password" hint="At least 8 characters" required>
<CoarPasswordInput v-model="form.password" required />
</CoarFormField>
<CoarFormField label="Role" :error="errors.role" required>
<CoarSelect v-model="form.role" :options="roles" placeholder="Select a role..." />
</CoarFormField>
<CoarFormField label="Department">
<CoarSelect v-model="form.department" :options="departments" placeholder="Optional..." />
</CoarFormField>
<CoarFormField :error="errors.terms">
<CoarCheckbox v-model="form.terms" label="I agree to the terms and conditions" />
</CoarFormField>
<div class="demo-form__actions">
<CoarButton type="submit" :disabled="!isValid">Create Account</CoarButton>
</div>
<CoarNote v-if="submitted" variant="success" padding="s">
Account created successfully!
</CoarNote>
</form>
</template>
<script setup lang="ts">
import { reactive, ref, computed } from 'vue';
import { CoarFormField, CoarTextInput, CoarPasswordInput, CoarSelect, CoarCheckbox, CoarButton, CoarNote } from '@cocoar/vue-ui';
import type { CoarSelectOption } from '@cocoar/vue-ui';
const form = reactive({
name: '',
email: '',
password: '',
role: null as string | null,
department: null as string | null,
terms: false,
});
const submitted = ref(false);
const touched = ref(false);
const roles: CoarSelectOption<string>[] = [
{ value: 'developer', label: 'Developer' },
{ value: 'designer', label: 'Designer' },
{ value: 'manager', label: 'Manager' },
{ value: 'qa', label: 'QA Engineer' },
];
const departments: CoarSelectOption<string>[] = [
{ value: 'engineering', label: 'Engineering' },
{ value: 'product', label: 'Product' },
{ value: 'marketing', label: 'Marketing' },
];
const errors = computed(() => {
if (!touched.value) return { name: '', email: '', password: '', role: '', terms: '' };
return {
name: form.name.length === 0 ? 'Name is required' : '',
email: form.email.length === 0 ? 'Email is required' : !form.email.includes('@') ? 'Enter a valid email' : '',
password: form.password.length === 0 ? 'Password is required' : form.password.length < 8 ? 'At least 8 characters' : '',
role: !form.role ? 'Please select a role' : '',
terms: !form.terms ? 'You must accept the terms' : '',
};
});
const isValid = computed(() =>
form.name.length > 0 &&
form.email.includes('@') &&
form.password.length >= 8 &&
form.role !== null &&
form.terms
);
function onSubmit() {
touched.value = true;
if (isValid.value) {
submitted.value = true;
}
}
</script>
<style scoped>
.demo-form {
display: flex;
flex-direction: column;
gap: var(--coar-spacing-m);
max-width: 400px;
}
.demo-form__actions {
padding-top: var(--coar-spacing-s);
}
</style>
Settings Panel
A settings page using every form control type — text inputs, textareas, selects, checkboxes, radio groups, and switches — all wrapped in CoarFormField for consistent layout.
<template>
<form class="demo-form" @submit.prevent>
<CoarFormField label="Display Name" hint="This is shown publicly">
<CoarTextInput v-model="displayName" placeholder="Your display name" />
</CoarFormField>
<CoarFormField label="Bio">
<CoarTextInput v-model="bio" placeholder="Tell us about yourself..." :rows="3" />
</CoarFormField>
<CoarFormField label="Language">
<CoarSelect v-model="language" :options="languages" />
</CoarFormField>
<CoarFormField label="Notifications">
<div class="demo-checks">
<CoarCheckbox v-model="emailNotifs" label="Email notifications" />
<CoarCheckbox v-model="pushNotifs" label="Push notifications" />
<CoarCheckbox v-model="weeklyDigest" label="Weekly digest" />
</div>
</CoarFormField>
<CoarFormField label="Theme">
<CoarRadioGroup v-model="theme" name="theme" orientation="horizontal">
<CoarRadioButton value="light" label="Light" />
<CoarRadioButton value="dark" label="Dark" />
<CoarRadioButton value="system" label="System" />
</CoarRadioGroup>
</CoarFormField>
<CoarFormField label="Experimental Features">
<CoarSwitch v-model="experiments" label="Enable beta features" />
</CoarFormField>
<div class="demo-form__actions">
<CoarButton>Save Settings</CoarButton>
</div>
</form>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { CoarFormField, CoarTextInput, CoarSelect, CoarCheckbox, CoarRadioGroup, CoarRadioButton, CoarSwitch, CoarButton } from '@cocoar/vue-ui';
import type { CoarSelectOption } from '@cocoar/vue-ui';
const displayName = ref('Jane Doe');
const bio = ref('');
const language = ref('en');
const emailNotifs = ref(true);
const pushNotifs = ref(false);
const weeklyDigest = ref(true);
const theme = ref('system');
const experiments = ref(false);
const languages: CoarSelectOption<string>[] = [
{ value: 'en', label: 'English' },
{ value: 'de', label: 'Deutsch' },
{ value: 'fr', label: 'Français' },
{ value: 'es', label: 'Español' },
];
</script>
<style scoped>
.demo-form {
display: flex;
flex-direction: column;
gap: var(--coar-spacing-m);
max-width: 400px;
}
.demo-checks {
display: flex;
flex-direction: column;
gap: var(--coar-spacing-s);
}
.demo-form__actions {
padding-top: var(--coar-spacing-s);
}
</style>
Validation with vee-validate
CoarFormField integrates seamlessly with vee-validate and Zod schemas. Use useField() to get reactive value and errorMessage refs, then bind them to the input and CoarFormField respectively.
<template>
<form class="demo-form" @submit="onSubmit">
<CoarFormField label="Email" :error="emailError" required>
<CoarTextInput v-model="email" placeholder="jane@example.com" required />
</CoarFormField>
<CoarFormField label="Password" :error="passwordError" hint="At least 8 characters" required>
<CoarPasswordInput v-model="password" required />
</CoarFormField>
<CoarFormField label="Confirm Password" :error="confirmError" required>
<CoarPasswordInput v-model="confirm" required />
</CoarFormField>
<CoarFormField :error="termsError">
<CoarCheckbox v-model="terms" label="I accept the terms of service" />
</CoarFormField>
<div class="demo-form__actions">
<CoarButton type="submit">Sign Up</CoarButton>
</div>
<CoarNote v-if="submitted" variant="success" padding="s">
Form submitted successfully!
</CoarNote>
</form>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useForm, useField } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/zod';
import { z } from 'zod';
import { CoarFormField, CoarTextInput, CoarPasswordInput, CoarCheckbox, CoarButton, CoarNote } from '@cocoar/vue-ui';
const schema = toTypedSchema(
z.object({
email: z.string().min(1, 'Email is required').email('Enter a valid email'),
password: z.string().min(8, 'At least 8 characters'),
confirm: z.string().min(1, 'Please confirm your password'),
terms: z.literal(true, { errorMap: () => ({ message: 'You must accept the terms' }) }),
}).refine((data) => data.password === data.confirm, {
message: 'Passwords do not match',
path: ['confirm'],
}),
);
const submitted = ref(false);
const { handleSubmit } = useForm({ validationSchema: schema });
const { value: email, errorMessage: emailError } = useField<string>('email', undefined, { initialValue: '' });
const { value: password, errorMessage: passwordError } = useField<string>('password', undefined, { initialValue: '' });
const { value: confirm, errorMessage: confirmError } = useField<string>('confirm', undefined, { initialValue: '' });
const { value: terms, errorMessage: termsError } = useField<boolean>('terms', undefined, { initialValue: false });
const onSubmit = handleSubmit(() => {
submitted.value = true;
});
</script>
<style scoped>
.demo-form {
display: flex;
flex-direction: column;
gap: var(--coar-spacing-m);
max-width: 400px;
}
.demo-form__actions {
padding-top: var(--coar-spacing-s);
}
</style>
Other validation libraries
CoarFormField is library-agnostic — it just takes an error string. Any validation approach works: vee-validate, vuelidate, or plain computed properties.
Standalone Form Controls
Form controls work without CoarFormField when no label or validation is needed — inline search inputs, table checkboxes, toolbar buttons.
<!-- No label needed -->
<CoarTextInput v-model="search" placeholder="Search..." />
<!-- Inline checkbox with its own label -->
<CoarCheckbox v-model="agree" label="I agree" />Accessibility
CoarFormField generates unique IDs automatically:
- Label:
<label for="...">points to the input — clicking the label focuses the control - Error/Hint: linked via
aria-describedby— screen readers announce the message on focus - Required: asterisk is
aria-hidden="true"— userequiredon the input for semantics
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | undefined | Label text displayed above the input |
error | string | '' | Error message — overrides hint when set |
hint | string | '' | Hint text displayed below the input |
required | boolean | false | Show required asterisk next to label |
disabled | boolean | false | Disabled state — propagated to child inputs |
id | string | auto | Explicit input ID (auto-generated if omitted) |
Slots
| Slot | Description |
|---|---|
default | The form control(s) to wrap |