Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Tailwind CSS v3.2 Beta 2 #9574

Oct 16, 2022 · 16 comments · 19 replies
Discussion options

Hey folks! We've made some changes and improvements since the last Tailwind CSS v3.2 pre-release and I'd love your help testing this stuff for a few days before we (hopefully!) tag the final release later this week.

If you've already been testing since the first pre-release, you can read about just the changes since that release, but here's an overview of all of the new stuff coming in v3.2 in its latest form:

To install the latest pre-release, install our insiders build from npm:

npm install -D tailwindcss@insiders

You can also try the new features directly in Tailwind Play by choosing "Insiders" from the version dropdown, but it's better for us if you actually install it in your project so we can get more information about how all this stuff is going to work alongside different UI libraries, templating languages, and build tools.


Multiple config files in one project using @config

We've added a new @config directive that you can use in a CSS file to specify which Tailwind CSS config to use for that file:

@config "./tailwind.admin.config.js"
@tailwind base;
@tailwind components;
@tailwind utilities;

This makes it a lot easier to build multiple stylesheets in a single project that have separate Tailwind configurations. For example, you might have one config file for the customer-facing part of your site, and another config for the admin/backend area.

Config files specified with @config are resolved relative to the CSS file where the @config directive is being used.

One known pain-point right now is that when using postcss-import, you can't put @config at the top of the file because postcss-import is strict about all @import statements having to come first as per the CSS spec. We might wrap postcss-import with our own import plugin to remove this limitation, but for now if you're using @import statements, your @config directive will need to come after them:

@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
@config "./tailwind.admin.config.js";

The Tailwind CSS IntelliSense plugin also doesn't have support for this stuff in any sort of automatic way yet, so you might not see the completions you expect depending on how you have things configured. We're working on this though, and hope to have a solution in place before the actual v3.2 release.


New variant for @supports rules

You can now conditionally style things based on whether a certain feature is supported in the user's browser with the supports-[...] variant, which generates @supports rules under the hood.

<div class="flex supports-[display:grid]:grid ...">
 <!-- ... -->
</div>

The supports-[...] variant takes anything you'd use with @supports (...) between the square brackets, like a property/value pair, and even expressions using and and or.

For terseness, if you only need to check if a property is supported (and not a specific value), you can just specify the property name:

<div
 class="bg-black/75 supports-[backdrop-filter]:bg-black/25 supports-[backdrop-filter]:backdrop-blur ..."
>
 <!-- ... -->
</div>

New variants for aria-* attributes

You can now conditionally style things based on ARIA attributes with the new aria-* variants.

For example, you can update the background color of an element based on whether the aria-checked state is true:

<div aria-checked="true" class="bg-gray-600 aria-checked:bg-blue-600">
 <!-- ... -->
</div>

By default we've included variants for the most common boolean ARIA attributes:

Variant CSS
aria-checked: [aria-checked="true"]
aria-disabled: [aria-disabled="true"]
aria-expanded: [aria-expanded="true"]
aria-hidden: [aria-hidden="true"]
aria-pressed: [aria-pressed="true"]
aria-readonly: [aria-readonly="true"]
aria-required: [aria-required="true"]
aria-selected: [aria-selected="true"]

For more complex ARIA attributes that take specific values (like aria-activedescendant), you can use the arbitrary value/square bracket notation to create custom aria-* variants on the fly:

<th
 aria-sort="ascending"
 class="aria-[sort=ascending]:bg-[url('/img/up-arrow.svg')] aria-[sort=descending]:bg-[url('/img/down-arrow.svg')]"
>
 <!-- ... -->
</th>

You can customize which aria-* variants are available by editing theme.aria or theme.extend.aria in your tailwind.config.js file:

module.exports = {
 theme: {
 extend: {
 aria: {
 asc: 'sort="ascending"',
 desc: 'sort="descending"',
 },
 },
 },
};

Then you can use any custom aria-* variants you've configured in your project:

<th
 aria-sort="ascending"
 class="aria-asc:bg-[url('/img/up-arrow.svg')] aria-desc:bg-[url('/img/down-arrow.svg')"
>
 <!-- ... -->
</th>

These variants also work as group-* and peer-* variants like many other variants in the framework:

<div>Message:</div>
<div role="textbox" contenteditable aria-required="true" class="peer"></div>
<div class="hidden peer-aria-required:block">A message is required.</div>

New variants for data-* attributes

You can now conditionally style things based on data attributes with the new data-* variants.

Since there are no standard data-* attributes by definition, we only support arbitrary values out of the box, for example:

<!-- Will apply -->
<div data-size="large" class="data-[size=large]:p-8">
 <!-- ... -->
</div>
<!-- Will not apply -->
<div data-size="medium" class="data-[size=large]:p-8">
 <!-- ... -->
</div>
<!-- Generated CSS -->
<style>
 .data-\[size\=large\]\:p-8[data-size="large"] {
 padding: 2rem;
 }
</style>

You can configure shortcuts for common data attribute selectors you're using in your project under the data key in the theme section of your tailwind.config.js file:

// tailwind.config.js
module.exports = {
 theme: {
 data: {
 checked: 'ui~="checked"',
 },
 },
 // ...
};
<div data-ui="checked active" class="data-checked:underline">
 <!-- ... -->
</div>

These variants also work as group-* and peer-* variants like many other variants in the framework:

<div data-size="large" class="group">
 <div class="group-data-[size=large]:p-8">
 <!-- Will apply `p-8` -->
 </div>
</div>
<div data-size="medium" class="group">
 <div class="group-data-[size=large]:p-8">
 <!-- Will not apply `p-8` -->
 </div>
</div>

Max-width and dynamic breakpoints

We're trying out a way to support range-based media queries as well as dynamic/arbitrary breakpoints for situations where you just need to make a very specific tweak at a specific viewport size.

We've added a new max-* variant that lets you apply max-width media queries based on your configured breakpoints:

<div class="max-lg:p-8">
 <!-- Will apply `p-8` until the `lg` breakpoint kicks in -->
</div>

As a general rule I would still recommend using min-width breakpoints personally, but this feature does unlock one useful workflow benefit which is not having to undo some style at a different breakpoint.

For example, without this feature you often end up doing things like this:

<div class="md:sr-only xl:not-sr-only">
 <!-- ... -->
</div>

With this feature, you can avoid undoing that style by stacking a max-* variant on the original declaration:

<div class="md:max-xl:sr-only">
 <!-- ... -->
</div>

Along with this, we've added support for arbitrary values, and a new min-* variant that only accepts arbitrary values, so you can do things like this:

<div class="min-[712px]:max-[877px]:right-16 ...">
 <!-- ... -->
</div>

It's important to note that these features will only be available if your project uses a simple screens configuration.

These features are a lot more complicated than they look due to needing to ensure that all of these media queries are sorted in the final CSS in a way that gives you the expected behavior in the browser. So for now, they will only work if your screens configuration is a simple object with string values, like the default configuration:

// tailwind.config.js
module.exports = {
 theme: {
 screens: {
 sm: "640px",
 md: "768px",
 lg: "1024px",
 xl: "1280px",
 "2xl": "1536px",
 },
 },
};

If you have a complex configuration where you already have max-width breakpoints defined, or range-based media queries, or anything other than just strings, these features won't be available. We might be able to figure that out in the future but it just creates so many questions about how the CSS should be ordered that we don't have answers for yet. So for now (and possibly forever), if you want to use these features, your screens configuration needs to be simple. My hope is that these features make complex screens configurations unnecessary anyways.


Dynamic group-[...] and peer-[...] variants

We're making it possible to create custom group-* and peer-* variants on the fly by passing your selector between square brackets

