Copied!

When Your Forms Don’t Match Your Design System Part 2

Your First Custom Form.io Theme: Solving the most common use case: CSS class overrides.

Updating Design System with Form.io Standard Template

In Part 1, we established the design system compliance problem. Now let’s solve it for 98% of cases using class overrides alone.

Class names let you replace the CSS classes on your form components without touching the HTML structure. This is faster, less fragile, and easier to maintain than template-level customization. Start with this method in every case, and only reach for template overrides if you can’t accomplish your design goals with classes alone.

What Is the Class Names Method?

Form.io templates are built from components, and each component has named parts: input, label, button, and so on. Class names let you define exactly which CSS classes apply to each part. The HTML structure stays the same; only the classes change. Because you’re working at the class level, this approach works with any CSS framework, whether you’re using Tailwind, Bootstrap, or something entirely your own, so you’re never locked into a particular toolchain.

Before You Start

This guide assumes you already have a working Form.io application using the @formio/js renderer. To add the Standard Template, install it from npm:

npm install @formio/standard-template

Note: The Standard Template is a new and actively evolving package. Check the GitHub repository for the latest version and API details before adopting it in production.

The Structure

Here’s what a theme definition looks like:

import { standardTemplate } from '@formio/standard-template';
import type { TemplateClasses } from '@formio/standard-template';

const myTheme: TemplateClasses = {
  // Component definitions go here
};

Formio.use(standardTemplate('my-custom-theme', myTheme));

Each component you want to customize gets an entry, and each entry defines classes for different contexts (form mode versus html display mode) and for the different parts of that component. Once you’ve seen one component defined, the rest follow the same shape.

Building a Complete Theme

Let’s build a complete theme from scratch using Tailwind classes, transforming a basic contact form to match a modern design system. We’ll add one piece at a time.

Our goal:

  • Minimal, underline-style input fields with an accent focus state
  • Small, uppercase labels in the accent color
  • A gradient pill button with hover effects
  • Comfortable spacing between fields

Step 1: Input Fields

const myTheme: TemplateClasses = {
  input: {
    form: {
      input: [
        'block',
        'w-full',
        'px-0',
        'py-2.5',
        'text-base',
        'text-slate-800',
        'bg-transparent',
        'border-0',
        'border-b-2',
        'border-slate-300',
        'focus:outline-none',
        'focus:border-violet-600',
        'placeholder:text-slate-400',
        'transition-colors',
        'duration-200'
      ]
    }
  }
};

Pause on the shape of that definition, because every other component follows it:

  • input (the outer key) is the component type: text fields, email fields, and so on
  • form is the context (edit mode; there’s also html for read-only display)
  • input (the inner key) is the specific element within that component
  • The array contains all the Tailwind classes applied to that element

The result is an underline-style input: no box, no background, just a bottom border that shifts to violet when the field has focus.

Step 2: Add Labels

const myTheme: TemplateClasses = {
  label: {
    form: {
      label: [
        'block',
        'text-xs',
        'font-bold',
        'uppercase',
        'tracking-wider',
        'text-violet-600',
        'mb-2'
      ]
    }
  },
  input: {
    // ...input classes from Step 1
  }
};

Labels now have their own styling: small, bold, uppercase, with letter-spacing and the accent color picking up the same violet as the input focus state. This sits alongside the input definition rather than inside it, because labels and inputs are independent components.

Step 3: Add Buttons

const myTheme: TemplateClasses = {
  button: {
    form: {
      button: [
        'w-full',
        'px-6',
        'py-3',
        'text-sm',
        'font-bold',
        'uppercase',
        'tracking-wider',
        'text-white',
        'bg-gradient-to-br',
        'from-violet-600',
        'to-violet-700',
        'rounded-full',
        'shadow-lg',
        'shadow-violet-600/30',
        'cursor-pointer',
        'transition',
        'duration-150',
        'hover:opacity-90',
        'hover:-translate-y-px',
        'disabled:opacity-50',
        'disabled:cursor-not-allowed'
      ]
    }
  },
  label: {
    // ...label classes from Step 2
  },
  input: {
    // ...input classes from Step 1
  }
};

The button becomes a full-width gradient pill: violet gradient background, a soft glow shadow, a subtle lift on hover, and proper disabled states. Notice the pattern: each component is self-contained, and adding a new one never disturbs the others.

Step 4: Add Field Spacing

