src/blog/tailwindcss-v3-1/index.mdx
import { adamwathan } from "@/app/blog/authors"; import { CodeExample, js, css, CodeExampleGroup, CodeBlock } from "@/components/code-example"; import { Example } from "@/components/example.tsx"; import { Figure } from "@/components/figure"; import card from "./card.jpg"; import discordMessage from "./discord-message.png"; import { Image, YouTubeVideo } from "@/components/media"; import Link from "next/link";
export const meta = {
title: "Tailwind CSS v3.1: You wanna get nuts? Come on, let's get nuts!",
description: Now with first-party TypeScript types, arbitrary variants, improved CSS variable DX, and tons more.,
date: "2022-06-07T15:00:00.000Z",
authors: [adamwathan],
image: card,
excerpt: (
<>
It's been about six months since we released <Link href="/blog/tailwindcss-v3">Tailwind CSS v3.0</Link>, and even
though we've been collecting a lot of little improvements in the codebase since then, we just didn't have{" "}
<em>that-one-feature</em> yet that makes you say
<em>"okay, it's release-cuttin' time"</em>.
</>
),
};
It's been about six months since we released Tailwind CSS v3.0, and even though we've been collecting a lot of little improvements in the codebase since then, we just didn't have that-one-feature yet that makes you say "okay, it's release-cuttin' time".
Then on a random Saturday night a couple of weeks ago, I was talking to Robin in our Discord about coming up with a way to target the html element using :has and a class deeper in the document, and explained how I thought it would look if we added support for arbitrary variants — something I've wanted to tackle for over a year:
<Image
alt={Adam Wathan: I think if we do arbitrary variants, the syntax should just be that exact thing, '[html:has(&)]:bg-blue-500'. Feel like that is pretty flexible, like anything you can do with a real variant you can also do with an arbitrary variant since they are the same thing. '[&>*:not(:first-child)]:pl-4'. Robin: This is going to break my brain haha because '[html:has(&)]:bg-blue-500' would be used as a literal inside the '&'. That in combination with other variants... 🤯. Adam Wathan: 😅 it'll be a brain melter for sure. The CSS would be this lol 'html:has([html:has(&)]:bg-blue-500 {"{"} background: blue 500 }'. Robin: exactly haha. ok, now I want to try that brb.}
src={discordMessage}
/>
Twenty minutes later Robin had a working proof of concept (in six lines of code!), and after another hour or so of Jordan performing regex miracles in our class detection engine, arbitrary variants were born and we had our release-worthy feature.
So here it is — Tailwind CSS v3.1! For a complete list of every fix and improvement check out the release notes, but here's the highlights:
Upgrade your projects by installing the latest version of tailwindcss from npm:
npm install tailwindcss@latest
Or spin up a Tailwind Play to play around with all of the new goodies right in the browser.
We're now shipping types for all of our JS APIs you work with when using Tailwind, most notably the tailwind.config.js file. This means you get all sorts of useful IDE support, and makes it a lot easier to make changes to your configuration without referencing the documentation quite as much.
To set it up, just add the type annotation above your config definition:
// [!code filename:tailwind.config.js]
/** @type {import('tailwindcss').Config} */ // [!code ++]
module.exports = {
content: [
// ...
],
theme: {
extend: {},
},
plugins: [],
};
If you're a big TypeScript nerd you might enjoy poking around the actual type definitions — lots of interesting stuff going on there to support such a potentially complex object.
If you're using our CLI tool to compile your CSS, postcss-import is now baked right in so you can organize your custom CSS into multiple files without any additional configuration.
<CodeExampleGroup filenames={["main.css", "select2-theme.css"]}>
<CodeBlock
example={css @import "tailwindcss/base"; /* [!code highlight:2] */ @import "./select2-theme.css"; @import "tailwindcss/components"; @import "tailwindcss/utilities"; }
/>
<CodeBlock
example={css .select2-dropdown { @apply rounded-b-lg shadow-md; } .select2-search { @apply rounded border border-gray-300; } .select2-results__group { @apply text-lg font-bold text-gray-900; } /* ... */ }
/>
</CodeExampleGroup>
If you're not using our CLI tool and instead using Tailwind as a PostCSS plugin, you'll still need to install and configure postcss-import yourself just like you do with autoprefixer, but if you are using our CLI tool this will totally just work now.
This is especially handy if you're using our standalone CLI and don't want to install any node dependencies at all.
I don't think tons of people know about this, but Tailwind exposes a theme() function to your CSS files that lets you grab values from your config file — sort of turning them into variables that you can reuse.
/* [!code filename:select2-theme.css] */
.select2-dropdown {
border-radius: theme(borderRadius.lg);
background-color: theme(colors.gray.100);
color: theme(colors.gray.900);
}
/* ... */
One limitation though was that you couldn't adjust the alpha channel any colors you grabbed this way. So in v3.1 we've added support for using a slash syntax to adjust the opacity, like you can with the modern rgb and hsl CSS color functions:
/* [!code filename:select2-theme.css] */
.select2-dropdown {
border-radius: theme(borderRadius.lg);
/* [!code highlight:2] */
background-color: theme(colors.gray.100 / 50%);
color: theme(colors.gray.900);
}
/* ... */
We've made this work with the theme function in your tailwind.config.js file, too:
// [!code filename:tailwind.config.js]
module.exports = {
content: [
// ...
],
theme: {
extend: {
colors: ({ theme }) => ({
primary: theme("colors.blue.500"),
// [!code highlight:2]
"primary-fade": theme("colors.blue.500 / 75%"),
}),
},
},
plugins: [],
};
You can even use this stuff in arbitrary values which is pretty wild — honestly surprisingly useful for weird custom gradients and stuff:
<div class="bg-[image:linear-gradient(to_right,theme(colors.red.500)_75%,theme(colors.red.500/25%))]">
<!-- ... -->
</div>
Anything to avoid editing a CSS file am I right?
If you like to define and configure your colors as CSS variables, you probably have some horrible boilerplate like this in your tailwind.config.js file right now:
// [!code filename:tailwind.config.js]
function withOpacityValue(variable) {
return ({ opacityValue }) => {
if (opacityValue === undefined) {
return `rgb(var(${variable}))`;
}
return `rgb(var(${variable}) / ${opacityValue})`;
};
}
module.exports = {
theme: {
colors: {
primary: withOpacityValue("--color-primary"),
secondary: withOpacityValue("--color-secondary"),
// ...
},
},
};
We've made this way less awful in v3.1 by adding support for defining your colors with a format string instead of having to use a function:
// [!code filename:tailwind.config.js]
module.exports = {
theme: {
colors: {
primary: "rgb(var(--color-primary) / <alpha-value>)",
secondary: "rgb(var(--color-secondary) / <alpha-value>)",
// ...
},
},
};
Instead of writing a function that receives that opacityValue argument, you can just write a string with an <alpha-value> placeholder, and Tailwind will replace that placeholder with the correct alpha value based on the utility.
If you haven't seen any of this before, check out our updated Using CSS variables documentation for more details.
We've added new set of utilities for the border-spacing property, so you can control the space between table borders when using separate borders:
<!-- [!code word:border-spacing-2] -->
<table class="border-separate border-spacing-2 ...">
<thead>
<tr>
<th class="border border-slate-300 ...">State</th>
<th class="border border-slate-300 ...">City</th>
</tr>
</thead>
<tbody>
<tr>
<td class="border border-slate-300 ...">Indiana</td>
<td class="border border-slate-300 ...">Indianapolis</td>
</tr>
<!-- ... -->
</tbody>
</table>
I know what you're thinking — "I have never in my life wanted to build a table that looks like that..." — but listen for a second!
One situation where this is actually super useful is when building a table with a sticky header row and you want a persistent bottom border under the headings:
<Figure hint="Scroll this table to see the sticky header row in action"> <Example padding={false}> { <div className="isolate h-72 overflow-auto rounded-xl"> <table className="min-w-full border-separate border-spacing-0"> <thead className="bg-gray-50"> <tr> <th scope="col" className="bg-opacity-75 sticky top-0 z-10 border-b border-gray-300 bg-gray-50 py-3.5 pr-3 pl-4 text-left text-sm font-semibold text-gray-900 backdrop-blur backdrop-filter sm:pl-6 lg:pl-8" > <>Name</> </th> <th scope="col" className="bg-opacity-75 sticky top-0 z-10 hidden border-b border-gray-300 bg-gray-50 px-3 py-3.5 text-left text-sm font-semibold text-gray-900 backdrop-blur backdrop-filter lg:table-cell" > <>Email</> </th> <th scope="col" className="bg-opacity-75 sticky top-0 z-10 border-b border-gray-300 bg-gray-50 px-3 py-3.5 text-left text-sm font-semibold text-gray-900 backdrop-blur backdrop-filter" > <>Role</> </th> </tr> </thead> <tbody className="bg-white"> <tr> <td className="border-b border-gray-200 py-4 pr-3 pl-4 text-sm font-medium whitespace-nowrap text-gray-900 sm:pl-6 lg:pl-8"> <>Courtney Henry</> </td> <td className="hidden border-b border-gray-200 px-3 py-4 text-sm whitespace-nowrap text-gray-500 lg:table-cell"> <>[email protected]</> </td> <td className="border-b border-gray-200 px-3 py-4 text-sm whitespace-nowrap text-gray-500">Admin</td> </tr> <tr> <td className="border-b border-gray-200 py-4 pr-3 pl-4 text-sm font-medium whitespace-nowrap text-gray-900 sm:pl-6 lg:pl-8"> <>Tom Cook</> </td> <td className="hidden border-b border-gray-200 px-3 py-4 text-sm whitespace-nowrap text-gray-500 lg:table-cell"> <>[email protected]</> </td> <td className="border-b border-gray-200 px-3 py-4 text-sm whitespace-nowrap text-gray-500">Member</td> </tr> <tr> <td className="border-b border-gray-200 py-4 pr-3 pl-4 text-sm font-medium whitespace-nowrap text-gray-900 sm:pl-6 lg:pl-8"> <>Whitney Francis</> </td> <td className="hidden border-b border-gray-200 px-3 py-4 text-sm whitespace-nowrap text-gray-500 lg:table-cell"> <>[email protected]</> </td> <td className="border-b border-gray-200 px-3 py-4 text-sm whitespace-nowrap text-gray-500">Admin</td> </tr> <tr> <td className="border-b border-gray-200 py-4 pr-3 pl-4 text-sm font-medium whitespace-nowrap text-gray-900 sm:pl-6 lg:pl-8"> <>Leonard Krasner</> </td> <td className="hidden border-b border-gray-200 px-3 py-4 text-sm whitespace-nowrap text-gray-500 lg:table-cell"> <>[email protected]</> </td> <td className="border-b border-gray-200 px-3 py-4 text-sm whitespace-nowrap text-gray-500">Owner</td> </tr> <tr> <td className="border-b border-gray-200 py-4 pr-3 pl-4 text-sm font-medium whitespace-nowrap text-gray-900 sm:pl-6 lg:pl-8"> <>Floyd Miles</> </td> <td className="hidden border-b border-gray-200 px-3 py-4 text-sm whitespace-nowrap text-gray-500 lg:table-cell"> <>[email protected]</> </td> <td className="border-b border-gray-200 px-3 py-4 text-sm whitespace-nowrap text-gray-500">Member</td> </tr> <tr> <td className="border-b border-gray-200 py-4 pr-3 pl-4 text-sm font-medium whitespace-nowrap text-gray-900 sm:pl-6 lg:pl-8"> <>Emily Selman</> </td> <td className="hidden border-b border-gray-200 px-3 py-4 text-sm whitespace-nowrap text-gray-500 lg:table-cell"> <>[email protected]</> </td> <td className="border-b border-gray-200 px-3 py-4 text-sm whitespace-nowrap text-gray-500">Member</td> </tr> <tr> <td className="border-b border-gray-200 py-4 pr-3 pl-4 text-sm font-medium whitespace-nowrap text-gray-900 sm:pl-6 lg:pl-8"> <>Kristin Watson</> </td> <td className="hidden border-b border-gray-200 px-3 py-4 text-sm whitespace-nowrap text-gray-500 lg:table-cell"> <>[email protected]</> </td> <td className="border-b border-gray-200 px-3 py-4 text-sm whitespace-nowrap text-gray-500">Admin</td> </tr> <tr> <td className="border-b border-gray-200 py-4 pr-3 pl-4 text-sm font-medium whitespace-nowrap text-gray-900 sm:pl-6 lg:pl-8"> <>Emma Dorsey</> </td> <td className="hidden border-b border-gray-200 px-3 py-4 text-sm whitespace-nowrap text-gray-500 lg:table-cell"> <>[email protected]</> </td> <td className="border-b border-gray-200 px-3 py-4 text-sm whitespace-nowrap text-gray-500">Member</td> </tr> <tr> <td className="border-b border-gray-200 py-4 pr-3 pl-4 text-sm font-medium whitespace-nowrap text-gray-900 sm:pl-6 lg:pl-8"> <>Alicia Bell</> </td> <td className="hidden border-b border-gray-200 px-3 py-4 text-sm whitespace-nowrap text-gray-500 lg:table-cell"> <>[email protected]</> </td> <td className="border-b border-gray-200 px-3 py-4 text-sm whitespace-nowrap text-gray-500">Admin</td> </tr> <tr> <td className="border-b border-gray-200 py-4 pr-3 pl-4 text-sm font-medium whitespace-nowrap text-gray-900 sm:pl-6 lg:pl-8"> <>Jenny Wilson</> </td> <td className="hidden border-b border-gray-200 px-3 py-4 text-sm whitespace-nowrap text-gray-500 lg:table-cell"> <>[email protected]</> </td> <td className="border-b border-gray-200 px-3 py-4 text-sm whitespace-nowrap text-gray-500">Owner</td> </tr> <tr> <td className="border-b border-gray-200 py-4 pr-3 pl-4 text-sm font-medium whitespace-nowrap text-gray-900 sm:pl-6 lg:pl-8"> <>Anna Roberts</> </td> <td className="hidden border-b border-gray-200 px-3 py-4 text-sm whitespace-nowrap text-gray-500 lg:table-cell"> <>[email protected]</> </td> <td className="border-b border-gray-200 px-3 py-4 text-sm whitespace-nowrap text-gray-500">Member</td> </tr> <tr> <td className="border-b border-gray-200 py-4 pr-3 pl-4 text-sm font-medium whitespace-nowrap text-gray-900 sm:pl-6 lg:pl-8"> <>Benjamin Russel</> </td> <td className="hidden border-b border-gray-200 px-3 py-4 text-sm whitespace-nowrap text-gray-500 lg:table-cell"> <>[email protected]</> </td> <td className="border-b border-gray-200 px-3 py-4 text-sm whitespace-nowrap text-gray-500">Member</td> </tr> <tr> <td className="border-b border-gray-200 py-4 pr-3 pl-4 text-sm font-medium whitespace-nowrap text-gray-900 sm:pl-6 lg:pl-8"> <>Jeffrey Webb</> </td> <td className="hidden border-b border-gray-200 px-3 py-4 text-sm whitespace-nowrap text-gray-500 lg:table-cell"> <>[email protected]</> </td> <td className="border-b border-gray-200 px-3 py-4 text-sm whitespace-nowrap text-gray-500">Admin</td> </tr> <tr> <td className="border-b border-gray-200 py-4 pr-3 pl-4 text-sm font-medium whitespace-nowrap text-gray-900 sm:pl-6 lg:pl-8"> <>Kathryn Murphy</> </td> <td className="hidden border-b border-gray-200 px-3 py-4 text-sm whitespace-nowrap text-gray-500 lg:table-cell"> <>[email protected]</> </td> <td className="border-b border-gray-200 px-3 py-4 text-sm whitespace-nowrap text-gray-500">Member</td> </tr> </tbody> </table> </div> } </Example><!-- [!code word:border-separate] -->
<!-- [!code word:border-spacing-0] -->
<table class="border-separate border-spacing-0">
<thead class="bg-gray-50">
<tr>
<th class="sticky top-0 z-10 border-b border-gray-300 ...">Name</th>
<th class="sticky top-0 z-10 border-b border-gray-300 ...">Email</th>
<th class="sticky top-0 z-10 border-b border-gray-300 ...">Role</th>
</tr>
</thead>
<tbody class="bg-white">
<tr>
<td class="border-b border-gray-200 ...">Courtney Henry</td>
<td class="border-b border-gray-200 ...">[email protected]</td>
<td class="border-b border-gray-200 ...">Admin</td>
</tr>
<!-- ... -->
</tbody>
</table>
You might think you could just use border-collapse here since you actually don't want any space between the borders but you'd be mistaken. Without border-separate and border-spacing-0, the border will scroll away instead of sticking under the headings. CSS is fun isn't it?
Check out the border spacing documentation for some more details.
We've added new variants for the :enabled and :optional pseudo-classes, which target form elements when they are, well, enabled and optional.
"But Adam why would I ever need these, enabled and optional aren't even states, they are the defaults. Do you even make websites?"
Ouch, that hurts because it's true — I pretty much just write emails and answer the same questions over and over again on GitHub now.
But check out this disabled button example:
<Figure> <Example> { <div className="flex items-center justify-center"> <button type="button" className="inline-flex cursor-not-allowed items-center rounded-md bg-indigo-500 px-4 py-2 text-sm leading-6 font-semibold text-white shadow transition duration-150 ease-in-out hover:bg-indigo-400 disabled:opacity-75" disabled > Processing... </button> </div> } </Example><button type="button" class="bg-indigo-500 hover:bg-indigo-400 disabled:opacity-75 ..." disabled>Processing...</button>
Notice how when you hover over the button, the background still changes color even though it's disabled? Before this release, you'd usually fix that like this:
<!-- [!code word:disabled\:hover\:bg-indigo-500] -->
<button
type="button"
class="bg-indigo-500 hover:bg-indigo-400 disabled:opacity-75 disabled:hover:bg-indigo-500 ..."
disabled
>
Processing...
</button>
But with the new enabled modifier, you can write it like this instead:
<!-- [!code word:hover\:enabled\:bg-indigo-400] -->
<button type="button" class="bg-indigo-500 hover:enabled:bg-indigo-400 disabled:opacity-75 ..." disabled>
Processing...
</button>
Instead of overriding the hover color back to the default color when the button is disabled, we combine the hover and enabled variants to just not apply the hover styles when the button is disabled in the first place. I think that's better!
Here's an example combining the new optional modifier with our sibling state features to hide a little "Required" notice for fields that aren't required:
<form>
<div>
<label for="email" ...>Email</label>
<div>
<input required class="peer ..." id="email" />
<!-- [!code word:peer-optional\:hidden] -->
<div class="peer-optional:hidden ...">Required</div>
</div>
</div>
<div>
<label for="name" ...>Name</label>
<div>
<input class="peer ..." id="name" />
<div class="peer-optional:hidden ...">Required</div>
</div>
</div>
<!-- ... -->
</form>
This lets you use the same markup for all of your form groups and letting CSS handle all of the conditional rendering for you instead of handling it yourself. Kinda neat!
Did you know there's a prefers-contrast media query? Well there is, and now Tailwind supports it out of the box.
Use the new contrast-more and contrast-less variants to modify your design when the user has requested more or less contrast, usually through an operating system accessibility preference like "Increase contrast" on macOS.
<form>
<label class="block">
<span class="block text-sm font-medium text-slate-700">Social Security Number</span>
<!-- [!code word:contrast-more\:border-slate-400] -->
<!-- [!code word:contrast-more\:placeholder-slate-500] -->
<!-- [!code word:contrast-more\:opacity-100] -->
<input
class="border-slate-200 placeholder-slate-400 contrast-more:border-slate-400 contrast-more:placeholder-slate-500"
/>
<p class="mt-2 text-sm text-slate-600 opacity-10 contrast-more:opacity-100">We need this to steal your identity.</p>
</label>
</form>
I wrote some documentation for this but honestly I wrote more here than I did there.
There's a pretty new HTML <dialog> element with surprisingly decent browser support that is worth playing with if you like to live on the bleeding edge.
Dialogs have this new ::backdrop pseudo-element that's rendered while the dialog is open, and Tailwind CSS v3.1 adds a new backdrop modifier you can use to style this baby:
<dialog class="backdrop:bg-slate-900/50 ...">
<form method="dialog">
<!-- ... -->
<button value="cancel">Cancel</button>
<button>Submit</button>
</form>
</dialog>
I recommend reading the MDN Dialog documentation if you want to dig in to this thing more — it's exciting stuff but there's a lot to know.
Okay so this one is the real highlight for me — you know how we give you the addVariant API for creating your own custom variants?
// [!code filename:tailwind.config.js]
const plugin = require("tailwindcss/plugin");
module.exports = {
// ...
plugins: [
plugin(function ({ addVariant }) {
// [!code highlight:2]
addVariant("third", "&:nth-child(3)");
}),
],
};
...and you know how we have arbitrary values for using any value you want with a utility directly in your HTML?
<!-- [!code word:top-[117px]] -->
<div class="top-[117px]">
<!-- ... -->
</div>
Well Tailwind CSS v3.1 introduces arbitrary variants, letting you create your own ad hoc variants directly in your HTML:
<!-- [!code word:[&\:nth-child(3)\]] -->
<div class="[&:nth-child(3)]:py-0">
<!-- ... -->
</div>
This is super useful for variants that sort of feel like they need to be parameterized, for example adding a style if the browser supports a specific CSS feature using a @supports query:
<!-- [!code word:@supports(backdrop-filter\:blur(0))\]\:bg-white/50] -->
<!-- [!code word:[@supports(backdrop-filter\:blur(0))\]\:backdrop-blur] -->
<div
class="bg-white [@supports(backdrop-filter:blur(0))]:bg-white/50 [@supports(backdrop-filter:blur(0))]:backdrop-blur"
>
<!-- ... -->
</div>
You can even use this feature to target child elements with arbitrary variants like [&>*]:
<div className="ml-3 overflow-hidden">
<p className="text-sm font-medium text-slate-900">Kristen Ramos</p>
<p className="truncate text-sm text-slate-500">[email protected]</p>
</div>
</li>
<li className="flex">
<div className="ml-3 overflow-hidden">
<p className="text-sm font-medium text-slate-900">Floyd Miles</p>
<p className="truncate text-sm text-slate-500">[email protected]</p>
</div>
</li>
<li className="flex">
<div className="ml-3 overflow-hidden">
<p className="text-sm font-medium text-slate-900">Courtney Henry</p>
<p className="truncate text-sm text-slate-500">[email protected]</p>
</div>
</li>
</ul>
</div>
} </Example>
<!-- [!code word:[&>*\]\:p-4] -->
<!-- [!code word:[&>*\]\:bg-white] -->
<!-- [!code word:[&>*\]\:rounded-lg] -->
<!-- [!code word:[&>*\]\:shadow] -->
<ul role="list" class="space-y-4 [&>*]:rounded-lg [&>*]:bg-white [&>*]:p-4 [&>*]:shadow">
<li class="flex">
<div class="ml-3 overflow-hidden">
<p class="text-sm font-medium text-slate-900">Kristen Ramos</p>
<p class="truncate text-sm text-slate-500">[email protected]</p>
</div>
</li>
<!-- ... -->
</ul>
You can even style the first p inside the div in the second child li but only on hover:
<div className="ml-3 overflow-hidden">
<p className="text-sm font-medium text-slate-900">Kristen Ramos</p>
<p className="truncate text-sm text-slate-500">[email protected]</p>
</div>
</li>
<li className="flex">
<div className="ml-3 overflow-hidden">
<p className="text-sm font-medium text-slate-900">Floyd Miles</p>
<p className="truncate text-sm text-slate-500">[email protected]</p>
</div>
</li>
<li className="flex">
<div className="ml-3 overflow-hidden">
<p className="text-sm font-medium text-slate-900">Courtney Henry</p>
<p className="truncate text-sm text-slate-500">[email protected]</p>
</div>
</li>
</ul>
</div>
} </Example>
<!-- [!code word:hover\:[&>li\:nth-child(2)>div>p\:first-child\]\:text-indigo-500] -->
<ul
role="list"
class="space-y-4 [&>*]:rounded-lg [&>*]:bg-white [&>*]:p-4 [&>*]:shadow hover:[&>li:nth-child(2)>div>p:first-child]:text-indigo-500"
>
<!-- ... -->
<li class="flex">
<div class="ml-3 overflow-hidden">
<p class="text-sm font-medium text-slate-900">Floyd Miles</p>
<p class="truncate text-sm text-slate-500">[email protected]</p>
</div>
</li>
<!-- ... -->
</ul>
Now should you do this? Probably not very often, but honestly it can be a pretty useful escape hatch when trying to style HTML you can't directly change. It's a sharp knife, but the best chefs aren't preparing food with safety scissors.
Play with them a bit and I'll bet you find they are a great tool when the situation calls for it. We're using them in a couple of tricky spots in these new website templates we're working on and the experience is much nicer than creating a custom class.
So that's Tailwind CSS v3.1! It's only a minor version change, so there are no breaking changes and you should be able to update your project by just installing the latest version:
npm install tailwindcss@latest
For the complete list of changes including bug fixes and a few minor improvements I didn't talk about here, dig in to the release notes on GitHub.
I've already got a bunch of ideas for Tailwind CSS v3.2 (maybe even text shadows finally?!), but right now we're working hard to push these new website templates over the finish line. Look for another update on that topic in the next week or two!