@tailwindcss/forms
Introduction
Form elements are not always the easiest to style. @tailwindcss/forms convieniently provides a basic reset for form styles that makes form elements easy to override with Tailwind CSS utility classes. @tailwindcss/forms is designed for Tailwind CSS v2.0 and replaces tailwindcss-custom-forms which was designed to work with Tailwind CSS v1.0.
Getting Started
Starting with @tailwindcss/forms is fairly routine. Install the plugin:
# Using npm
npm install @tailwindcss/forms
# Using Yarn
yarn add @tailwindcss/forms
Then add the plugin to your tailwind.config.js
file:
module.exports = {
theme: {
// ...
},
plugins: [
require('@tailwindcss/forms'),
// ...
],
}
Usage
I found that the easiest way to get started is to look at the provided live demo examples. For this post, we will recreate the contact form found on the homepage of jillian.dev. We will start by using dev tools to copy the underline-style form example and modify the fields as needed (also adjusting the HTML to be valid JSX/TSX):
<form name="contact">
<div className="grid grid-cols-1 gap-6">
<label className="block">
<span className="text-stone-700">Name</span>
<input type="text" className="mt-0 block w-full px-0.5 border-0 border-b-2 border-stone-200 focus:ring-0 focus:border-black" placeholder="">
</label>
<label className="block">
<span className="text-stone-700">Email</span>
<input type="email" className="mt-0 block w-full px-0.5 border-0 border-b-2 border-stone-200 focus:ring-0 focus:border-black" placeholder="">
</label>
<label className="block">
<span className="text-stone-700">Message</span>
<textarea className="mt-0 block w-full px-0.5 border-0 border-b-2 border-stone-200 focus:ring-0 focus:border-black" rows={4}></textarea>
</label>
</div>
</div>
Which should display like this:
With a nice reset in place, now we can just add or remove any of the Tailwind CSS utility classes that we want in order to style the form to our liking:
<form name="contact">
<div className="grid grid-cols-1 gap-4 mt-8">
<label className="block">
<span className="text-sm tracking-wider text-stone-600">Name</span>
<div className="relative">
<input
className="block w-full pl-3 pr-10 mt-1 text-lg border-0 border-l-4 border-purple-300 rounded-lg shadow-md text-stone-600 bg-stone-200 focus:ring-0 focus:border-fuchsia-500"
aria-required="true"
placeholder=""
type="text"
name="name"
/>
</div>
</label>
<label className="block">
<span className="text-sm tracking-wider text-stone-600">Email</span>
<div className="relative">
<input
className="block w-full pl-3 pr-10 mt-1 text-lg border-0 border-l-4 border-purple-300 rounded-lg shadow-md text-stone-600 bg-stone-200 focus:ring-0 focus:border-fuchsia-500"
aria-required="true"
type="email"
name="email"
/>
</div>
</label>
<label className="block">
<span className="text-sm tracking-wider text-stone-600">Message</span>
<div className="relative">
<textarea
className="block w-full pl-3 pr-10 mt-1 text-lg border-0 border-l-4 border-purple-300 rounded-lg shadow-md text-stone-600 bg-stone-200 focus:ring-0 focus:border-fuchsia-500"
aria-required="true"
rows={4}
name="message"
/>
</div>
</label>
<button
className="outline-none focus:outline-none hover:outline-none active:outline-none overflow-hidden text-center tracking-wide transition block active:shadow-sm duration-200 ring-1 ring-offset-1 focus:ring-1 focus:ring-offset-1 hover:ring-1 hover:ring-offset-1 active:ring-1 active:ring-offset-1 px-6 py-2 rounded-full shadow-lg bg-purple-300 text-purple-900 font-medium ring-offset-purple-300 ring-purple-200 ring-opacity-75 focus:ring-offset-purple-700 focus:ring-purple-200 focus:ring-opacity-75 hover:ring-offset-purple-700 hover:ring-purple-200 hover:ring-opacity-75 active:ring-offset-purple-700 active:ring-purple-200 active:ring-opacity-75 transform hover:-translate-y-0.5 focus:-translate-y-0.5 active:translate-y-0.5 ease-in-out"
role="button"
title="Submit"
type="submit"
>
Submit
</button>
</div>
</form>
Note that on lines 6, 18, and 29 I preemptively changed the div
's to display: relative;
since we will be using those elements to position icons for visual feedback during form field validation in the next post. Of course I also added a button since that is an important part of any form 😊. And there we have it, a gorgeously styled and accessible contact form:
Lets refactor this a bit to deal with our unwieldy Tailwind CSS class names so that our ContactForm
component should look as follows:
import React, { ReactNode } from 'react'
import { formatClassList } from '../../utils/utils'
import Button from '../../components/button'
const FIELD_BASE: string = `
bg-stone-200
block
border-0
border-l-4
focus:ring-0
mt-1
pl-3
pr-10
rounded-lg
shadow-md
text-stone-600
text-lg
w-full
`
const FIELD: string = `
${FIELD_BASE}
border-purple-300
focus:border-fuchsia-500
`
const GRID: string = `
gap-4
grid
grid-cols-1
mt-8
`
const LABEL: string = `
text-stone-600
text-sm
tracking-wider
`
const ContactForm = ({}: ReactNode) => {
const formattedField: string = formatClassList(FIELD)
const formattedLabel: string = formatClassList(LABEL)
const formattedGrid: string = formatClassList(GRID)
return (
<form name="contact">
<div className={formattedGrid}>
<label className="block">
<span className={formattedLabel}>Name</span>
<div className="relative">
<input
className={formattedField}
aria-required="true"
placeholder=""
type="text"
name="name"
/>
</div>
</label>
<label className="block">
<span className={formattedLabel}>Email</span>
<div className="relative">
<input
className={formattedField}
aria-required="true"
type="email"
name="email"
/>
</div>
</label>
<label className="block">
<span className={formattedLabel}>Message</span>
<div className="relative">
<textarea
className={formattedField}
aria-required="true"
rows={4}
name="message"
/>
</div>
</label>
<Button
action="primary"
className="mt-4"
role="button"
title="Submit"
type="submit"
disabled={false}
>
Send your message
</Button>
</div>
</form>
)
}
export default ContactForm
For further understanding and an in-depth look that my approach to keeping components tidy while working with Tailwind CSS please see Tidy React-Typescript component design with Tailwind CSS.
Above I have also abstracted the button into its own component, which keeps things DRY and further cleans up the TSX in the contact form component.
Note the Button import. As a standard practice, I keep an index.ts
file inside of all component subdirectories (including contact-form
) which cleans up imports throughout the app.
./src/components/button/index.ts
export { default } from './button'
Now would be a prudent opportunity to break parts of our form component into smaller subcomponents before we start adding a bunch of form validation logic. Otherwise our form component will become difficult to maintain. Including our button component, which we aren't going to cover in this post, we can abstract two more subcomponents from this form (within reason1): TextInput
and TextArea
.
./text-input.tsx
import React from 'react'
import { formatClassList } from '../../utils/utils'
export type TextInputProps = {
label: string
}
const FIELD_BASE: string = `
bg-stone-200
block
border-0
border-l-4
focus:ring-0
mt-1
pl-3
pr-10
rounded-lg
shadow-md
text-stone-600
text-lg
w-full
`
const FIELD: string = `
${FIELD_BASE}
border-purple-300
focus:border-fuchsia-500
`
const LABEL: string = `
text-stone-600
text-sm
tracking-wider
`
const TextInput = ({
label
}: TextInputProps) => {
label = label.toLowerCase()
const formattedField: string = formatClassList(FIELD)
const formattedLabel: string = formatClassList(LABEL)
return (
<label className="block">
<span className={formattedLabel}>{label.charAt(0).toUpperCase() + label.slice(1)}</span>
<div className="relative">
<input
className={formattedField}
aria-required="true"
placeholder=""
type="text"
name={label}
/>
</div>
</label>
)
}
export default TextInput
./text-area.tsx
import React from 'react'
import { formatClassList } from '../../utils/utils'
export type TextAreaProps = {
label: string
}
const FIELD_BASE: string = `
bg-stone-200
block
border-0
border-l-4
focus:ring-0
mt-1
pl-3
pr-10
rounded-lg
shadow-md
text-stone-600
text-lg
w-full
`
const FIELD: string = `
${FIELD_BASE}
border-purple-300
focus:border-fuchsia-500
`
const LABEL: string = `
text-stone-600
text-sm
tracking-wider
`
const TextInput = ({
label
}: TextAreaProps) => {
label = label.toLowerCase()
const formattedField: string = formatClassList(FIELD)
const formattedLabel: string = formatClassList(LABEL)
return (
<label className="block">
<span className={formattedLabel}>{label.charAt(0).toUpperCase() + label.slice(1)}</span>
<div className="relative">
<textarea
className={formattedField}
aria-required="true"
rows={4}
name={label}
/>
</div>
</label>
)
}
export default TextInput
And for completeness, lets turn our grid into a component as well.
./grid.tsx
import React, { ReactNode } from 'react'
import { formatClassList } from '../../utils/utils'
export type GridProps = {
children: ReactNode
}
const GRID: string = `
gap-4
grid
grid-cols-1
mt-8
`
const Grid = ({children}: GridProps) => {
const formattedGrid: string = formatClassList(GRID)
return(
<div className={formattedGrid}>{children}</div>
)
}
export default Grid
So our form component is now in good shape for managing the added complexity of React Hook Form.
import React, { ReactNode } from 'react'
import { formatClassList } from '../../utils/utils'
import Button from '../../components/button'
import Grid from './grid'
import TextArea from './text-area'
import TextInput from './text-input'
const ContactForm = ({}: ReactNode) => {
return (
<form name="contact">
<Grid>
<TextInput
label="name"
/>
<TextInput
label="email"
/>
<TextArea
label="message"
/>"
<Button
action="primary"
className="mt-4"
role="button"
title="Submit"
type="submit"
disabled={false}
>
Send your message
</Button>
</Grid>
</form>
)
}
export default ContactForm
Next up, we will dive into adding React Hook Form client-side form field validation to this form.
1I'm not a fan of over-abstraction, ie. componentizing too much bloats a codebase and makes it just as difficult to follow as it would be if one hadn't broken it into components at all. Balance is important here and takes good judgement. Before breaking a piece into a component ask yourself if it will be beneficial to do so.