<div>
 <!-- ... -->
 <a class="peer" href="#">...</a>
 <a class="peer-[:local-link]:ml-3">
 <!-- ... -->
 </a>
 <!-- ... -->
</div>

Whatever you pass will just get tacked on to the end of the .group or .peer class in the selector by default, but if you need to do something more complex you can use the & character to mark where .group/.peer should end up in the final selector relative to the selector you are passing in

<div class="group">
 <div class="group-[:nth-of-type(3)_&]:border-black">
 <!-- ... -->
 </div>
</div>

Parameterized variants with matchVariant

Both the new supports-[..] feature and dynamic group-*/peer-* variants are powered by a new matchVariant plugin API that you can use to create your own dynamic/parameterized variants.

Here's an example of creating a placement-* variant for some imaginary tooltip library that uses a data-placement attribute to tell you where the tooltip is currently positioned:

let plugin = require("tailwindcss/plugin");
module.exports = {
 // ...
 plugins: [
 plugin(function ({ matchVariant }) {
 matchVariant(
 "placement",
 ({ value }) => {
 return `&[data-placement=${value}]`;
 },
 {
 values: {
 t: "top",
 r: "right",
 b: "bottom",
 l: "left",
 },
 }
 );
 }),
 ],
};

The variant defined above would give you variants like placement-t and placement-b, but would also support the arbitrary portion in square brackets, so if this imaginary tooltip library had other potential values that you didn't feel the need to create built-in values for, you could still do stuff like this:

<div class="placement-[top-start]:mb-2 ...">
 <!-- ... -->
</div>

When defining a custom variant with this API, it's often important that you have some control over which order the CSS is generated in to make sure each class has the right precedence with respect to other values that come from the same variant. To support this, there's a sort function you can provide when defining your variant:

matchVariant("min", ({ value }) => `@media (min-width: ${value})`, {
 sort(a, z) {
 return parseInt(a) - parseInt(z);
 },
});

Nested group and multiple peer support using variant modifiers

Sometimes you can run into problems when you have multiple group chunks nested within each other because Tailwind has no real way to disambiguate between them.

To solve this, we're adding support for variant modifiers, which are a new dynamic chunk that you can add to the end of a variant (inspired by our optional opacity modifier syntax) that you can use to give each group/peer your own identifier.

Here's what it looks like:

<div class="group/sidebar ...">
 <!-- ... -->
 <div class="group/navitem ...">
 <a
 href="#"
 class="opacity-50 group-hover/sidebar:opacity-75 group-hover/navitem:bg-black/75"
 >
 <!-- ... -->
 </a>
 </div>
 <!-- ... -->
</div>

This lets you give each group a clear name that makes sense for that context on the fly, and Tailwind will generate the necessary CSS to make it work.

I'm really excited to have a solution out there for this because it's something I've been trying to land on a good approach for solving for several years, and this is the first thing we've come up with that really feels like it offers the power and flexibility I think it should.


Changes since the last pre-release

Since the last pre-release, we've added three new features:

The only change to any existing new features is that we've dropped the angle-brackets <label> syntax for what we're now calling variant modifiers:

- <div class="group<sidebar> ...">
+ <div class="group/sidebar ...">
 <!-- ... -->
- <div class="group<navitem> ...">
+ <div class="group/navitem ...">
 <a
 href="#"
- class="opacity-50 group<sidebar>-hover:opacity-75 group<navitem>-hover:bg-black/75"
+ class="opacity-50 group-hover/sidebar:opacity-75 group-hover/navitem:bg-black/75"
 >
 <!-- ... -->
 </a>
 </div>
 <!-- ... -->
 </div>

I'm excited about this change because the original implementation added this whole new "labels" concept which felt risky to me. The new implementation leans on the existing "modifier" syntax we already use for opacity modifiers, but brings it to variants.

So now we have this consistent "shape" to the components of a class name, and the idea of a "modifier" is a lot more generalized and useful in lots of situations, whereas "label" was very specific to this group/peer problem. We're going to expose the modifier in the plugin API for both variants and utilities, so your own plugins can use the modifier for whatever you want — it basically just becomes a second parameter like opacity is for a color utility.


