src/blog/tailwindcss-v4-1/index.mdx
import { adamwathan, danhollick } from "@/app/blog/authors"; import card from "./card.jpg"; import SafariDark from "./safari-15-dark.png"; import SafariLight from "./safari-15-light.png"; import KeyboardLight from "./keyboard-light.png"; import KeyboardDark from "./keyboard-dark.png"; import { Figure } from "@/components/figure"; import { Example } from "@/components/example"; import { Stripes } from "@/components/stripes"; import { CodeExampleStack } from "@/components/code-example"; import { Image, YouTubeVideo } from "@/components/media";
export const meta = { title: "Tailwind CSS v4.1: Text shadows, masks, and tons more", description: "I wasn't sure it would ever happen but we did it — we released a version of Tailwind CSS that includes text shadow utilities. Tailwind CSS v4.1 is here and it's packed with improvements that will help you (or your LLM, you coward) build even better interactive experiences.", date: "2025-04-03T20:00:00.000Z", authors: [adamwathan, danhollick], image: card, excerpt: ( <> I wasn't sure it would ever happen but we did it — we released a version of Tailwind CSS that includes text shadow utilities. Tailwind CSS v4.1 is here and it's packed with improvements that will help you (or your LLM, you coward) build even better interactive experiences. </> ), };
<YouTubeVideo id="HTFHoA12MJk" />I wasn't sure it would ever happen but we did it — we released a version of Tailwind CSS that includes text-shadow utilities.
Tailwind CSS v4.1 is here and it's packed with new utilities, variants, and developer experience improvements that will help you (or your LLM, you coward) build even better interactive experiences.
Here's all the best stuff we got into this release:
text-shadow-* utilities — only about twenty years after they were first supported by a browser.mask-* utilities — use images and gradients to mask elements with new ergonomic APIs.overflow-wrap — defend the integrity of your layouts from even the longest German words your users will throw at you.drop-shadow support — can't really remember why we didn't have these before but we do now.pointer-* and any-pointer-* — tweak your design for touch devices explicitly instead of relying on viewport size.items-baseline-last and self-baseline-last utilities.safe alignment — center content in flex and grid layouts without it disappearing when there's not enough space.@source not — explicitly ignore irrelevant large directories and speed up your builds even more.@source inline(…) — force Tailwind to include classes that aren't in your source files.noscript, user-valid, inverted-colors, and more.That's all the cool stuff, but there's a few other little things hiding in the release notes that you might want to check out too.
Upgrade your projects by installing the latest version of tailwindcss from npm:
# [!code filename:Using the Tailwind CLI]
npm install tailwindcss@latest @tailwindcss/cli@latest
# [!code filename:Using Vite]
npm install tailwindcss@latest @tailwindcss/vite@latest
# [!code filename:Using PostCSS]
npm install tailwindcss@latest @tailwindcss/postcss@latest
text-shadow-* utilitiesWe've been threatening to add text shadows for at least the last six years and today they are finally here.
We've added five text shadows to the default theme, from text-shadow-2xs to text-shadow-lg. They are particularly useful for making headings stand out against a busy background:
<!-- [!code classes:text-shadow-2xs,text-shadow-xs,text-shadow-sm,text-shadow-md,text-shadow-lg,text-shadow-xl] -->
<p class="text-shadow-2xs ...">The quick brown fox...</p>
<p class="text-shadow-xs ...">The quick brown fox...</p>
<p class="text-shadow-sm ...">The quick brown fox...</p>
<p class="text-shadow-md ...">The quick brown fox...</p>
<p class="text-shadow-lg ...">The quick brown fox...</p>
You can change the color of the shadow using the text-shadow-<color> utilities. For instance, you can create a sort of embossed effect by using a small white shadow on dark text:
<!-- [!code classes:text-shadow-sky-300] -->
<button class="text-sky-950 text-shadow-2xs text-shadow-sky-300 ...">Book a demo</button>
<button class="text-gray-950 dark:text-white dark:text-shadow-2xs ...">See pricing</button>
If you just want to adjust the opacity of a text shadow without changing the color, you can slap an opacity modifier directly on text shadow size utilities like text-shadow-lg.
For example, text-shadow-lg/50 is the same as setting text-shadow-lg and text-shadow-black/50 at the same time:
<!-- [!code classes:text-shadow-lg/20,text-shadow-lg/30] -->
<p class="text-shadow-lg ...">The quick brown fox...</p>
<p class="text-shadow-lg/20 ...">The quick brown fox...</p>
<p class="text-shadow-lg/30 ...">The quick brown fox...</p>
Check out the text-shadow docs for more details.
mask-* utilitiesOne of the coolest features of modern CSS is the ability to use images and gradients as masks - basically using the opacity of an image to hide certain parts of an element:
<Figure> <Example padding={false}> { <div className="mx-auto flex items-center p-16 max-sm:p-6"> <div className="font-medium max-sm:-mx-3">
<p className="font-mono text-xs text-blue-500 uppercase dark:text-blue-400">Speed</p>
<p className="mt-2 text-base whitespace-nowrap text-gray-700 dark:text-gray-300">Built for power users</p>
<p className="mt-1 text-sm leading-relaxed text-balance text-gray-500">
Work faster than ever with our keyboard shortcuts
</p>
</div>
</div>
} </Example>
<!-- [!code classes:mask-radial-from-transparent,mask-radial-from-15%,mask-radial-to-black,mask-radial-to-55%,mask-radial-at-right] -->
<div class="mx-auto flex items-center p-16 max-sm:p-8">
<div class="font-medium">
<p class="font-mono text-xs text-blue-500 uppercase dark:text-blue-400">Speed</p>
<p class="mt-2 text-base text-gray-700 dark:text-gray-300">Built for power users</p>
<p class="mt-1 text-sm leading-relaxed text-balance text-gray-500">
Work faster than ever with our keyboard shortcuts
</p>
</div>
</div>
Because you can use any background-image as a mask, the logical thing to do was to copy the bg-* utilities so they share the same API. The problem with that approach is you often want to combine multiple masks together and the bg-* utilities are not composable.
So instead, we created a new set of utilities to work with mask-image that are composable and purpose-built for the masking use case. For example, you can use utilities like mask-b-from-<value> and mask-t-to-<value> to add a linear gradient mask to a single side of an element:
It's more natural to think about which side you want to mask, rather than trying to work out the exact gradient you need to use.
The gradient mask utilities are also composable, so you can combine radial, conic and linear gradients together to create more complex masks:
<Figure> <Example> { <div className="grid grid-cols-1 items-end gap-x-2 gap-y-8 p-8 text-center font-mono text-xs font-medium text-gray-500 max-sm:justify-between max-sm:px-2 sm:grid-cols-2 dark:text-gray-400"> <div className="flex flex-col items-center"> <div className="grid aspect-3/2 w-48 grid-cols-1"> <Stripes className="col-start-1 row-start-1 rounded-lg" border /> <div className="col-start-1 row-start-1 rounded-lg bg-[url(https://images.unsplash.com/photo-1554629947-334ff61d85dc?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1000&h=1000&q=80)] mask-b-from-50% mask-radial-[50%_90%] mask-radial-from-80% mask-radial-at-bottom bg-cover bg-center mask-no-repeat"></div> </div> </div> <div className="flex flex-col items-center"> <div className="grid aspect-3/2 w-48 grid-cols-1"> <Stripes className="col-start-1 row-start-1 rounded-lg" border /> <div className="col-start-1 row-start-1 rounded-lg bg-[url(https://images.unsplash.com/photo-1554629947-334ff61d85dc?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1000&h=1000&q=80)] mask-r-from-80% mask-b-from-80% mask-radial-from-70% mask-radial-to-85% mask-circle mask-radial-at-top-left bg-cover bg-center mask-no-repeat"></div> </div> </div> </div> } </Example> ```html <!-- [!code classes:mask-b-from-50%,mask-radial-[50%_90%],mask-radial-from-80%,mask-r-from-80%,mask-b-from-80%,mask-radial-from-70%,mask-radial-to-85%] --> <div class="mask-b-from-50% mask-radial-[50%_90%] mask-radial-from-80% bg-[url(/img/mountains.jpg)] ..."></div> <div class="mask-r-from-80% mask-b-from-80% mask-radial-from-70% mask-radial-to-85% bg-[url(/img/mountains.jpg)] ..."></div> ``` </Figure>Masking is a super powerful technique and there's a lot more to the API than we can cover here. For a full breakdown of the new utilities, check out the documentation.
We went all-in on modern platform features with Tailwind CSS v4.0 to make the best framework we could, and give this version the longest shelf-life possible.
Unfortunately some of those features degrade really poorly in older browsers, to the point where even basic things like colors and shadows might not render at all for someone visiting from an old iPhone or iPad that's stuck on Safari 15.
For Tailwind CSS v4.1, we put a bunch of effort into coming up with and testing our own framework-specific fallbacks to make your sites render as best as possible in older browsers, even if some super modern things still don't behave quite the same.
<Image alt="Comparison between how Tailwind CSS v4.0 (left) and Tailwind CSS v4.1 (right) render in Safari 15.5. Tailwind CSS v4.0 fails to render some background gradients that are now visible in Tailwind CSS v4.1" src={SafariLight} className="dark:hidden" />
<Image alt="Comparison between how Tailwind CSS v4.0 (left) and Tailwind CSS v4.1 (right) render in Safari 15.5. Tailwind CSS v4.0 fails to render some background gradients that are now visible in Tailwind CSS v4.1" src={SafariDark} className="hidden dark:block" />
Here's a list of the things we've managed to improve in this release:
oklab now render in older versions of Safari@property (like shadows, transforms, gradients and more) now work in older versions of Safari and FirefoxTailwind CSS v4 is still designed for modern browsers like Safari 16.4 and up and still depends on a lot of modern features for everything to work perfectly, but at least now your sites will render and be usable in older browsers, even if in certain specific situations the odd shadow color be different.
To learn everything you need to know about browser compatibility in Tailwind CSS v4, you can read the full browser compatibility documentation.
overflow-wrapThe new overflow-wrap utilities let you control how text wraps within an element. The wrap-break-word utility is especially useful for long words or URLs that might otherwise break your layout:
<!-- [!code classes:wrap-break-word] -->
<p class="wrap-break-word">The longest word in any of the major...</p>
The one case where this doesn't quite behave like you'd expect is inside a flex container and that's where you probably want to use the new wrap-anywhere utility instead.
It's similar to wrap-break-word, but it allows mid-word line breaks when calculating the intrinsic size of the element, replacing the need to set min-width: 0 on the child element:
<div className="wrap-break-word">
<p className="text-sm font-medium text-gray-900 dark:text-white">Jay Riemenschneider</p>
<p className="text-sm text-gray-500 dark:text-gray-400">[email protected]</p>
</div>
</div>
<p className="mt-8 mb-3 text-center font-mono text-xs font-medium text-gray-500 dark:text-gray-400">
wrap-anywhere
</p>
<div className="mx-auto flex max-w-sm items-center gap-4 rounded-xl bg-white p-3 shadow-sm ring ring-black/2.5 dark:bg-black/10 dark:ring-white/10">
<div className="wrap-anywhere">
<p className="text-sm font-medium text-gray-900 dark:text-white">Jay Riemenschneider</p>
<p className="text-sm text-gray-500 dark:text-gray-400">[email protected]</p>
</div>
</div>
</div>
} </Example>
<!-- [!code classes:wrap-anywhere,wrap-break-word] -->
<div class="flex max-w-sm">
<div class="wrap-break-word">
<p class="font-medium">Jay Riemenschneider</p>
<p>[email protected]</p>
</div>
</div>
<div class="flex max-w-sm">
<div class="wrap-anywhere">
<p class="font-medium">Jay Riemenschneider</p>
<p>[email protected]</p>
</div>
</div>
There's not much more to it than that, but here's the overflow-wrap documentation if you want to read it again in slightly different words.
drop-shadow supportWhile we were building out text-shadow support we thought we might as well add another feature we never got around to implementing: colored drop shadows.
Now you can use utilities like drop-shadow-indigo-500 and drop-shadow-cyan-500/50 to change the color of a drop shadow:
<!-- [!code classes:drop-shadow-cyan-500/50,drop-shadow-blue-500/50,drop-shadow-indigo-500/50] -->
<svg class="fill-cyan-500 drop-shadow-xl drop-shadow-cyan-500/50 ...">...</svg>
<svg class="fill-blue-500 drop-shadow-xl drop-shadow-blue-500/50 ...">...</svg>
<svg class="fill-indigo-500 drop-shadow-xl drop-shadow-indigo-500/50 ...">...</svg>
There isn't much more to it but here's the drop-shadow documentation anyway.
pointer-* and any-pointer-*The new pointer-fine and pointer-coarse variants let you style something differently depending on whether the user is using a device with a mouse or using a touchscreen.
Use pointer-fine to target precise pointing devices like mouses and trackpads, and pointer-coarse to target devices lower precision like touchscreens:
<!-- [!code classes:pointer-coarse:mt-6,pointer-coarse:grid-cols-3,pointer-coarse:gap-4,pointer-coarse:p-4] -->
<fieldset aria-label="Choose a memory option">
<div class="flex items-center justify-between">
<div>RAM</div>
<a href="#"> See performance specs </a>
</div>
<div class="mt-4 grid grid-cols-6 gap-2 pointer-coarse:mt-6 pointer-coarse:grid-cols-3 pointer-coarse:gap-4">
<label class="p-2 pointer-coarse:p-4 ...">
<input type="radio" name="memory-option" value="4 GB" className="sr-only" />
<span>4 GB</span>
</label>
<!-- ... -->
</div>
</fieldset>
If you're on your phone, you'll see the pointer-coarse styles applied, which make the touch targets larger and easier to hit. If you're on a desktop, you'll see the pointer-fine styles applied, which make the touch targets smaller and more precise.
The any-pointer-* variants work the same way but instead of just checking the user's primary pointing device, they check if any pointing device matches. So any-pointer-coarse will match on a laptop with a touchscreen for a example, even if the user also has a mouse connected.
When working with flex or grid layouts, sometimes you need to align something to the baseline of the last line of text rather than the end of the container.
The new items-baseline-last utility does just that:
<div className="font-semibold text-gray-900 sm:col-start-2 dark:text-white">Spencer Sharp</div>
<p className="text-sm sm:col-start-2">Working on the future of astronaut recruitment at Space Recruit.</p>
</div>
<a
href="#"
className="font-mono text-xs font-medium text-gray-400 underline hover:text-blue-500 dark:text-gray-500"
>
spacerecruit.com
</a>
</div>
<div className="grid grid-cols-[1fr_auto] items-baseline-last gap-x-4 px-4 py-6">
<div className="grid grid-cols-[auto_1fr] gap-x-4 max-sm:grid-cols-1">
<div className="font-semibold text-gray-900 sm:col-start-2 dark:text-white">Alex Reed</div>
<p className="text-sm sm:col-start-2">A multidisciplinary designer.</p>
</div>
<a
href="#"
className="font-mono text-xs font-medium text-gray-400 underline hover:text-blue-500 dark:text-gray-500"
>
alex-reed.com
</a>
</div>
</div>
} </Example>
<!-- [!code classes:items-baseline-last] -->
<div class="grid grid-cols-[1fr_auto] items-baseline-last">
<div>
<h4>Spencer Sharp</h4>
<p>Working on the future of astronaut recruitment at Space Recruit.</p>
</div>
<p>spacerecruit.com</p>
</div>
We've also added self-baseline-last for when you need to align just a single item, and not all items in the flex or grid container.
safe alignmentEver had center aligned content overflow in both directions when the container got too small? Now you don't have to use a container query to switch the alignment at different sizes.
The new safe alignment utilities will change the alignment to start when the container starts to overflow, so it only overflows in one direction.
<!-- [!code filename:justify-center] -->
<!-- [!code classes:justify-center] -->
<ul class="flex justify-center gap-2 ...">
<li>Sales</li>
<li>Marketing</li>
<li>SEO</li>
<!-- ... -->
</ul>
<!-- [!code filename:justify-center-safe] -->
<!-- [!code classes:justify-center-safe] -->
<ul class="flex justify-center-safe gap-2 ...">
<li>Sales</li>
<li>Marketing</li>
<li>SEO</li>
<!-- ... -->
</ul>
These utilities work with both flexbox and grid layouts, and are available for all alignment properties.
@source notSometimes you need to specifically exclude some parts of your code base from being scanned by Tailwind. Now you can use @source not to ignore specific paths when scanning for class names:
/* [!code filename:CSS] */
@import "tailwindcss";
/* [!code highlight:2] */
@source not "./src/components/legacy";
This is useful when you have a large number of files in your project, but only want to scan a specific subset of them.
@source inline(…)If you need to make sure Tailwind generates certain class names that don’t exist in your content files, you can force them to be generated by using @source inline():
/* [!code filename:CSS] */
@import "tailwindcss";
/* [!code highlight:2] */
@source inline("underline");
/* [!code filename:Generated CSS] */
.underline {
text-decoration: underline;
}
This is the equivalent of the safelist configuration option in previous versions of Tailwind, but now you can use it in your CSS files instead of your config file.
The source input is brace-expanded, so you can generate multiple classes at once. For example, to generate all the red shades with hover variants, you can add a range to the source input:
<CodeExampleStack> ```css /* [!code filename:CSS] */ @import "tailwindcss"; /* [!code highlight:2] */ @source inline("{hover:,}bg-red-{50,{100..900..100},950}"); ```/* [!code filename:Generated CSS] */
.bg-red-50 {
background-color: var(--color-red-50);
}
.bg-red-100 {
background-color: var(--color-red-100);
}
.bg-red-200 {
background-color: var(--color-red-200);
}
/* ... */
.bg-red-800 {
background-color: var(--color-red-800);
}
.bg-red-900 {
background-color: var(--color-red-900);
}
.bg-red-950 {
background-color: var(--color-red-950);
}
@media (hover: hover) {
.hover\:bg-red-50:hover {
background-color: var(--color-red-50);
}
/* ... */
.hover\:bg-red-950:hover {
background-color: var(--color-red-950);
}
}
You can also use @source inline() with the not modifier to exclude specific classes from being generated:
/* [!code filename:CSS] */
@import "tailwindcss";
/* [!code highlight:2] */
@source not inline("container");
This will specifically prevent the container class from being generated, even if the word container is detected in your source files.
For more details, check out the detecting classes in source files documentation.
details-contentWhile you could always add styles to the children of a <details> element, it's been impossible to style the content container itself.
The new details-content variant targets the content container which is useful for positioning the content container relative to the <summary> element:
<!-- [!code classes:details-content:mt-3,details-content:-ml-0.5] -->
<details class="rounded-lg border border-transparent p-6 details-content:mt-3 details-content:-ml-0.5" open>
<summary class="text-sm leading-6 font-semibold text-gray-900 select-none dark:text-white">
Why do they call it Ovaltine?
</summary>
<div class="border-gray-200 bg-gray-50 py-3 pl-3 dark:border-white/10 dark:bg-gray-800/50 ...">
<p>The mug is round. The jar is round. They should call it Roundtine.</p>
</div>
</details>
inverted-colors modeUse the inverted-colors variant to conditionally add styles when the user has enabled an inverted color scheme in their OS:
<!-- [!code classes:inverted-colors:shadow-none] -->
<div class="shadow-xl inverted-colors:shadow-none ...">
<!-- ... -->
</div>
This is useful for things like preventing black shadows being turned white when inverted colors are enabled.
noscript variantYes, some people disable JavaScript and now you can tell them your app doesn't work without it. The noscript variant lets you conditionally apply styles when JS is disabled:
<div class="hidden noscript:block">Please enable JavaScript to use this app.</div>
You could already do this with the <noscript> tag but now you can do it with CSS too, which Tailwind lets you write in your HTML, so… yeah.
user-valid and user-invalidEver tried the :invalid pseudo-class only for the page to be full of red invalid states as soon as it loads, before the user has even touched your form?
The new user-valid and user-invalid variants try to solve this problem, by only applying validation-related styling after the user has actually interacted with the controls:
<!-- [!code classes:user-valid:border-green-500,user-invalid:border-red-500] -->
<input required class="border user-valid:border-green-500" />
<input required class="border user-invalid:border-red-500" />
So that's it, that's Tailwind CSS v4.1! Update to the latest version using npm and start playing with it today:
<CodeExampleStack># [!code filename:Using the Tailwind CLI]
npm install tailwindcss@latest @tailwindcss/cli@latest
# [!code filename:Using Vite]
npm install tailwindcss@latest @tailwindcss/vite@latest
# [!code filename:Using PostCSS]
npm install tailwindcss@latest @tailwindcss/postcss@latest
Looking forward to seeing what you build with the new features!