DEV Community

Cover image for Creating a Tailwind CSS theme for FormKit
Andrew Boyd
Andrew Boyd

Posted on

Creating a Tailwind CSS theme for FormKit

FormKit ❤️ Tailwind CSS

FormKit ships with first-class support for Tailwind CSS.

For small projects – using Tailwind CSS with FormKit can be as easy as using the inline class props available on the <FormKit /> component. For more advanced use cases FormKit ships with a package (@formkit/tailwindcss) that makes creating a robust Tailwind CSS theme a breeze.

The following guide will walk you through both processes and show you how they can work together to provide incredible flexibility when styling your FormKit inputs.

This guide assumes you are using a standard Vue 3 build tool like Vite, Nuxt 3, or Vue CLI that will allow you to import .vue single file components.

Inline usage for simple use cases

If FormKit represents a small portion of your project — say a single contact form on a brochure website — then you'll likely be able to apply all the styling you need using the ${sectionKey}-class props or the classes prop available on the <FormKit /> component.

Here's a contact form styled using only the classes prop for a FormKit form:

<template>
  <FormKit
    type="form"
    :actions="false"
    :config="{
      // config override applies to all nested FormKit components
      classes: {
        outer: 'mb-5',
        label: 'block mb-1 font-bold text-sm',
        input: 'text-gray-800 mb-1 w-full',
        help: 'text-xs text-gray-500',
        message: 'text-red-500 text-xs',
      },
    }"
  >
    <FormKit
      type="text"
      label="Name"
      help="First and last name"
      validation="required"
      :classes="{
        input: 'border border-gray-400 py-1 px-2 rounded-md',
      }"
    />
    <FormKit
      type="email"
      label="Email"
      validation="required|email"
      :classes="{
        input: 'border border-gray-400 py-1 px-2 rounded-md',
      }"
    />
    <FormKit
      type="textarea"
      label="Message"
      validation="required|length:10"
      :classes="{
        input: 'border border-gray-400 py-1 px-2 rounded-md',
      }"
    />
    <FormKit
      type="submit"
      label="Submit"
      :classes="{
        outer: 'mb-0',
        input: 'bg-blue-500 text-white font-bold py-2 px-3 rounded-md w-auto',
      }"
    />
  </FormKit>
</template>
Enter fullscreen mode Exit fullscreen mode

This is a low-barrier way to apply Tailwind CSS styles to your FormKit forms. But what if you have multiple forms? Copy-pasting class lists between components is not ideal and can lead to inadvertent variations in styling across your project over time.

Let's explore how we can apply Tailwind CSS classes globally to all FormKit inputs within our project.

Using @formkit/tailwindcss

FormKit ships with a first-party package called @formkit/tailwindcss that makes it simple to create a Tailwind CSS theme for FormKit.

This package allows you to author your theme as a JavaScript object grouped by input type and sectionKey. Additionally, it exposes a number of Tailwind CSS variants based on FormKit state such as formkit-invalid: and formkit-disabled: which allow you to dynamically change your input styling.

To get started we first need to add the package to our project.

npm install @formkit/tailwindcss
Enter fullscreen mode Exit fullscreen mode

From there we need to:

  • Add the @formkit/tailwindcss plugin to our project's tailwind.config.js file.
  • Import generateClasses from @formkit/tailwindcss and use it where we define our FormKit config options.
// tailwind.config.js
module.exports {
  ...
  plugins: [
    require('@formkit/tailwindcss').default
  ]
  ...
}
Enter fullscreen mode Exit fullscreen mode
// app.js
import { createApp } from 'vue'
import App from './App.vue'
import { plugin, defaultConfig } from '@formkit/vue'
import { generateClasses } from '@formkit/tailwindcss'
import '../dist/index.css' // wherever your Tailwind styles exist

createApp(App)
  .use(
    plugin,
    defaultConfig({
      config: {
        classes: generateClasses({
          // our theme will go here.
          // ...
          // text: {
          //   label: 'font-bold text-gray-300',
          //   ...
          // }
          // ...
        }),
      },
    })
  )
  .mount('#app')