So that's everything! Please test the hell out of it and let us know what you think before it's too late 😄

You must be logged in to vote

Replies: 16 comments 19 replies

Comment options

Love the Max-width and dynamic breakpoints changes

You must be logged in to vote
0 replies
Comment options

Super excited about the @config feature. We have an application that runs multiple sites, and allows each site to pick its theme. Up until now, they've all had to share the same config, and just utilize them differently in their views, but this should give us the ability to customize even more!

You must be logged in to vote
0 replies
Comment options

What is the generated CSS for md:max-xl:sr-only?

You must be logged in to vote
2 replies
Comment options

It would generate exactly what you asked for:

@media (min-width: 768px) {
 @media not all and (min-width: 1280px) {
 .md\:max-xl\:sr-only {
 position: absolute;
 width: 1px;
 height: 1px;
 padding: 0;
 margin: -1px;
 overflow: hidden;
 clip: rect(0, 0, 0, 0);
 white-space: nowrap;
 border-width: 0;
 }
 }
}

So this now applies sr-only between the 768px and 1280px range.

Comment options

Thanks! I was asking because I wasn't sure if I should expect nested media queries or a single combined one because I'm old school and hadn't caught up with the browser support, but it's all good nowadays.

Comment options

Really excited for @config and for nested groups. Felt the need for both last week 🙌🙌

You must be logged in to vote
0 replies
Comment options

This all looks incredible! Great work on this. Y'all killed it on this release 🙌🏼

You must be logged in to vote
0 replies
Comment options

Would it be too late in the game to include the existing not- PR in this release?

I just checked up on it, and it's looking pretty refined: #7976

I commonly find myself reaching for negations more verbose and using something like this: [&:not(:hover)]:text-blue-500. I'd love to use not-hover:text-blue-500, which follows the existing pattern seen in other selectors already in Tailwind, as seen in the example above for not-sr-only (though a variant).

This PR feels especially fitting for a change like that, categorically.

You must be logged in to vote
2 replies
Comment options

adamwathan Oct 17, 2022
Maintainer Author

Won’t make it into this release I’m afraid but hoping to dig into it for Nov/Dec. I think we need a more generic/composable solution than what we have so far, which is what I’m hoping we can explore in the next few months.

Comment options

That sounds ideal. I'm looking forward to it! Thanks 🙌🏼

Comment options

This is sooo good. Hyped for the nested group/peer modifiers. 🔥🔥🔥

You must be logged in to vote
0 replies
Comment options

For the matchVariant plugin API, is it possible to include a value for no parameter? Or would I use the addVariant API for that one and matchVariant for parameters?

EDIT: You can simply add a default value for the value parameter

let plugin = require("tailwindcss/plugin");
module.exports = {
 // ...
 plugins: [
 plugin(function ({ matchVariant }) {
 matchVariant(
 "placement",
 (value = '') => {
 return `&[data-placement=${value}]`;
 },
 {
 values: {
 t: "top",
 r: "right",
 b: "bottom",
 l: "left",
 },
 }
 );
 }),
 ],
};
You must be logged in to vote
1 reply
Comment options

This is not going to work as in your example. What you want to do is this instead:

matchVariant('foo', (value) => '.foo-${value} &', {
 values: {
 DEFAULT: 'bar'
 }
})

This is the same API as matchUtilities 👍

Comment options

Love this, it looks dope! <3

You must be logged in to vote
0 replies
Comment options

So great to see ARIA variants being added to Tailwind, awesome release!

You must be logged in to vote
0 replies
Comment options

I see good opportunity to implement nth-child variant (not only odd and even Tailwind has but any) in a core with matchVariant

You must be logged in to vote
0 replies
Comment options

Is there a way to make a custom variant work with nested groups? For example, I often make a custom hocus: variant that works with group- and peer- like this:

