Tailwind CSS v3.4: Dynamic viewport units, :has() support, balanced headlines, subgrid, and more

Date
Tailwind CSS v3.3

There’s nothing like building a major new product for finding all the features you wish you had in your own tools, so we capitalized on some of that inspiration and turned it into this — Tailwind CSS v3.4.

As always the improvements range from things you’ve been angry about for years, to supporting CSS features you’ve never even heard of and probably can’t even use at work.

All the good stuff is in that list, but check out the release notes for a couple more details that weren’t exciting enough to earn a spot in this post.

Upgrade your projects by installing the latest version of tailwindcss from npm:

$ npm install tailwindcss@latest

Or try out all of the new features on Tailwind Play, right in your browser.


Dynamic viewport units

When the vh unit was added to browsers we all got so excited — finally a way to build full-height application layouts and stuff without drilling height: 100% through 17 layers of DOM! But mobile devices and their damn disappearing menu bars spoiled all the fun, effectively making the vh unit just a cruel reminder of a future that could’ve been so great.

Well we’ve got a new future now — dvh, lvh, and svh are designed to accommodate that disappearing browser chrome and Tailwind CSS v3.4 supports them out of the box:

Scroll up and down in the viewport to hide/show the browser UI

tailwindcss.com

h-dvh

<div class="h-dvh">
  <!-- ... -->
</div>

We’ve added the following new classes by default:

ClassCSS
h-svhheight: 100svh
h-lvhheight: 100lvh
h-dvhheight: 100dvh
min-h-svhmin-height: 100svh
min-h-lvhmin-height: 100lvh
min-h-dvhmin-height: 100dvh
max-h-svhmax-height: 100svh
max-h-lvhmax-height: 100lvh
max-h-dvhmax-height: 100dvh

If you need other values, you can always use arbitrary values too like min-h-[75dvh].

Browser support is pretty great for these nowadays, so unless you need to support Safari 14 you can start using these right away.


New :has() variant

The :has() pseudo-class is the most powerful thing that’s been added to CSS since flexbox. For the first time ever, you can style an element based on its children, not just based on its parents. It even makes it possible to style based on subsequent siblings.

Here’s an example where the parent gets a colored ring if the radio button inside of it is checked:

Payment method
<label class="has-[:checked]:ring-indigo-500 has-[:checked]:text-indigo-900 has-[:checked]:bg-indigo-50 ..">
  <svg fill="currentColor">
    <!-- ... -->
  </svg>
  Google Pay
  <input type="radio" class="accent-indigo-500 ..." />
</label>

I feel like I’ve found a new use-case for :has() every week while working on this new UI kit we’ve been building for the last few months, and it’s replaced a crazy amount of JavaScript in our code.

For example, our text inputs are pretty complicated design-wise and require a little wrapper element to build. Without :has(), we had no way of styling the wrapper based on things like the :disabled state of the input, but now we can:

input.jsx
export function Input({ ... }) {
  return (
    <span className="has-[:disabled]:opacity-50 ...">
      <input ... />
    </span>
  )
}

This one is pretty bleeding edge but as of literally today it’s now supported in the latest version of all major browsers. Give it a few weeks for any Firefox users to install today’s update and we should be able to go wild with it.


Style children with the * variant

Here’s one people have wanted for literally ever — a way to style children from the parent using utility classes.

We’ve added a new * variant that targets direct children, letting you do stuff like this:

Categories

Sales
Marketing
SEO
Analytics
Design
Strategy
Security
Growth
Mobile
UX/UI
<div>
  <h2>Categories:<h2>
  <ul class="*:rounded-full *:border *:border-sky-100 *:bg-sky-50 *:px-2 *:py-0.5 dark:text-sky-300 dark:*:border-sky-500/15 dark:*:bg-sky-500/10 ...">
    <li>Sales</li>
    <li>Marketing</li>
    <li>SEO</li>
    <!-- ... -->
  </ul>
</div>

Generally I’d recommend just styling the children directly, but this can be useful when you don’t control those elements or need to make a conditional tweak because of the context the element is used in.

It can be composed with other variants too, for instance hover:*:underline will style any child when the child is hovered.

Here’s a cool way we’re using that to conditionally add layout styles to different child elements in the new UI kit we’re working on:

JSX
function Field({ children }) {
  return (
    <div className="data-[slot=description]:*:mt-4 ...">
      {children}
    </div>
  )
}

function Description({ children }) {
  return (
    <p data-slot="description" ...>{children}</p>
  )
}

function Example() {
  return (
    <Field>
      <Label>First name</Label>
      <Input />
      <Description>Please tell me you know your own name.</Description>
    </Field>
  )
}

See that crazy data-[slot=description]:*:mt-4 class? It first targets all direct children (that’s the *: part), then filters them down to just items with a data-slot="description" attribute using data-[slot=description].

This makes it easy to target only specific children, without having to drop all the way down to a raw arbitrary variant.

Looking forward to seeing all the horrible stuff everyone does to make me regret adding this feature.


New size-* utilities

You’re sick of typing h-5 w-5 every time you need to size an avatar, you know it and I know it.

In Tailwind CSS v3.4 we’ve finally added a new size-* utility that sets width and height at the same time:

HTML
<div>
  <img class="h-10 w-10" ...>
  <img class="h-12 w-12" ...>
  <img class="h-14 w-14" ...>
  <img class="size-10" ...>
  <img class="size-12" ...>
  <img class="size-14" ...>
</div>

We’ve wanted to add this forever but have always been hung up on the exact name — size-* felt like so much to type compared to w-* or h-* and s-* felt way too cryptic.