Enter fullscreen mode Exit fullscreen mode

Once this setup is complete we are ready to begin writing our Tailwind CSS theme!

Our first Tailwind CSS input

To start, let's apply some classes to a text style input. This will cover a large surface area because we'll be able to easily re-use these styles on other text-like inputs such as email, password, date, etc.

To specifically target text inputs we'll create a text key in our theme object and then apply classes to each sectionKey as needed.

Here is a text input with Tailwind CSS classes applied using our default FormKit config values:

import { createApp } from 'vue';
import App from './App.vue';
import { plugin, defaultConfig } from '@formkit/vue';
import { generateClasses } from '@formkit/tailwindcss';

createApp(App)
  .use(
    plugin,
    defaultConfig({
      config: {
        classes: generateClasses({
          text: {
            outer: 'mb-5',
            label: 'block mb-1 font-bold text-sm',
            inner: 'bg-white max-w-md border border-gray-400 rounded-lg mb-1 overflow-hidden focus-within:border-blue-500',
            input: 'w-full h-10 px-3 bg-transparent border-none focus:outline-none text-base text-gray-700 placeholder-gray-400 focus:outline-none',
            help: 'text-xs text-gray-500',
            messages: 'list-none p-0 mt-1 mb-0',
            message: 'text-red-500 mb-1 text-xs',
          },
        }),
      },
    })
  )
  .mount('#app');
Enter fullscreen mode Exit fullscreen mode

Using variants

That's looking good! But it's fairly static at the moment. It would be nice if we could react with different styles based on the state of our inputs.

The @formkit/tailwindcss package provides a number of variants you can use in your class lists to dynamically respond to input and form state.

The currently shipped variants are:

  • formkit-disabled:
  • formkit-invalid:
  • formkit-errors:
  • formkit-complete:
  • formkit-loading:
  • formkit-submitted:
  • formkit-multiple:
  • formkit-action:
  • formkit-message-validation:
  • formkit-message-error:

You can use these variant the same way you would use built-in Tailwind CSS variants such as dark: and hover:.

Let's add some variants for formkit-invalid and formkit-disabled to our text input styles.

export default {
  text: {
    outer: 'mb-5 formkit-disabled:opacity-40',
    label: 'block mb-1 font-bold text-sm formkit-invalid:text-red-500',
    inner: `
      max-w-md
      border border-gray-400
      rounded-lg
      mb-1
      overflow-hidden
      focus-within:border-blue-500
      formkit-invalid:border-red-500
    `,
    input: 'w-full h-10 px-3 border-none text-base text-gray-700 placeholder-gray-400 focus:outline-none',
    help: 'text-xs text-gray-500',
    messages: 'list-none p-0 mt-1 mb-0',
    message: 'text-red-500 mb-1 text-xs',
  },
};
Enter fullscreen mode Exit fullscreen mode

Creating a full theme

Now we're cooking! To create a comprehensive theme we need to define class lists for the sectionKeys of all of the other input types we'll use in our project.

Before we go too far though, there are some improvements we can make.

The generateClasses function in @formkit/tailwindcss allows for a special input type key called global that will apply to all inputs. This is helpful for targeting sectionKeys such as help and messages that are often styled identically across all input types within a project.

Let's create class list definitions for all input types included in FormKit. We'll group common types of inputs into "classifications" to avoid being too repetitive.

// We'll create some re-useable definitions
// because many input types are identical
// in how we want to style them.
const textClassification = {
  label: 'block mb-1 font-bold text-sm formkit-invalid:text-red-500',
  inner: 'max-w-md border border-gray-400 formkit-invalid:border-red-500 rounded-lg mb-1 overflow-hidden focus-within:border-blue-500',
  input: 'w-full h-10 px-3 border-none text-base text-gray-700 placeholder-gray-400',
}
const boxClassification = {
  fieldset: 'max-w-md border border-gray-400 rounded-md px-2 pb-1',
  legend: 'font-bold text-sm',
  wrapper: 'flex items-center mb-1 cursor-pointer',
  help: 'mb-2',
  input: 'form-check-input appearance-none h-5 w-5 mr-2 border border-gray-500 rounded-sm bg-white checked:bg-blue-500 focus:outline-none focus:ring-0 transition duration-200',
  label: 'text-sm text-gray-700 mt-1'
}
const buttonClassification = {
  wrapper: 'mb-1',
  input: 'bg-blue-500 hover:bg-blue-700 text-white text-sm font-normal py-3 px-5 rounded'
}