addVariant('hocus', [
 '@media (hover: hover) and (pointer: fine) { &:hover }',
 '&:focus',
]);
addVariant('group-hocus', [
 '@media (hover: hover) and (pointer: fine) { :merge(.group):hover & }',
 ':merge(.group):focus &',
]);
addVariant('peer-hocus', [
 '@media (hover: hover) and (pointer: fine) { :merge(.peer):hover ~ & }',
 ':merge(.peer):focus ~ &',
]);

I'd like to be able to use group-hocus/sidebar:, say.

You must be logged in to vote
10 replies
Comment options

@MichaelAllenWarner any idea if this is supported yet? It'd be great to have.

Comment options

No, I don't think so. I've been doing this sort of thing:

// hocus
addVariant('hocus', [
 '&:hover',
 '&:focus',
]);
// group-hocus / group-hocus/{name}
matchVariant(
 'group',
 (_value, { modifier }) =>
 modifier
 ? [
 `:merge(.group\\/${modifier}):hover &`,
 `:merge(.group\\/${modifier}):focus &`,
 ]
 : [
 `:merge(.group):hover &`,
 `:merge(.group):focus &`,
 ],
 { values: { hocus: 'hocus' } }
);
// peer-hocus / peer-hocus/{name}
matchVariant(
 'peer',
 (_value, { modifier }) =>
 modifier
 ? [
 `:merge(.peer\\/${modifier}):hover ~ &`,
 `:merge(.peer\\/${modifier}):focus ~ &`,
 ]
 : [
 `:merge(.peer):hover ~ &`,
 `:merge(.peer):focus ~ &`,
 ],
 { values: { hocus: 'hocus' } }
);
// ...
// make this the last custom plugin (see explanation below)
const defaultGroupPeerVariants = {
 group: (_, { modifier }) =>
 modifier
 ? [`:merge(.group\\/${escapeClassName(modifier)})`, ' &']
 : [`:merge(.group)`, ' &'],
 peer: (_, { modifier }) =>
 modifier
 ? [`:merge(.peer\\/${escapeClassName(modifier)})`, ' ~ &']
 : [`:merge(.peer)`, ' ~ &'],
};
for (const [name, fn] of Object.entries(defaultGroupPeerVariants)) {
 matchVariant(name, (value = '', extra) => {
 let result = normalize(
 typeof value === 'function' ? value(extra) : value
 );
 if (!result.includes('&')) result = '&' + result;
 const [a, b] = fn('', extra);
 let start = null;
 let end = null;
 let quotes = 0;
 for (let i = 0; i < result.length; ++i) {
 const c = result[i];
 if (c === '&') {
 start = i;
 } else if (c === "'" || c === '"') {
 quotes += 1;
 } else if (start !== null && c === ' ' && !quotes) {
 end = i;
 }
 }
 if (start !== null && end === null) {
 end = result.length;
 }
 // Basically this but can handle quotes:
 // result.replace(/&(\S+)?/g, (_, pseudo = '') => a + pseudo + b)
 return (
 result.slice(0, start) +
 a +
 result.slice(start + 1, end) +
 b +
 result.slice(end)
 );
 });
}

The big chunk of code at the end is copied from core Tailwind. It's needed because our custom group/peer matchVariant() stuff overrides the core group/peer stuff, which matters for arbitrary group/peer variants (like group-[&.foo]:). So to get Tailwind to process arbitrary group/peer variants correctly, we have to override our custom stuff with core stuff, if that makes sense. It's ugly but it works.

Comment options

Oh, and the normalize() and escapeClassName() dependencies can be imported into the TW config like this:

const { default: escapeClassName } = require('tailwindcss/lib/util/escapeClassName');
const { normalize } = require('tailwindcss/lib/util/dataTypes');

To rephrase my answer to your question: no, as far as I know it isn't officially supported, but yes, it's possible (though awkward).

