At the heart of the FormKit framework is @formkit/core. This zero-dependency package is responsible for nearly all of FormKit's low-level critical functions, such as:

  • Configuration
  • Value input/output
  • Event bubbling
  • Plugin management
  • Tree state tracking
  • Message management
  • Lifecycle hooks


The functionality of FormKit core is not exposed to your application via a centralized instance but rather a distributed set of "nodes" (FormKitNode) where each node represents a single input.

This mirrors HTML — in fact DOM structure is actually a general tree and FormKit core nodes reflect this structure. For example, a simple login form could be drawn as the following tree graph:

Hover over each node to see its initial options.

In this diagram, a form node is a parent to three child nodes — email, password and submit. Each input component in the graph "owns" a FormKit core node, and each node contains its own options, configuration, props, events, plugins, lifecycle hooks, etc. This architecture ensures that FormKit’s primary features are decoupled from the rendering framework (Vue) — a key to reducing side effects and maintaining blazing fast performance.

Additionally, this decentralized architecture allows for tremendous flexibility. For example — one form could use different plugins than other forms in the same app, a group input could modify the configuration of its sub-inputs, and validation rules can even be written to use props from another input.


Every <FormKit> component owns a single core node, and each node must be one of three types:

Input vs node types
Core nodes are always one of three types (input, list, or group). These are not the same as input types — of which there can be unlimited variation. Strictly speaking all inputs have 2 types: their node type (like input), and their input type (like checkbox).


Most of FormKit’s native inputs have a node type of input — they operate on a single value. The value itself can be of any type, such as objects, arrays, strings, and numbers — any value is acceptable. However, nodes of type input are always leafs — meaning they cannot have children.


A list is a node that produces an array value. Children of a list node produce a value in the list’s array value. The names of immediate children are ignored — instead each is assigned an index in the list’s array.


A group is a node that produces an object value. Children of a group node use their name to produce a property of the same name in the group’s value object — <FormKit type="form"> is an instance of a group.


In addition to specifying the type of node when calling createNode(), you can pass any of the following options:

children[]Child FormKitNode instances.
config{}Configuration options. These become the defaults of the props object.
name{type}_{n}The name of the node/input.
parentnullThe parent FormKitNode instance.
plugins[]An array of plugin functions.
props{}An object of key/value pairs that represent the current node instance details.
typeinputThe type of FormKitNode to create (list, group, or input).
valueundefinedThe initial value of the input.

Config & Props

FormKit uses an inheritance-based configuration system. Any values declared in the config option are automatically passed to children (and all descendants) of that node, but not passed to siblings or parents. Each node can override its inherited values by providing its own config, and these values will in turn be inherited by any deeper children and descendants. For example:

The above code will result in each node having the following configuration:

Notice how the list subtree is pink.
Use props to read config
It is best practice to read configuration values from node.props rather than node.config. The next section details this feature.


The node.props and node.config objects are closely related. node.config is best thought of as the initial values for node.props. props is an arbitrarily shaped object that contains details about the current instance of the node.

The best practice is to always read configuration and prop data from node.props even if the original value is defined using node.config. Explicitly defined props take precedence over configuration options.

FormKit component props
When using the <FormKit> component, any props defined for the input type are automatically set as node.props properties. For example: <FormKit label="Email" /> would result in node.props.label being Email.

Setting values

You can set the initial value of a node by providing the value option on createNode() — but FormKit is all about interactivity, so how do we update the value of an already defined node? By using node.input(value).

In the above example username.value is still undefined immediately after it’s set because node.input() is asynchronous. If you need to read the resulting value after calling node.input() you can await the returned promise.

Because node.input() is asynchronous, the rest of our form does not need to recompute its dependencies on every keystroke. It also provides an opportunity to perform modifications to the unsettled value before it is "committed" to the rest of the form. However — for internal node use only — a _value property containing the unsettled value of the input is also available.

Don’t assign values
You cannot directly assign the value of an input node.value = 'foo'. Instead, you should always use node.input(value)

Value settlement

Now that we understand node.input() is asynchronous, let's explore how FormKit solves the "settled tree" problem. Imagine a user quickly types in their email address and hits "enter" very quickly — thus submitting the form. Since node.input() is asynchronous, incomplete data would likely be submitted. We need a mechanism to know when the whole form has "settled".

To solve this, FormKit’s nodes automatically track tree, subtree, and node "disturbance". This means the form (usually the root node) always knows the settlement state of all the inputs it contains.

The following graph illustrates this "disturbance counting". Click on any input node (blue) to simulate calling node.input() and notice how the whole form is always aware of how many nodes are "disturbed" at any given time. When the root node has a disturbed count of 0 the form is settled and safe to submit.