// We'll export our definitions using our above
// classification templates and declare
// one-offs and overrides as needed.
export default {
  // the global key will apply to _all_ inputs
  global: {
    outer: 'mb-5 formkit-disabled:opacity-50',
    help: 'text-xs text-gray-500',
    messages: 'list-none p-0 mt-1 mb-0',
    message: 'text-red-500 mb-1 text-xs'
  },
  button: buttonClassification,
  color: {
    label: 'block mb-1 font-bold text-sm',
    input: 'w-16 h-8 appearance-none cursor-pointer border border-gray-300 rounded-md mb-2 p-1'
  },
  date: textClassification,
  'datetime-local': textClassification,
  checkbox: boxClassification,
  email: textClassification,
  file: {
    label: 'block mb-1 font-bold text-sm',
    inner: 'max-w-md cursor-pointer',
    input: 'text-gray-600 text-sm mb-1 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:bg-blue-500 file:text-white hover:file:bg-blue-600',
    noFiles: 'block text-gray-800 text-sm mb-1',
    fileItem: 'block flex text-gray-800 text-sm mb-1',
    removeFiles: 'ml-auto text-blue-500 text-sm'
  },
  month: textClassification,
  number: textClassification,
  password: textClassification,
  radio: {
    // if we want to override a given sectionKey
    // from a classification we can do a spread
    // of the default value along with a new
    // definition for our target sectionKey.
    ...boxClassification,
    input: boxClassification.input.replace('rounded-sm', 'rounded-full'),
  },
  range: {
    inner: 'max-w-md',
    input: 'form-range appearance-none w-full h-2 p-0 bg-gray-200 rounded-full focus:outline-none focus:ring-0 focus:shadow-none'
  },
  search: textClassification,
  select: textClassification,
  submit: buttonClassification,
  tel: textClassification,
  text: textClassification,
  textarea: {
    ...textClassification,
    input: 'block w-full h-32 px-3 border-none text-base text-gray-700 placeholder-gray-400 focus:shadow-outline',
  },
  time: textClassification,
  url: textClassification,
  week: textClassification,
}
Enter fullscreen mode Exit fullscreen mode

Selective overrides

And there we have it! All FormKit inputs are now styled with Tailwind CSS classes across our entire project.

If we ever need to override any specific one-offs within our project, we can do so using the section-key class props or the classes prop on a given FormKit element.

Of particular importance when performing an override is the $reset modifier.

When the FormKit class system encounters a class named $reset it will discard the current class list for the given sectionKey and only collect class names that occur after the $reset class. This is helpful for systems like Tailwind CSS where it can be cumbersome to override a large number of classes when you need to deviate from your base theme.

<template>
  <FormKit
    type="text"
    label="I use the global theme we defined"
    help="I play by the rules"
  />
  <FormKit
    type="text"
    label="I'm special and have a $reset and custom styles"
    help="I'm a rebel"
    label-class="$reset italic text-lg text-red-500"
    help-class="$reset font-bold text-md text-purple-800"
  />
</template>
Enter fullscreen mode Exit fullscreen mode

Next steps

This guide has walked through creating a Tailwind CSS theme for all input types included in FormKit, but there is still more that could be done!

Here are some ways to take the above guide even further:

  • Add dark-mode support using the built-in Tailwind CSS dark: modifier.
  • Combine multiple variants such as formkit-invalid:formkit-submitted: to add extra emphasis to invalid fields when a user tries to submit an incomplete form.
  • Publish your theme as an npm package for easy importing and sharing between projects.

If you want to dive in deeper into FormKit there's plenty to learn about the core internals of the framework as well as the FormKit schema which allows generating forms from JSON with conditionals, expressions, and more!

Now go forth and make beautiful forms!

Top comments (0)