Getting Started

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.

Composition API

This guide assumes you are are familiar with the Vue Composition API.

Robust Vue.js Forms - Vue School Course

1 hr, 49 mins

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:

example
formkit.config
<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>
import { defineFormKitConfig } from '@formkit/vue'import { createProPlugin, inputs } from '@formkit/pro'import { genesisIcons } from '@formkit/icons'const pro = createProPlugin(import.meta.env.VITE_FORMKIT_PRO_KEY, inputs)export default defineFormKitConfig({  plugins: [pro],  icons: {    ...genesisIcons,  },})

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>
Your full name

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?"/>
How many strength points should this character have?

Remember: You have a budget of 20 points to build your character.

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?"/>
How many strength points should this character have?

Slide the strength above 9 or below 2.

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>
How many strength points should this character have?

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">:

Form values

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

Enter your character's full name
How many strength points should this character have?
How much skill points to start with
How much dexterity points to start with

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

Enter your character's full name
How many strength points should this character have?
How many skill points should this character have?
How many dexterity points should this character have?
{}

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>

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

Your full name
How many strength points should this character have?
How many skill points should this character have?
How many dexterity points should this character have?
{}

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: getNode gets an input's core node using their id as an identifier. Each input has an associated core node.
  • events: events listen to changes to a certain input.
  • node.input(): the input function 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

Your full name
How much skill points to start with
How much strength points to start with
How many dexterity points should this character have?
{}

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:

at() uses name

The at function uses the name attribute instead of the id that getNode uses.

example
plugins
<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>
export 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)))}export const updateAttributesPlugin = (node) => {  const CHARACTER_BASE_STATS = {    Warrior: {      strength: 10,      skill: 4,      dexterity: 6,    },    Mage: {      strength: 3,      skill: 10,      dexterity: 7,    },    Assassin: {      strength: 5,      skill: 5,      dexterity: 10,    },  }  node.on('commit', ({ payload }) => {    // Get the sibling attributes using at()    const attributeNode = node.at('attributes')    if (attributeNode && CHARACTER_BASE_STATS[payload])      attributeNode.input(CHARACTER_BASE_STATS[payload])  })}

Extract logic to a plugin

New Character

Your full name
How many strength points should this character have?
How many skill points should this character have?
How many dexterity points should this character have?
{}

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:

Groups do not display messages by default

By default, the group type does not output any markup, so to show validation errors we need to manually add it.

example
plugins
rules
<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>
export 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)))}export const updateAttributesPlugin = (node) => {  const CHARACTER_BASE_STATS = {    Warrior: {      strength: 10,      skill: 4,      dexterity: 6,    },    Mage: {      strength: 3,      skill: 10,      dexterity: 7,    },    Assassin: {      strength: 5,      skill: 5,      dexterity: 10,    },  }  node.on('commit', ({ payload }) => {    // Get the sibling attributes using at()    const attributeNode = node.at('attributes')    if (attributeNode && CHARACTER_BASE_STATS[payload])      attributeNode.input(CHARACTER_BASE_STATS[payload])  })}
export const max_sum = (node, max = 20) => {  return Object.values(node.value).reduce((a, b) => a + b) <= max}

Add a custom validation rule to the attributes group.

New Character

Your full name

Character Attributes

You have a max budget of 20 points for character attributes.

How many strength points should this character have?
How many skill points should this character have?
How many dexterity points should this character have?

Try using greater than the alloted 20 point budget for the attributes.

{}

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 FormKit components receive their context object in the #default slot prop.
  • The value of a group - The value of a group (and form) input is an object with the values of its children, keyed by the children's names.
Vue's key property

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:

example
plugins
rules
<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>
export 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)))}export const updateAttributesPlugin = (node) => {  const CHARACTER_BASE_STATS = {    Warrior: {      strength: 10,      skill: 4,      dexterity: 6,    },    Mage: {      strength: 3,      skill: 10,      dexterity: 7,    },    Assassin: {      strength: 5,      skill: 5,      dexterity: 10,    },  }  node.on('commit', ({ payload }) => {    // Get the sibling attributes using at()    const attributeNode = node.at('attributes')    if (attributeNode && CHARACTER_BASE_STATS[payload])      attributeNode.input(CHARACTER_BASE_STATS[payload])  })}
export const max_sum = (node, max = 20) => {  return Object.values(node.value).reduce((a, b) => a + b) <= max}

Conditional rendering based on the value of form fields.

New Character

Your full name

Character Attributes

You have a max budget of 20 points for character attributes.

How many strength points should this character have?
How many skill points should this character have?
How many dexterity points should this character have?

Try using greater than the alloted 20 point budget for the attributes.

{}

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.