Click on the inputs (blue) to simulate calling a user input.
To ensure a given tree (form), subtree (group), or node (input) is "settled" you can await the `node.settled` property:
The form type
The <FormKit type="form"> input already incorporates this await behavior. It will not call your @submit handler until your form is completely settled. However when building advanced inputs it can be useful to understand these underlying principles.

Getting a component’s node

Sometimes it can be helpful to get the underlying instance of a node from the Vue <FormKit> component. There are three primary methods of fetching an input’s node.

  • Using the Vue plugin’s $formkit.get() (or getNode() for composition API)
  • Using the @node event.
  • Using a template ref.

Using this.$formkit.get()

When using FormKit with the Vue plugin (recommended), you can access a node by assigning it an id and then accessing it by that property.

You must assign the input an id to use this method.
Load Live Example
Composition API
When using Vue’s composition API, you don’t have access to this within setup. You can access the same getNode() function by importing it from @formkit/core.

import { getNode } from '@formkit/core'

Using the node event

Another way to get the underlying node is to listen to the @node event which is emitted only once when the component first initializes the node.

Load Live Example

Using a template ref

Assigning a <FormKit> component to a ref also allows easy access to the node.

Load Live Example


To traverse nodes within a group or list use — where address is the name of the node being accessed (or the relative path to the name). For example:

If the starting node has siblings, it will attempt to locate a match in the siblings (internally, this is what FormKit uses for validation rules like confirm:address).

Deep traversal

You can go deeper than one level by using a dot-syntax relative path. Here's a more complex example:

Notice how traversing the list uses numeric keys, this is because the list type uses array indexes automatically.

Traversal path of'users.0.password') shown in red.
Array paths
Node addresses may also be expressed as arrays. For example'') could be expressed as['foo', 'bar']).

Traversal tokens

Also available for use in are a few special "tokens":

$parentThe immediate ancestor of the current node.
$rootThe root node of the tree (the first node with no parent).
$selfThe current node in the traversal.
find()A function that performs a breadth-first search for a matching value and property. For example:'$root.find(555, value)')

These tokens are used in dot-syntax addresses just like you would use a node’s name:

Traversal path of'$parent.$') shown in red.


Nodes have their own events which are emitted during the node’s lifecycle (unrelated to Vue’s events).

Add listener

To observe a given event, use node.on().

Event handler callbacks all receive a single argument of type FormKitEvent, the object shape is:

Node events (by default) bubble up the node tree, but node.on() will only respond to events emitted by the same node. However, if you would like to also catch events bubbling up from descendants you may append the string .deep to the end of your event name:

Remove listener

Every call to register an observer with node.on() returns a “receipt” — a randomly generated key — that can be used later to stop observing that event (similar to setTimeout() and clearTimeout()) using

Core events

The following is a comprehensive list of all events emitted by @formkit/core. Third-party code may emit additional events not included here.

commitanyyesEmitted when a node's value is committed but before it has been transmitted to the rest of the form.
config:{property}any (the value)yesEmitted any time a specific configuration option is set or changed.
count:{property}any (the value)noEmitted any time a a ledger’s counter value changes.
childFormKitNodeyesEmitted when a new child node is added, created or assigned to a parent.
createdFormKitNodeyesEmitted immediately before the node is returned when calling createNode() (plugins and features have already run).
definedFormKitTypeDefinitionyesEmitted when the node’s "type" is defined, this typically happens during createNode().
destroyingFormKitNodeyesEmitted when the node.destroy() is called, after it has been detached from any parents.
dom-input-eventEventyesEmitted when the DOMInput handler is called, useful for getting the original HTML input event in core.
inputany (the value)yesEmitted when node.input() is called — after the input hook has run.
message-addedFormKitMessageyesEmitted when a new message was added.
message-removedFormKitMessageyesEmitted when a message was removed.
message-updatedFormKitMessageyesEmitted when a message was changed.
prop:{propName}any (the value)yesEmitted any time a specific prop is set or changed.
prop{ prop: string, value: any }yesEmitted any time a prop is set or changed.
resetFormKitNodeyesEmitted any time a form or group is reset.
settledbooleannoEmitted anytime a node’s disturbance counting settles or unsettles.
settled:{counterName}booleannoEmitted anytime a specific ledger counter settles (returns to zero).
unsettled:{counterName}booleannoEmitted anytime a specific ledger counter becomes unsettled (goes above zero).
textstring or FormKitTextFragmentnoEmitted after the text hook has run — typically when processing interface text that may have been translated.
Prop events on config changes
When a configuration option changes, any inheriting nodes (including the origin node) will also emit prop and prop:{propName} events, so long as they do not override that property in their own props or config objects.