Comment options

@MichaelAllenWarner Nice! That's pretty clever, importing escapeClassName and normalize from TailwindCSS like that. I might have to adopt that technique for some of my own plugins.

Specifically, I think that'll be especially useful if I get around to building my peer-group variant idea as a plugin.

Currently, there exists no native way to do anything like uncle ~ parent uncle:checked { ... }, which is what peer-group strives to solve. I run into that issue at least once or twice a month and have to resort to using arbitrary variants.

Comment options

@brandonmcconnell

There's a much simpler solution to this that @thecrypticace explains here. You can just do:

matchVariant(
 'group-hocus',
 (_value, { modifier }) => { /* ... */ },
 { values: { DEFAULT: '' } }
);

The magic sauce is the DEFAULT key, which I didn't know about.

Comment options

With named groups and peers now supported, I'd love to see a couple features added—

  • naming collisions between peers and groups. Personally, I think peers and groups should share the same namespace, so having an ancestor group and a previous peer of the same name would look at both as the same object, not two separate entities.

    While this may sound like a limitation, it would actually allow Tailwind to integrate the two, which leads me to my next suggestion, re proposal Proposal for `peer-group` #9611 :

  • I'd really like to see added support for peer-group which would allow us to create groups that can act on a peer's state:

    WITHOUT peer-group (example):

     ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
    ◯━━┳━ peer ╭┄┄┄┴┄┄┄┄╮ ╰┄┤ requires introducing a non-utility ┆
     ┗━ peer-checked:[&_.hideable]:collapse ╭┄┤ class to identify the element using ┆
     ┗━ hideable ┆ ┆ a peer with an arbitrary variant ┆
     ╰┄┄┄┬┄┄┄┄╯ ┆ ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
     ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
    

    WITH peer-group (example of code, preview not supported):

    ◯━━┳━ peer/toggle
     ┗━ peer-group/toggle
     ┗━ peer-group-checked/toggle:collapse
    

    I explain all the rationale for this in the full proposal (Proposal for `peer-group` #9611 ).

You must be logged in to vote
0 replies
Comment options

Has anyone found that height, width and background has stopped functioning properly with this update?

You must be logged in to vote
1 reply
Comment options

@Jae-Kae Are you still experiencing that? You should open a bug report so the team can address it.

Comment options

Hi there, does the new "max" breakpoints work with the @screen directive?

This was our previous configuration:

screens: {
 xs: '420px',
 sm: '576px',
 md: '829px',
 lg: '992px',
 xl: '1200px',
 '-xl': { max: '1199px' },
 '-lg': { max: '991px' },
 '-md': { max: '828px' },
 '-sm': { max: '575px' },
 '-xs': { max: '419px' },
}

I'd like to replace it with the new max- breakpoints:

screens: {
 xs: '420px',
 sm: '576px',
 md: '829px',
 lg: '992px',
 xl: '1200px',
}

But it seems like @screen directive is not following the new convention because I get this error:

The 'max-sm' screen does not exist in your theme:
@screen max-sm {
 @apply ...

Did I miss something? Is it unsupported on purpose?

You must be logged in to vote
3 replies
Comment options

@nicooprat Is it possible that you have a plugin installed that was building your previous max breakpoints, which is now preventing the new ones from being automatically generated?

Comment options

Nothing I can think of...

I did a very simple reproduction in Tailwind Playground: https://play.tailwindcss.com/f7dREBB6Fn (in the HTML, max-md:hidden works though)

image

Comment options

Hmm yeah I'm not quite sure. I'm curious of @tailwindlabs' take on this, though this might be a better discussion for the official Tailwind CSS discord server: https://tailwindcss.com/discord

Comment options

  • to the comments above.

For some reason, after upgrading to 3.2.4, dynamic breakpoints are not working for me anymore.
Things like max-md:hidden or max-lg:p-4

You must be logged in to vote
0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

AltStyle によって変換されたページ (->オリジナル) /