Originally published on Substack (opens in a new tab)

I've been learning how to make FormKit plugins and want to walk you through creating one from scratch. We'll build a plugin that automatically adds icons to inputs - it's a good example because it touches on a lot of key FormKit concepts.

Starting With the Problem

Before diving into code, let's think about what we want to do. I was adding icons to my form inputs manually like this:

<FormKit
  type="email"
  prefixIcon="email"
/>

For every single input. It was repetitive and I kept forgetting to add them. So I thought - why not make a plugin that does this automatically based on the input type?

Building Our First Plugin

All FormKit plugins start as a function that receives a node. The node is your connection to the input - it has all the properties, methods and lifecycle hooks you need. Here's our starting point:

function addPrefixIconPlugin(node) {
  // This is where we'll put our code
}

Adding Lifecycle Hooks

We need to know when our input is created so we can add the icon. FormKit gives us lifecycle events for this:

function addPrefixIconPlugin(node) {
  node.on("created", () => {
    // This runs when the input is created
  });
}

Making Smart Decisions

Now we need some logic to decide if and when to add icons. Let's add some checks:

function addPrefixIconPlugin(node) {
  node.on("created", () => {
    // Which input types should get icons?
    const typesToApply = ["email", "password", "text", "url", "search"];

    // Skip if:
    if (node.props.prefixIcon) return; // already has an icon
    if (!typesToApply.includes(node.props.type)) return; // not an input we care about 
    if (!node.props.definition) return; // something's wrong

The Hard Part: Modifying the Schema

This is where it gets interesting. FormKit uses something called a schema to build its inputs. Think of it like a blueprint. To add our icon, we need to modify this blueprint:

// Keep the original schema function
    const originalSchema = node.props.definition.schema;

    // Create our new schema function
    node.props.definition.schema = (extensions) => {
      // Add our prefix section with the icon
      const localExtensions = {
        ...extensions,
        prefix: {
          $el: "label", 
          attrs: {
            class: "formkit-prefix-icon formkit-icon",
            innerHTML: icons[node.props.type],
          },
        },
      };

      // Important! Call the original schema with our changes
      return originalSchema(localExtensions);
    };

Let's break down what's happening here:

  1. We save the original schema function - we'll need it later

  2. We create a new schema function that adds our prefix section

  3. The prefix section is a label element with our icon

  4. We pass everything back to the original schema function

Putting It All Together

Here's our complete plugin:

function addPrefixIconPlugin(node) {
  node.on("created", () => {
    const typesToApply = ["email", "password", "text", "url", "search"];

    if (node.props.prefixIcon) return;
    if (!typesToApply.includes(node.props.type)) return;
    if (!node.props.definition) return;

    const originalSchema = node.props.definition.schema;
    
    node.props.definition.schema = (extensions) => {
      const localExtensions = {
        ...extensions,
        prefix: {
          $el: "label",
          attrs: {
            class: "formkit-prefix-icon formkit-icon",
            innerHTML: icons[node.props.type],
          },
        },
      };
      return originalSchema(localExtensions);
    };
  });
}

Using Our Plugin

To use the plugin, we add it to our FormKit config:

import { defaultConfig } from '@formkit/vue'

export default defineFormKitConfig({
  plugins: [addPrefixIconPlugin]
})

Now all our inputs automatically get the right icons based on their type!

Key Lessons About Plugin Creation

Through building this, I learned some important things about making FormKit plugins:

  1. Always hook into lifecycle events - created, mounted, etc. Don't try to run your code immediately.

  2. Check your conditions early - Get all your if checks out of the way at the start, return early if conditions aren't met.

  3. Preserve existing functionality - Notice how we kept the original schema function and called it with our changes? That's super important!

  4. Think about overrides - We check for prefixIcon first so users can still set their own icons if they want.

Next Steps

Once you understand these basics, you can create all kinds of plugins! Some ideas:

  • Add custom validation messages

  • Automatically format certain input types

  • Add tooltips (which is actually another plugin I made...)

The key is starting small and building up. This icon plugin looks simple but it teaches all the core concepts you need to know.

Let me know if anything's unclear! Plugin development seems scary at first but it's really just about understanding a few core concepts.

Formkit Schema

Back to all writing