Your first form
Introduction
Let's start by creating our first FormKit form! We'll learn some of FormKit's key features and how they benefit you. We'll also pick up some nice tips along the way — like how to manage form state without using v-model.
This guide assumes you are are familiar with the Vue Composition API.
Our first input
One of the main features of FormKit is its single component API — the <FormKit /> component. This one component gives you access to all input types. And while some types may extend and add features, they share the same base functionality. You can learn more about inputs here.
Even without any props, the bare <FormKit /> component has already given our input a great starting point, with accessible markup, a base text input type, and additional features that will be explained in later sections.
Basic props
The type
By default, the <FormKit /> component will use type="text" if no type is specified. The type is how we specify what input we want. Just like native inputs, we have inputs like text, select, checkbox and so on. However, we are not confined to only "native" inputs, FormKit Pro adds non-native controls like the repeater, taglist, and autocomplete types, which can handle more complex interactions.
<template> <FormKit type="text" /></template>The name and id
If you look at the HTML generated by the previous example, you will see that FormKit already created accessible markup. However, as we did not specify the name and id properties, they were auto-generated for us: name: "text_1" id="input_0". Even so, as a best practice, we should always at least specify the name as it makes using inputs inside a form easier. The name is used by the form and group types to collect values from, and pass values down to, their children based on the name:
<template> <!-- Always specify a name --> <!-- and optionally an id if you need to target a specific input --> <FormKit type="text" name="name" id="name" /></template>Props for accessibility
Our input is still missing some key accessibility functionality like a label, help, and maybe even a placeholder. FormKit accepts all these as props, and outputs the proper aria attributes. Screen readers will now announce the input name, the help text, and that the input is ready for editing:
<template> <FormKit type="text" name="name" id="name" label="Name" help="Your full name" placeholder="“Jon Doe”" /></template>Setting an initial value
Sometimes you want to add an initial value to an input, such as providing a sensible starting place, or populating pre-saved data from a database. We do this with the value prop.
Let's start building an example that we can add to for this guide. Imagining we are building a "character creation form" for a game. Let's assign our character a strength rating. We could use the range input with a predefined value of 5 when the users first opens the form:
<FormKit type="range" name="strength" id="strength" label="Strength" value="5" min="1" max="10" step="1" help="How many strength points should this character have?"/>Adding validation
Validation is one of the main features of FormKit. It helps the user know if the value they are submitting is correct. Adding validation is a breeze, with many powerful built-in validation rules already implemented for you. We will be using the validation prop to make sure the character is not too strong or too weak. The validation-visibility prop allows us to control when to show validation messages to the user — whether immediately, when the user blurs the input, or on form submit. The actual validity state is calculated real-time and always up to date — we just choose when to expose the messages:
<FormKit type="range" name="strength" id="strength" label="Strength" value="5" validation="min:2|max:9" validation-visibility="live" min="1" max="10" step="1" help="How many strength points should this character have?"/>Note that the min and max props above are built-in browser props for a range input, and represent the top and bottom of the range slider .
Adding a plugin
Suppose our "backend" requires that data like strength be cast to a number. By default, FormKit follows HTML "native" inputs behavior, making all values as "strings". To fix that, we can use one of the coolest features of FormKit — plugins — which can be thought of as middleware for inputs. With a plugin, which are just functions, we can change how the value of our input is returned:
<script setup>const castNumber = (node) => { node.hook.input((value, next) => next(Number(value)))}</script><template> <FormKit type="range" name="strength" id="strength" label="Strength" value="5" min="1" max="10" step="1" help="How many strength points should this character have?" :plugins="[castNumber]" /> <p>Strength is now a Number instead of a String.</p></template>Strength is now a Number instead of a String.
Creating the form
First, let's create a basic form and add more inputs so we have content to work with. We will add more features to it in each section, like validation, grouping, and changing values based on other inputs.
We will use one of the inputs called form, which will make grouping and validation of fields much easier. You just need to wrap all your inputs inside a <FormKit type="form">:
The form type will actively collect all the values from child inputs using the name of each input as a data object for you (just like group).
<template> <div><h4 class="form-label">Creating the form</h4></div> <h1>New Character</h1> <FormKit type="form" @submit="() => false"> <FormKit type="text" name="name" id="name" validation="required|not:Admin" label="Name" help="Enter your character's full name" placeholder="“Scarlet Sword”" /> <FormKit type="select" label="Class" name="class" id="class" placeholder="Select a class" :options="['Warrior', 'Mage', 'Assassin']" /> <FormKit type="range" name="strength" id="strength" label="Strength" value="5" min="1" max="10" step="1" help="How many strength points should this character have?" /> <FormKit type="range" name="skill" id="skill" validation="required|max:10" label="Skill" value="5" min="1" max="10" step="1" help="How much skill points to start with" /> <FormKit type="range" name="dexterity" id="dexterity" validation="required|max:10" label="Dexterity" value="5" min="1" max="10" step="1" help="How much dexterity points to start with" /> </FormKit></template>Creating the form
New Character
Adding the submit handler
The first feature of a form that we'll explore is that we have a @submit event ready to make our life easier when the time comes to submit our form. The @submit event gives us as the first argument all the descendant fields the form gathered from the inputs. There is no need to use numerous v-models to collect the form data. Let's add our createCharacter() submit handler:
<script setup>const castRangeToNumber = (node) => { // We add a check to add the cast only to range inputs if (node.props.type !== 'range') return node.hook.input((value, next) => next(Number(value)))}const createCharacter = async (fields) => { await new Promise((r) => setTimeout(r, 1000)) alert(JSON.stringify(fields))}</script><template> <div> <h4 class="form-label">Adding the createCharacter submit handler</h4> </div> <h1>New Character</h1> <!-- form is also an input, so it also accepts plugins --> <FormKit type="form" @submit="createCharacter" :plugins="[castRangeToNumber]" #default="{ value }" > <FormKit type="text" name="name" id="name" validation="required|not:Admin" label="Name" help="Enter your character's full name" placeholder="“Scarlet Sword”" /> <FormKit type="select" label="Class" name="class" id="class" placeholder="Select a class" :options="['Warrior', 'Mage', 'Assassin']" /> <FormKit type="range" name="strength" id="strength" label="Strength" value="5" min="1" max="10" step="1" help="How many strength points should this character have?" /> <FormKit type="range" name="skill" id="skill" validation="required|max:10" label="Skill" value="5" min="1" max="10" step="1" help="How many skill points should this character have?" /> <FormKit type="range" name="dexterity" id="dexterity" validation="required|max:10" label="Dexterity" value="5" min="1" max="10" step="1" help="How many dexterity points should this character have?" /> <pre wrap>{{ value }}</pre> </FormKit> <p>Press submit to see the collected form data.</p></template>Adding the createCharacter submit handler
New Character
Press submit to see the collected form data.
Changing the submit button
As a convenience when using type="form", the form outputs a submit button automatically. For our case, a "Submit" text does not show the intent of the form correctly. To fix that, we can use the submit-label prop, which is a form-specific feature, by adding submit-label="Create Character" to show the intent of the form:
<FormKit type="form" @submit="createCharacter" submit-label="Create Character">
<!-- Rest of our creation form -->
</FormKit>
Grouping related inputs
While the form works right now, we can see that some related inputs are separated (i.e., the form data is a flat structure where all form data are siblings). Suppose our backend needs all attributes inside an attributes property. We can use the group type to group related inputs together by a common name.
Just like the form type, you can wrap all yours fields inside a <FormKit type="group" name: "attributes">. Don't forget to add the name property:
<template> <div><h4 class="form-label">Grouping the attributes</h4></div> <h1>New Character</h1> <!-- form is also an input, so it also accepts plugins --> <FormKit type="form" @submit="createCharacter" :plugins="[castRangeToNumber]" submit-label="Create Character" #default="{ value }" > <FormKit type="text" name="name" id="name" validation="required|not:Admin" label="Name" help="Your full name" placeholder="Please add your name" /> <FormKit type="select" name="class" label="Class" id="class" placeholder="Select a class" :options="['Warrior', 'Mage', 'Assassin']" /> <FormKit type="group" name="attributes" id="attributes"> <FormKit type="range" name="strength" id="strength" label="Strength" value="5" min="1" max="10" step="1" help="How many strength points should this character have?" /> <FormKit type="range" name="skill" id="skill" validation="required|max:10" label="Skill" value="5" min="1" max="10" step="1" help="How many skill points should this character have?" /> <FormKit type="range" name="dexterity" id="dexterity" validation="required|max:10" label="Dexterity" value="5" min="1" max="10" step="1" help="How many dexterity points should this character have?" /> </FormKit> <pre wrap>{{ value }}</pre> </FormKit></template>Grouping the attributes
New Character
Going deeper
And that is it! We could stop here for an introduction on how forms and inputs work with FormKit. However, let's add some UX enhancements and use that to expose ourselves to additional concepts and features that you can use to take your forms to the next level.
Updating values based on another input
One thing we can do to improve this form is to change the character's default attributes based on the selected character class. For that, we will be using some new features:
- getNode:
getNodegets an input's core node using theiridas an identifier. Each input has an associated core node. - events:
eventslisten to changes to a certain input. - node.input(): the
inputfunction on a node lets us update the value of it.
With those features combined, we can get an input's core node, listen for and respond to events, and update a value of another field using the input function:
<script setup>import { onMounted } from 'vue'import { getNode } from '@formkit/core'const castRangeToNumber = (node) => { // We add a check to add the cast only to range inputs if (node.props.type !== 'range') return node.hook.input((value, next) => next(Number(value)))}const CHARACTER_BASE_STATS = { Warrior: { strength: 9, skill: 1, dexterity: 5, }, Mage: { strength: 5, skill: 10, dexterity: 8, }, Assassin: { strength: 5, skill: 4, dexterity: 10, },}// We add it inside a onMounted to make sure the node existsonMounted(() => { // Use the IDs of the inputs you want to get, for our case the class and the attributes group const classNode = getNode('class') const attributesNode = getNode('attributes') // Here we are listening for the 'commit' event classNode.on('commit', ({ payload }) => { // We update the value of the attributes group using its children name to pass down automatically by FormKit attributesNode.input(CHARACTER_BASE_STATS[payload]) })})const createCharacter = async (fields) => { await new Promise((r) => setTimeout(r, 1000)) alert(JSON.stringify(fields))}</script><template> <div> <h4 class="form-label">Updating values based on the class input</h4> </div> <h1>New Character</h1> <FormKit type="form" @submit="createCharacter" :plugins="[castRangeToNumber]" submit-label="Create Character" #default="{ value }" > <FormKit type="text" name="name" id="name" validation="required|not:Admin" label="Name" help="Your full name" placeholder="Please add your name" /> <FormKit type="select" label="Class" name="class" id="class" placeholder="Select a class" :options="['Warrior', 'Mage', 'Assassin']" /> <FormKit type="group" name="attributes" id="attributes"> <FormKit type="range" name="skill" id="skill" label="Skill" value="5" min="1" max="10" step="1" help="How much skill points to start with" /> <FormKit type="range" name="strength" id="strength" label="Strength" value="5" min="1" max="10" step="1" help="How much strength points to start with" /> <FormKit type="range" name="dexterity" id="dexterity" label="Dexterity" value="5" min="1" max="10" step="1" help="How many dexterity points should this character have?" /> </FormKit> <pre wrap>{{ value }}</pre> </FormKit> <p>Change the character's class to see the changes in attribute values.</p></template>Updating values based on the class input
New Character
Change the character's class to see the changes in attribute values.
Make it into a plugin
The code now got a bit less readable, so let's extract the logic to another file to create a plugin instead. Note that we are placing the new updateAttributesPlugin only on the class input, so it will not affect any other input. We will also learn another useful feature called traversal by using the at function of a node:
The at function uses the name attribute instead of the id that getNode uses.
<script setup>import { castRangeToNumber, updateAttributesPlugin } from './plugins.js'const createCharacter = async (fields) => { await new Promise((r) => setTimeout(r, 1000)) alert(JSON.stringify(fields))}</script><template> <div><h4 class="form-label">Extract logic to a plugin</h4></div> <h1>New Character</h1> <FormKit type="form" @submit="createCharacter" :plugins="[castRangeToNumber]" submit-label="Create Character" #default="{ value }" > <FormKit type="text" name="name" id="name" validation="required|not:Admin" label="Name" help="Your full name" placeholder="Please add your name" /> <FormKit type="select" label="Class" name="class" id="class" placeholder="Select a class" :options="['Warrior', 'Mage', 'Assassin']" :plugins="[updateAttributesPlugin]" /> <FormKit type="group" name="attributes" id="attributes"> <FormKit type="range" name="strength" id="strength" label="Strength" value="5" min="1" max="10" step="1" help="How many strength points should this character have?" /> <FormKit type="range" name="skill" id="skill" validation="required|max:10" label="Skill" value="5" min="1" max="10" step="1" help="How many skill points should this character have?" /> <FormKit type="range" name="dexterity" id="dexterity" validation="required|max:10" label="Dexterity" value="5" min="1" max="10" step="1" help="How many dexterity points should this character have?" /> </FormKit> <pre wrap>{{ value }}</pre> </FormKit> <p>Change the character's class to see the changes in attribute values.</p></template>Extract logic to a plugin
New Character
Change the character's class to see the changes in attribute values.
Adding group validation
Let's assume that while different characters are better at different attributes, none should be too powerful. We can do this by creating a budget of points, and adding group validation to the attributes group to ensure they do not exceed 20 points in totality. We'll learn a new feature — custom rules — to accomplish this:
By default, the group type does not output any markup, so to show validation errors we need to manually add it.
<script setup>import { castRangeToNumber, updateAttributesPlugin } from './plugins.js'import { max_sum } from './rules.js'const createCharacter = async (fields) => { await new Promise((r) => setTimeout(r, 1000)) alert(JSON.stringify(fields))}</script><template> <div> <h4 class="form-label"> Add a custom validation rule to the attributes group. </h4> </div> <h1>New Character</h1> <FormKit type="form" @submit="createCharacter" :plugins="[castRangeToNumber]" submit-label="Create Character" #default="{ value }" > <FormKit type="text" name="name" id="name" validation="required|not:Admin" label="Name" help="Your full name" placeholder="Please add your name" /> <FormKit type="select" label="Class" name="class" id="class" placeholder="Select a class" :options="['Warrior', 'Mage', 'Assassin']" :plugins="[updateAttributesPlugin]" /> <div class="character-attributes"> <h4>Character Attributes</h4> <p>You have a max budget of 20 points for character attributes.</p> <FormKit type="group" name="attributes" id="attributes" :validation-rules="{ max_sum }" validation-visibility="live" validation="max_sum" :validation-messages="{ max_sum: ({ name, args }) => `${name} has exceeded the max budget of 20. Your character can't be that strong!`, }" #default="{ id, messages, fns, classes }" > <FormKit type="range" name="strength" id="strength" label="Strength" value="5" min="1" max="10" step="1" help="How many strength points should this character have?" /> <FormKit type="range" name="skill" id="skill" label="Skill" value="5" min="1" max="10" step="1" help="How many skill points should this character have?" /> <FormKit type="range" name="dexterity" id="dexterity" label="Dexterity" value="5" min="1" max="10" step="1" help="How many dexterity points should this character have?" /> <!-- By default groups do not show validation messages, so we need to add it manually --> <ul :class="classes.messages" v-if="fns.length(messages)"> <li v-for="message in messages" :key="message.key" :class="classes.message" :id="`${id}-${message.key}`" :data-message-type="message.type" > {{ message.value }} </li> </ul> </FormKit> </div> <p> Try using greater than the alloted 20 point budget for the attributes. </p> <pre wrap>{{ value }}</pre> </FormKit> <p>Change the character's class to see the changes in attribute values.</p></template>Add a custom validation rule to the attributes group.
New Character
Change the character's class to see the changes in attribute values.
Conditional rendering
Sometimes forms need to show or hide fields depending on the value of another input. We can do this by learning 2 new concepts:
- Context object — We can access an input's value (along with other data) inside our form because all
FormKitcomponents receive their context object in the#defaultslot prop. - The value of a
group- The value of a group (andform) input is an object with the values of its children, keyed by the children'snames.
When using conditional rendering, note that Vue needs hints to know that a DOM element needs a re-render, instead of trying to reuse it. We can add a unique key property to the element to help Vue.
So, let's grab the context object of the group input and extract the value: #default="{ value }". We want to add a small easter egg for our users if they decide to change all attributes to 1:
<FormKit type="group" name="attributes" id="attributes" :validation-rules="{ max_sum }" validation-visibility="live" validation="max_sum" :validation-messages="{ max_sum: ({ name, args }) => `${name} has exceeded the max budget of 20. Your character can't be that strong!`, }" #default="{ value, id, messages, fns, classes }"> <FormKit type="range" name="strength" id="strength" label="Strength" value="5" min="1" max="10" step="1" help="How many strength points should this character have?" /> <FormKit type="range" name="skill" id="skill" label="Skill" value="5" min="1" max="10" step="1" help="How many skill points should this character have?" /> <FormKit type="range" name="dexterity" id="dexterity" label="Dexterity" value="5" min="1" max="10" step="1" help="How many dexterity points should this character have?" /> <!-- By default groups do not show validation messages, so we need to add it manually --> <ul :class="classes.messages" v-if="fns.length(messages)"> <li v-for="message in messages" :key="message.key" :class="classes.message" :id="`${id}-${message.key}`" :data-message-type="message.type" > {{ message.value }} </li> </ul> <!-- Conditional rendering is simple, just get the value and a property of the object --> <p v-if=" value.strength === 1 && value.skill === 1 && value.dexterity === 1 " key="easter-egg" class="easter-egg" > Are you trying to make the game harder for yourself? </p></FormKit>Conditional rendering based on the value of form fields.
New Character
Change the character's class to see the changes in attribute values.
Next steps
And that concludes our introduction to FormKit! You are now ready to start using it!
There are some topics we recommend you exploring next, which you can read in any order or even later after trying out FormKit for yourself:
- Style your forms: Learn how to add classes to FormKit using sections, or use one of our pre-made Tailwind CSS themes available at themes.formkit.com.
- Multi-language forms: Learn how FormKit makes i18n easy with dozens of available languages.
- Explore FormKit's schema: Learn about FormKit's JSON-compatible schema and how you can use it for form generation, saving forms to a database in a JSON format, or building your own form-builder style projects.