Emitting events

Node events are emitted with node.emit(). You can leverage this feature to emit your own synthetic events from your own plugins.

An optional third argument bubble is also available. When set to false, it prevents your event from bubbling up through the form tree.


Hooks are middleware dispatchers that are triggered during pre-defined lifecycle operations. These hooks allow external code to extend the internal functionality of @formkit/core. The following table details all available hooks:

property: string,
classes: Record<string, boolean>
Dispatched after all class operations have been run, before final conversion to a string.
commitanyDispatched when setting the value of a node after the input and debounce of node.input() is called.
errorstringDispatched when processing a thrown error — errors are generally inputs, and the final output should be a string.
initFormKitNodeDispatched after the node is initially created but before it is returned in createNode().
inputanyDispatched synchronously on every input event (every keystroke) before commit.
messageFormKitMessageDispatched when a message is being set on
prop: string,
value: any
Dispatched when any prop is being assigned.
setErrors{ localErrors: ErrorMessages, childErrors?: ErrorMessages }Dispatched when explicit errors are being set on a node (not validation errors).
submitRecord<string, any>Dispatched when the FormKit form is submitted and passing validation. This hook allows you to modify the (cloned) form values before they are passed to the submit handler
textFormKitTextFragmentDispatched when a FormKit-generated string needs to be displayed — allowing i18n or other plugins to intercept.

Hook middleware

To make use of these hooks, you must register hook middleware. A middleware is simply a function that accepts 2 arguments — the value of the hook and next — a function that calls the next middleware in the stack and returns the value.

To register a middleware, pass it to the node.hook you want to use:

Use with plugins
Hooks can be registered anywhere in your application, but the most common place hooks are used is in a plugin.


Plugins are the primary mechanism for extending the functionality of FormKit. The concept is simple — a plugin is just a function that accepts a node. These functions are then automatically called when a node is created, or when the plugin is added to the node. Plugins work similar to configuration options — they are automatically inherited by children and descendants.

In the example above, the plugin is only defined on the parent, but the child also inherits the plugin. The function myPlugin will be called twice — once for each node in the graph (which only has two in this example):

The plugin is inherited by the child, but executed independently.


In addition to extending and modifying nodes, plugins serve one additional role — exposing input libraries. A “library” is a function assigned to the library property of a plugin that accepts a node and determines whether it knows how to “define” that node. If it does, it calls node.define() with an input definition.

For example, if we wanted to create a plugin that exposed a couple new inputs: italy and france we could write a plugin to do this:

Load Live Example

Experienced developers will notice a few exciting properties of this plugin-library pattern:

  1. Multiple input libraries can be installed on the same project.
  2. Plugins (and libraries) can be exposed locally, per form, group, or globally.
  3. A plugin can bundle new inputs along with plugin logic making installation simple for end users.
  4. The library function has full control over what conditions result in a call to node.define(). Frequently, this is simply checking node.props.type but you can define different inputs based on other conditions, like if a particular prop is set.
Learn to create your own custom inputs Custom input docs

Message store

Each node has its own data store. The objects in these stores are called "messages" and these messages are especially valuable for three primary use cases:

  • Displaying information about the node to a user with i18n support (the validation plugin uses it).
  • "Blocking" form submission.
  • General data store for plugin authors.

Each message (FormKitMessage in TypeScript) in the store is an object with the following shape:

Create message helper
A helper function createMessage({}) can be imported from @formkit/core to merge your message data with the above default values to create a new message object.

Read and write messages

To add or update a message, use Messages are then made available on{messageKey}

Message locales
Messages will automatically be translated if the @formkit/i18n plugin is installed and a matching key is available in the active locale. Read the i18n docs.


One of the keys to FormKit’s performance is its ability to efficiently count messages matching a given criteria (in the store), and then keep a running tally of those messages as changes are made (including from child nodes). These counters are created using node.ledger.

Creating a counter

Let's say we want to count how many messages are currently being displayed. We could do this by counting messages with the visible property set to true.

Load Live Example

Notice the second argument of node.ledger.count() is a function. This function accepts a message as an argument and expects the return value to be a boolean, indicating whether that message should be counted or not. This allows you to craft arbitrary counters for any message type.

When using a counter on a group or list node, the counter will propagate down the tree summing the value of all messages passing the criteria function and then tracking that count for store changes.

Validation counter
The validation plugin already declares a counter called blocking which counts the blocking property of all messages. This is how FormKit forms know if all their children are "valid".