const myTheme: TemplateClasses = {
  component: {
    form: {
      component: [
        'mb-6'
      ]
    }
  },
  button: {
    // ...button classes from Step 3
  },
  label: {
    // ...label classes from Step 2
  },
  input: {
    // ...input classes from Step 1
  }
};

The component entry wraps every field in the form, so a single mb-6 here gives each field comfortable breathing room below it. That’s the last piece of the theme.

The Complete Theme

Here’s the full theme definition:

import { standardTemplate } from '@formio/standard-template';
import type { TemplateClasses } from '@formio/standard-template';

const myTheme: TemplateClasses = {
  label: {
    form: {
      label: [
        'block',
        'text-xs',
        'font-bold',
        'uppercase',
        'tracking-wider',
        'text-violet-600',
        'mb-2'
      ]
    }
  },
  input: {
    form: {
      input: [
        'block',
        'w-full',
        'px-0',
        'py-2.5',
        'text-base',
        'text-slate-800',
        'bg-transparent',
        'border-0',
        'border-b-2',
        'border-slate-300',
        'focus:outline-none',
        'focus:border-violet-600',
        'placeholder:text-slate-400',
        'transition-colors',
        'duration-200'
      ]
    }
  },
  button: {
    form: {
      button: [
        'w-full',
        'px-6',
        'py-3',
        'text-sm',
        'font-bold',
        'uppercase',
        'tracking-wider',
        'text-white',
        'bg-gradient-to-br',
        'from-violet-600',
        'to-violet-700',
        'rounded-full',
        'shadow-lg',
        'shadow-violet-600/30',
        'cursor-pointer',
        'transition',
        'duration-150',
        'hover:opacity-90',
        'hover:-translate-y-px',
        'disabled:opacity-50',
        'disabled:cursor-not-allowed'
      ]
    }
  },
  component: {
    form: {
      component: [
        'mb-6'
      ]
    }
  }
};

// Apply it globally
Formio.use(standardTemplate('my-custom-theme', myTheme));

Apply this once when your application initializes, and every form in your application will use the styling.

Before:

After:

Next Steps

You now have everything you need to style Form.io forms to match your design system using class overrides. From here, the work is mostly a matter of breadth. The same pattern extends to every other component:

  • Radio buttons and checkboxes: radio, checkbox
  • Select dropdowns: select
  • Text areas: textarea
  • Error and help text: errorLabel, helpBlock
  • Field containers: component, field

In every case you define classes for the component type and its parts, exactly as we did above. A textarea, for example, would take the same underline treatment as the inputs, plus resize-y and a min-h-20 so users can expand it comfortably.

As your themes grow, it helps to pull them into their own modules so you can reuse them across projects:

// themes/tailwind-modern.ts
export const tailwindModern: TemplateClasses = {
  // Your theme definition
};

// app.ts
import { tailwindModern } from './themes/tailwind-modern';
Formio.use(standardTemplate('tailwind-modern', tailwindModern));

Do this consistently and you end up with a library of themes you can share across your organization and maintain as part of your design system.

Additional Resources

Published by

Aaron Eaton - Senior Applications Developer
Aaron Eaton
Senior Applications Developer

Aaron is a Senior Applications Developer and Developer Advocate at Form.io, where he helps customers get the most out of the platform. With over 15 years of application development experience, he focuses on the enterprise side of Form.io: integrations, complex workflows, and the architectural decisions that come with building at scale. He makes that depth accessible through hands-on content, technical webinars, and real-world solutions.

Published by

Aaron Eaton - Senior Applications Developer
Aaron Eaton
Senior Applications Developer

Aaron is a Senior Applications Developer and Developer Advocate at Form.io, where he helps customers get the most out of the platform. With over 15 years of application development experience, he focuses on the enterprise side of Form.io: integrations, complex workflows, and the architectural decisions that come with building at scale. He makes that depth accessible through hands-on content, technical webinars, and real-world solutions.

LighthouseHQ Case Study: Digital Transformation
Get Answers

Need More Answers?

Ask and we'll get back with you in 1 business day.

Contact Us

Send us a message to contact support or ask a question.

Schedule a meeting

Open Source Platform

Read our FAQ to find out what exactly is Open Source

View the Platform Documentation

View the API Documentation

View the Open Source Code

Learn More

Learn How It Works

Read the Release Notes

Discover Industries that use Form.io

Read our Blog