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:
![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.](https://tailwindcss.com/_next/static/media/discord-message.62df985f9f9584432c8afe9761d13d1e.png)
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 improvemement check out the release notes, but here’s the highlights:
- First-party TypeScript types
- Built-in support for CSS imports in the CLI
- Change color opacity when using the theme function
- Easier CSS variable color configuration
- Border spacing utilities
- Enabled and optional variants
- Prefers-contrast variants
- Style native dialog backdrops
- Arbitrary values but for variants
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:
/** @type {import('tailwindcss').Config} */
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.
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.
.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:
.select2-dropdown {
border-radius: theme(borderRadius.lg);
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:
module.exports = {
content: [
// ...
],
theme: {
extend: {
colors: ({ theme }) => ({
primary: theme('colors.blue.500'),
'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:
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:
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:
State | City |
---|---|
Indiana | Indianapolis |
Ohio | Columbus |
Michigan | Detroit |
<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>
<table class="border-separate border-spacing-2 ..."> <thead> <tr> <th class="border border-slate-600 ...">State</th> <th class="border border-slate-600 ...">City</th> </tr> </thead> <tbody> <tr> <td class="border border-slate-700 ...">Indiana</td> <td class="border border-slate-700 ...">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:
Scroll this table to see the sticky header row in action
Name | Role |
---|---|
Courtney Henry | Admin |
Tom Cook | Member |
Whitney Francis | Admin |
Leonard Krasner | Owner |
Floyd Miles | Member |
Emily Selman | Member |
Kristin Watson | Admin |
Emma Dorsey | Member |
Alicia Bell | Admin |
Jenny Wilson | Owner |
Anna Roberts | Member |
Benjamin Russel | Member |
Jeffrey Webb | Admin |
Kathryn Murphy | Member |
<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 ...">This email address is being protected from spambots. You need JavaScript enabled to view it. </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:
<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:
<button type="button" class="disabled:hover:bg-indigo-500 bg-indigo-500 hover:bg-indigo-400 disabled:opacity-75 ..." disabled>
Processing...
</button>
But with the new enabled
modifier, you can write it
like this instead:
<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" />
<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.
Try emulating `prefers-contrast: more` in your developer tools to see the changes
We need this to steal your identity.
<form>
<label class="block">
<span class="block text-sm font-medium text-slate-700">Social Security Number</span>
<input class="border-slate-200 placeholder-slate-400 contrast-more:border-slate-400 contrast-more:placeholder-slate-500"/>
<p class="mt-2 opacity-10 contrast-more:opacity-100 text-slate-600 text-sm">
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?
const plugin = require('tailwindcss/plugin')
module.exports = {
// ...
plugins: [
plugin(function({ addVariant }) {
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?
<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:
<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:
<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 [&>*]
:
Kristen Ramos
This email address is being protected from spambots. You need JavaScript enabled to view it. Floyd Miles
This email address is being protected from spambots. You need JavaScript enabled to view it. Courtney Henry
This email address is being protected from spambots. You need JavaScript enabled to view it.
<ul role="list" class="[&>*]:p-4 [&>*]:bg-white [&>*]:rounded-lg [&>*]:shadow space-y-4">
<li class="flex">
<img class="h-10 w-10 rounded-full" src="..." alt="" />
<div class="ml-3 overflow-hidden">
<p class="text-sm font-medium text-slate-900">Kristen Ramos</p>
<p class="text-sm text-slate-500 truncate">This email address is being protected from spambots. You need JavaScript enabled to view it. </p>
</div>
</li>
<!-- ... -->
</ul>
You can even style the first p
inside the
div
in the second child li
but only on
hover
:
Try hovering over the text “Floyd Miles”
Kristen Ramos
This email address is being protected from spambots. You need JavaScript enabled to view it. Floyd Miles
This email address is being protected from spambots. You need JavaScript enabled to view it. Courtney Henry
This email address is being protected from spambots. You need JavaScript enabled to view it.
<ul role="list" class="hover:[&>li:nth-child(2)>div>p:first-child]:text-indigo-500 [&>*]:p-4 [&>*]:bg-white [&>*]:rounded-lg [&>*]:shadow space-y-4">
<!-- ... -->
<li class="flex">
<img class="h-10 w-10 rounded-full" src="..." alt="" />
<div class="ml-3 overflow-hidden">
<p class="text-sm font-medium text-slate-900">Floyd Miles</p>
<p class="text-sm text-slate-500 truncate">This email address is being protected from spambots. You need JavaScript enabled to view it. </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!
(The post Tailwind CSS v3.1: You wanna get nuts? Come on, let's get nuts! appeared first on Tailwind CSS Blog.)