After using it for a few weeks though I can say decisively that even with the longer name, it’s way better than separate width and height utilities. Super convenient, especially if you’re combining it with variants or using a complex arbitrary value.


Balanced headlines with text-wrap utilities

How much time have you spent fiddling with max-width or inserting responsive line breaks to try and make those little section headings wrap nicely on your landing pages? Well now you can spend zero time on it, because the browser can do it for you with text-wrap: balance:

Beloved Manhattan soup stand closes

New Yorkers are facing the winter chill with less warmth this year as the city's most revered soup stand unexpectedly shutters, following a series of events that have left the community puzzled.

<article>
  <h3 class="text-balance ...">Beloved Manhattan soup stand closes<h3>
  <p>New Yorkers are facing the winter chill...</p>
</article>

We’ve also added text-pretty which tries to avoid orphaned words at the end of paragraphs using text-wrap: pretty:

Beloved Manhattan soup stand closes

New Yorkers are facing the winter chill with less warmth this year as the city's most revered soup stand unexpectedly shutters, following a series of events that have left the community puzzled.

<article class="text-pretty ...">
  <h3>Beloved Manhattan soup stand closes<h3>
  <p>New Yorkers are facing the winter chill...</p>
</article>

The nice thing about these features is that even if someone visits your site with an older browser, they’ll just fallback to the regular wrapping behavior so it’s totally safe to start using these today.


Subgrid support

Subgrid is a fairly recent CSS feature that lets an element sort of inherit the grid columns or rows from its parent, make it possible to place its child elements in the parent grid.

HTML
<div class="grid grid-cols-4 gap-4">
  <!-- ... -->
  <div class="grid grid-cols-subgrid gap-4 col-span-3">
      <div class="col-start-2">06</div>
  </div>
  <!-- ... -->
</div>

We’re using subgrid in the new UI kit we’re working on for example in dropdown menus, so that if any item has an icon, all of the other items are indented to keep the text aligned:

HTML
<div role="menu" class="grid grid-cols-[auto_1fr]">
  <a href="#" class="grid-cols-subgrid col-span-2">
    <svg class="mr-2">...</svg>
    <span class="col-start-2">Account</span>
  </a>
  <a href="#" class="grid-cols-subgrid col-span-2">
    <svg class="mr-2">...</svg>
    <span class="col-start-2">Settings</span>
  </a>
  <a href="#" class="grid-cols-subgrid col-span-2">
    <span class="col-start-2">Sign out</span>
  </a>
</div>

When none of the items have an icon, the first column shrinks to 0px and the text is aligned all the way to left.

Check out the MDN documentation on subgrid for a full primer — it’s a bit of a tricky feature to wrap your head around at first, but once it clicks it’s a game-changer.


Extended min-width, max-width, and min-height scales

We’ve finally extended the min-width, max-width, and min-height scales to include the full spacing scale, so classes like min-w-12 are actually a real thing now:

HTML
<div class="min-w-12">
  <!-- ... -->
</div>

We should’ve just done this for v3.0 but never really got around to it — I’m sorry and you’re welcome.


Extended opacity scale

We’ve also extended the opacity scale to include every step of 5 out of the box:

HTML
<div class="opacity-35">
  <!-- ... -->
</div>

Hopefully that means a few less arbitrary values in your markup. I’m coming for you next 2.5%.


Extended grid-rows-* scale

We’ve also bumped the baked-in number of grid rows from 6 to 12 because why not:

HTML
<div class="grid grid-rows-9">
  <!-- ... -->
</div>

Maybe we’ll get even crazier and bump it to 16 in the next release.


New forced-colors variant

Ever heard of forced colors mode? Your site probably looks pretty bad in it.

Well now you can’t blame us at least, because Tailwind CSS v3.4 includes a forced-colors variant for adjusting styles for forced colors mode:

HTML
<form>
  <input type="checkbox" class="appearance-none forced-colors:appearance-auto ...">
</form>

It’s really useful for fine-tuning totally custom controls, especially combined with arbitrary values and a working knowledge of CSS system colors.


New forced-color-adjust utilities

We’ve also added new forced-color-adjust-auto and forces-color-adjust-none utilities to control how forced colors mode affects your design:

HTML
<fieldset>
  <legend>Choose a color</legend>
  <div class="forced-color-adjust-none ...">
    <label>
      <input class="sr-only" type="radio" name="color-choice" value="white" />
      <span class="sr-only">White</span>
      <span class="size-6 rounded-full bg-white"></span>
    </label>
    <label>
      <input class="sr-only" type="radio" name="color-choice" value="gray" />
      <span class="sr-only">Gray</span>
      <span class="size-6 rounded-full bg-gray-300"></span>
    </label>
    <!-- ... -->
  </div>
</fieldset>

These should be used pretty sparingly, but they can be useful when it’s critical that something is rendered in a specific color no matter what, like choosing the color of something someone is buying in an online store.

To learn more about all this forced colors stuff, I recommend reading “Forced colors explained: A practical guide” on the Polypane blog — by far the most useful post I’ve found on this topic.


If you’ve been paying close attention, you might be wondering about Oxide, the engine improvements we previewed at Tailwind Connect this summer.

We’d originally slated those improvements for v3.4, but we have a few things still to iron out and so many of these other improvements had been piling up that we felt it made sense to get it all out the door instead of holding it back. The Oxide stuff is still coming, and will be the headlining improvement for the next Tailwind CSS release in the new year.

In the mean time, dig in to Tailwind CSS v3.4 by updating to the latest version with npm:

$ npm install tailwindcss@latest

With :has() and the new * variant, your HTML is about to get more out of control than ever.