Originally I had planned on a February release for Tailwind CSS v1.0, but I didn't anticipate just how long it would take to simply make decisions about things like the new config file format, renaming some existing classes, or the naming scheme for the new color palette.
It turns out actually writing the code is the easy part, deciding what code to write is the challenge.
I think the bulk of the paralyzing decisions are behind me at this point though, and I'm hoping to get 1.0.0-alpha.0
out today (!) so people can start trying it out while I make a few final adjustments and crank out the documentation.
The last two weeks have been pretty productive and I banged out a ton of stuff for v1.0, here are most of the highlights...
In 0.x, all of the Flexbox-related features in Tailwind are lumped together into a single "flexbox" module/plugin.
In 1.0, every Flexbox feature will get its own plugin (#689), for two reasons:
align-items
, justify-content
, etc.), so categorizing those properties under flexbox
will feel incorrect when we add grid support.flex
, flex-grow
, and flex-shrink
), so it would be nice if those properties had their own key in the theme
configuration.This means that instead of customizing the variants for all Flexbox utilities at once using a single flexbox
key in the variants
section of your config, you can now control each one individually using the separate plugin names: flexDirection
, flexWrap
, alignItems
, alignSelf
, justifyContent
, alignContent
, flex
, flexGrow
, and flexShrink
.
I also added new theme
sections for flexGrow
, flexShrink
, and flex
, so you can customize those values if needed, instead of being limited to the default values (#690, #700).
That means if you've ever needed flex-grow-2
, you can easily add it through your config instead of creating a custom utility:
module.exports = {
extend: {
theme: {
flexGrow: {
2: 2,
}
}
}
}
To make the class names more predictable after adding customization support, I had to rename flex-no-grow
to flex-grow-0
and flex-no-shrink
to flex-shrink-0
(#687) which is unfortunately a breaking change, but I think ultimately for the best.
The default shadows we include in 0.x are okay, but since the approach for v1.0 is to encourage people to rely on the defaults and only customize when necessary, it's important that we make the v1.0 shadows kick ass.
Steve and I learned a lot about crafting really great shadows when writing Refactoring UI, so we spent a couple of days applying those ideas to create some new shadows for v1.0 (#691.
Here's where we ended up:
Check out a live demo on CodePen.
The new shadows incorporate a progressively larger negative spread value to better convey elevation, and I think they look super pro.
Back in January I wrote about how I was hoping to leverage some new built-in escape handling functionality in postcss-selector-parser
to simplify our own escape handling, but couldn't because of an annoying bug in css-loader
.
While the actual bug hasn't been resolved, the latest version of postcss-selector-parser
makes it possible for me to work around it, so I decided to give this another go.
Ultimately I was able to remove our dependency on css.escape
and defer all escape handling to postcss-selector-parser
(#694), which is nice because now all escaping happens through the same code path and nothing can get out of sync.
This also prompted me to revisit how we were dealing with escaping in plugins that add new variants to Tailwind. In 0.x, plugin authors didn't need to really worry about escaping when adding variants, but did when adding utilities.
This was only possible by us naively escaping segments of class names in isolation, which is sort of a bad idea because it could lead to unnecessary (though harmless) escape sequences.
So in v1.0, I've decided that plugin authors will need to escape class names themselves using the e
helper (#695), just like they do with utilities.
function({ addVariant, e }) {
addVariant('first-child', ({ modifySelectors, separator }) => {
modifySelectors(({ className }) => {
- return `.first-child${separator}${className}:first-child`
+ return `.${e(`first-child${separator}${className}`)}:first-child`
})
})
},
It's a tiny bit more work for plugin authors, but at least the plugin authoring experience is now consistent.
This is more of a philosophical change than a real code change, but in earlier drafts of v1.0, core plugins were configured by Tailwind from the outside. Internally, Tailwind would load up each core plugin, find a matching key in the theme
and variants
sections of the config, and pass those values directly to the plugin as arguments.
This was fine for core plugins, but the philosophy of "plugins should be configured directly" put third-party plugins at a disadvantage when it came to things like loading up all of your theme values as a JS module, or extending a base theme.
The reason is that if a third-party plugin is configured directly, like this:
module.exports = {
// ...
plugins: [
require('my-gradient-plugin')({
gradients: {
'blue-green': [colors['blue'], colors['green']],
'purple-blue': [colors['purple'], colors['blue']],
// ...
},
variants: ['responsive', 'hover'],
})
],
// ...
}
...there's no way to access the configuration values anywhere else, because they are inlined and effectively "swallowed up" by the plugin function.
I talked about this problem with Brad Cornes (the wizard behind the Tailwind CSS IntelliSense plugin) and he convinced me that a better approach would be for all plugins to reach into the theme and grab the values they need themselves, rather than being configured explicitly.
It sounds like a bad idea intuitively (why introduce weird indirection that relies on matching up two strings if you don't have to), but in practice it's a lot more practical.
Basically, the recommendation now is that plugins read their config from the theme
section, and plugin authors simply document what key(s) they are going to look for.
So the plugin above would instead be configured like this:
module.exports = {
// ...
theme: {
// ...
gradients: {
'blue-green': [colors['blue'], colors['green']],
'purple-blue': [colors['purple'], colors['blue']],
// ...
},
},
variants: {
// ...
gradients: ['responsive', 'hover'],
},
plugins: [
require('my-gradient-plugin'),
],
// ...
}
This way if you ever wanted to bundle up a customized theme that depended on a plugin, end users could still extend those values using the same mechanisms they are already used to.
All of Tailwind's core plugins have been updated to work this way internally ((#696)), and I'll be updating the documentation when I release v1.0 to reflect this new best practice, as previously I was encouraging the exact opposite.
This change doesn't actually affect any end-users of Tailwind, but will impact how plugin authors write their plugins if they want to follow our guidelines.
config()
to theme()
In Tailwind 0.x, the config()
function could be used in your actual CSS to reference values from the config, for example:
.shadow-red {
box-shadow: 0 0 3px config('colors.red');
}
This is useful as an escape hatch instead of @apply
when you need to reference something from your config as part of a property declaration rather than the whole declaration.
But since v1.0 moves all of the theme-related configuration into the theme
key, you'd have to write the above example like this:
.shadow-red {
box-shadow: 0 0 3px config('theme.colors.red');
}
I couldn't think of a single reason why someone would ever want to reference top-level options like prefix
in their CSS, so I decided to rename the function to theme()
and automatically scope it to the theme key (#697):
.shadow-red {
box-shadow: 0 0 3px theme('colors.red');
}
It feels like a much more expressive name anyways.
When I introduced the shared spacing
key under theme
, I updated width
and height
to use that scale as well so everything would be easy to update in one place, but width
and height
had some additional larger values (40
, 48
, and 64
) that were not part of the spacing scale and needed to be included separately.
To unify the entire scale, I decided to add those missing values to spacing
so they would be available to padding and margin as well (#699).
I also added 56
to the scale because it felt like a bit of a hole that might be useful for sizing larger components like dropdowns (#698).
In 0.x our default maxWidth
scale was sort of an afterthought — we just started at 20rem and bumped it up by 10rem at a time until it felt like there were a decent number of options.
In hindsight we should have agonized over this a little more, because the max-width utilities are some of the most important layout tools in the entire framework; they are crucial for sizing larger blocks in situations where it doesn't make sense to follow a grid, like a large centered card for example.
The big mistake in the original scale was increasing the size linearly. Just like a type scale or spacing scale, it makes a lot more sense for the lower values to be clustered closer together, and for the gap to progressively increase as you get to larger sizes.
After a bunch of experimentation, Steve and I landed on a set of values we think are pretty solid (#701). The scale goes from 20rem to 72rem, with the values at the low end being only 4rem apart, and the values at the high end being 8rem apart.
Check out a demo of the new scale on CodePen.
Something that bites me personally all the time is accidentally relying on user-agent styles that don't match values in my config file. For example, creating an h1
and not assigning an explicit font size, or creating an input
and not assigning it a text color.
So something I'm trying to improve in v1.0 is making it as hard as possible to accidentally use styles that deviate from your system.
The first step was to reset heading styles by default, so all headings default to the base font size (#703). This means that by default all headings are the same size. This might sound unhelpful at first, but I think it's much better for a heading to be obviously the wrong size and trigger you to assign it a size than it is for it to look like almost the right size, but actually not be a size in your type scale.
A lot of the time, text that is semantically a heading shouldn't actually be large and emphasized anyways.
This week I'm going to looking for more opportunities to make Preflight more helpful in this space, starting with resetting font properties on buttons and inputs (#741).
Steve has spent the last two weeks working on the new color palette for v1.0, and I think we're getting pretty close.
We decided to jump from 7 shades per color to 9, and are using a numeric scale borrowed from Material Design, where 100
is the lightest shade, and 900
is the darkest.
We're still in the process of fine-tuning things, but I've opened a PR (#737) that at least introduces the new naming scheme and previews where we are at with the palette:
I'll likely ship 1.0.0-alpha.0 with the draft palette, and Steve and I will continue to refine it and test it in real UIs before we tag the final 1.0.
We're also planning to create a bunch of additional colors that are equally balances and harmonious but aren't enabled by default, so you can easily pull those in if you need them for your project. One of the most common questions we get is "how do a generate a set of shades for my own custom base color?" so we're hoping to help solve that problem by eventually having a library of dozens of base colors to choose from that all have 9 pre-selected shades.
You'll be able to pull them into your project by doing something like this:
const colors = require('@tailwindcss/colors')
module.exports = {
theme: {
colors: {
gray: colors.coolGray,
blue: colors.lightBlue,
red: colors.crimson,
green: colors.forestGreen
}
}
}
This will be possible thanks to another feature I recently added (#707), where you can now specify colors using an object syntax like this:
const colors = require('@tailwindcss/colors')
module.exports = {
theme: {
colors: {
gray: {
100: '#eee',
300: '#ccc',
500: '#999',
700: '#555',
900: '#222',
}
}
}
}