There was once upon a time when native CSS lacked many essential features, leaving developers to come up with all sorts of ways to make CSS easier to write over the years.
These ways can mostly be categorized into two groups:
- Pre-processors
- Post-processors
Pre-processors include tools like Sass, Less, and Stylus. Like what the category’s name suggests, these tools let you write CSS in their syntax before compiling your code into valid CSS.
Post-processors work the other way — you write non-valid CSS syntax into a CSS file, then post-processors will change those values into valid CSS.
There are two major post-processors today:
- PostCSS
- LightningCSS
PostCSS is the largest kid on the block while Lightning CSS is a new and noteworthy one. We’ll talk about them both in a bit.
I think post-processors have won the compiling game
Post-processors have always been on the verge of winning since PostCSS has always been a necessary tool in the toolchain.
The most obvious (and most useful) PostCSS plugin for a long time is Autoprefixer — it creates vendor prefixes for you so you don’t have to deal with them.
/* Input */
.selector {
transform: /* ... */;
}
.selector {
-webkit-transform: /* ... */;
transform: /* ... */;
}
Arguably, we don’t need Autoprefixer much today because browsers are more interopable, but nobody wants to go without Autoprefixer because it eliminates our worries about vendor prefixing.
What has really tilted the balance towards post-processors includes:
- Native CSS gaining essential features
- Tailwind removing support for pre-processors
- Lightning CSS
Let me expand on each of these.
Native CSS gaining essential features
CSS pre-processors existed in the first place because native CSS lacked features that were critical for most developers, including:
- CSS variables
- Nesting capabilities
- Allowing users to break CSS into multiple files without additional fetch requests
- Conditionals like
if
andfor
- Mixins and functions
Native CSS has progressed a lot over the years. It has gained great browser support for the first two features:
- CSS Variables
- Nesting
With just these two features, I suspect a majority of CSS users won’t even need to fire up pre-processors or post-processors. What’s more, The if()
function is coming to CSS in the future too.
But, for the rest of us who needs to make maintenance and loading performance a priority, we still need the third feature — the ability to break CSS into multiple files. This can be done with Sass’s use
feature or PostCSS’s import
feature (provided by the postcss-import
plugin).
PostCSS
also contains plugins that can help you create conditionals, mixins, and functions should you need them.
Although, from my experience, mixins can be better replaced with Tailwind’s @apply
feature.
This brings us to Tailwind.
Tailwind removing support for pre-processors
Tailwind 4 has officially removed support for pre-processors. From Tailwind’s documentation:
Tailwind CSS v4.0 is a full-featured CSS build tool designed for a specific workflow, and is not designed to be used with CSS pre-processors like Sass, Less, or Stylus. Think of Tailwind CSS itself as your pre-processor — you shouldn’t use Tailwind with Sass for the same reason you wouldn’t use Sass with Stylus. Since Tailwind is designed for modern browsers, you actually don’t need a pre-processor for things like nesting or variables, and Tailwind itself will do things like bundle your imports and add vendor prefixes.
If you included Tailwind 4 via its most direct installation method, you won’t be able to use pre-processors with Tailwind.
@import `tailwindcss`
That’s because this one import statement makes Tailwind incompatible with Sass, Less, and Stylus.
But, (fortunately), Sass lets you import CSS files if the imported file contains the .css
extension. So, if you wish to use Tailwind with Sass, you can. But it’s just going to be a little bit wordier.
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/preflight.css" layer(base);
@import "tailwindcss/utilities.css" layer(utilities);
Personally, I dislike Tailwind’s preflight styles so I exclude them from my files.
@layer theme, base, components, utilities;
@import 'tailwindcss/theme.css' layer(theme);
@import 'tailwindcss/utilities.css' layer(utilities);
Either way, many people won’t know you can continue to use pre-processors with Tailwind. Because of this, I suspect pre-processors will get less popular as Tailwind gains more momentum.
Now, beneath Tailwind is a CSS post-processor called Lightning CSS, so this brings us to talking about that.
Lightning CSS
Lightning CSS is a post-processor can do many things that a modern developer needs — so it replaces most of the PostCSS tool chain including:
Besides having a decent set of built-in features, it wins over PostCSS because it’s incredibly fast.
Lightning CSS is over 100 times faster than comparable JavaScript-based tools. It can minify over 2.7 million lines of code per second on a single thread.

Speed helps Lightning CSS win since many developers are speed junkies who don’t mind switching tools to achieve reduced compile times. But, Lightning CSS also wins because it has great distribution.
It can be used directly as a Vite plugin (that many frameworks support). Ryan Trimble has a step-by-step article on setting it up with Vite if you need help.
// vite.config.mjs
export default {
css: {
transformer: 'lightningcss'
},
build: {
cssMinify: 'lightningcss'
}
};
If you need other PostCSS plugins, you can also include that as part of the PostCSS tool chain.
// postcss.config.js
// Import other plugins...
import lightning from 'postcss-lightningcss'
export default {
plugins: [lightning, /* Other plugins */],
}
Many well-known developers have switched to Lightning CSS and didn’t look back. Chris Coyier says he’ll use a “super basic CSS processing setup” so you can be assured that you are probably not stepping in any toes if you wish to switch to Lightning, too.
If you wanna ditch pre-processors today
You’ll need to check the features you need. Native CSS is enough for you if you need:
- CSS Variables
- Nesting capabilities
Lightning CSS is enough for you if you need:
- CSS Variables
- Nesting capabilities
import
statements to break CSS into multiple files
Tailwind (with @apply
) is enough for you if you need:
- all of the above
- Mixins
If you still need conditionals like if
, for
and other functions, it’s still best to stick with Sass for now. (I’ve tried and encountered interoperability issues between postcss-for
and Lightning CSS that I shall not go into details here).
That’s all I want to share with you today. I hope it helps you if you have been thinking about your CSS toolchain.
I believe that no matter how far native CSS develops, Sass will always have a right to exist. As long as no colour functions such as lighten(), darken(), maps etc. are available, I see no reason to do without it. The function that if the value ‘null’ is assigned to a variable, the entire line is not output is also extremely practical and offers possibilities for more precise control.
Multiple variables that are linked together but should not be publicly visible are also not possible with native CSS.
Still a long way to go. =)
Native CSS can do all the functions you just mentioned. See CSS Relative Color syntax.
@Jason: Ok, so can you please explain how you would do this with native css?
Not to be rude, but it seems like you just scratched the surface of whats possible with SASS.
Azragh, this kind of thing is super cool with Sass — something that Tailwind or Native CSS can’t do yet. Really hard to make
postcss-for
work together with Lightning to make this happen for some reason.So, if you need proper loops, Sass is still the way to go.
@Azragh
Not to replicate everything on your example (you didn’t define everything but we can see where you were going with it), but here’s a baby example of how I would customise a single value:
Codepen example here: https://codepen.io/spartanatreyu/pen/qEEPBqN
The first element shows a shadow using a default value
The second, third and forth shows a pre-defined re-usable small/medium/large shadow size without needing to write lists or each loops. It avoids scss’ interpolation which keeps it open to extension from elsewhere at both compile-time and run-time.
The fifth example shows a custom once-off value. That avoids the whole one-more-thing-in-the-scss-list problem and it’s also updatable at run-time.
You can extend this example to include shadow offsets/blurs/opacities, colors (including color mixing and components), etc…
@Jayden
Oh you’re right, I forgot some variables:
In my opinion, the most practical thing about my example is the possibility to define a shadow color (of any type), which is then converted to
rgba()
viared()
,green()
, andblue()
. This would certainly also be possible in a similar way withcolor-mix()
, but not so easily controllable via a single config file.In my systems, I have to change exactly one variable to adjust the entire color system – combining SASS color functions like
mix()
,saturate()
,adjust-hue()
etc. makes it extremely easy to obtain harmonious colour scales within a short time, tweak them or to adopt existing ones – these colors are then assigned to the theme components. I can’t imagine doing this with native variables/functions.And even if it were possible to implement my work in this way, the output CSS file would hardly be readable, because pretty much every value would be a variable. Everything that could possibly be theme-specific is defined via the files
_colors.scss
,_layout.scss
,_typography.scss
and_forms.scss
and then inherited by others. This complexity would not be comprehensible via inspector.Then if, for example, you are in a WordPress environment and want to output different stylesheets for backend, editor and frontend on the same basis, there is simply no better solution than SASS.
Also helps with version control. And there are source maps. I could go on.
My CSS toolchain is a combination of SASS and regular CSS, no post-processors involved, and I have found absolutely no disadvantages to this approach. It gives me variables, multiple files, nesting and mixins, and prefixes haven’t been an issue for a while now. I’ll stay with my methodology thanks.
I’ve gradually weened myself off Sass, and now prefer to author ALL my CSS with vanilla CSS.
I stopped writing complex Sass functions and loops many years ago, so the only remaining issue for me was mixins. These will be coming to vanilla CSS soon. Hopefully.
I also use @import for vanilla CSS partials, which Parcel bundler compiles into a single optimised bundle. Parcel uses LightningCSS under the covers.
See https://github.com/basher/Web-UI-Boilerplate/blob/master/ui/src/css/index.css
The cool trick I do is use Tailwind’s utilities + @apply as mixins.
I’ve experimented with vanilla CSS for my personal projects for a while now, and while it’s mostly there, the two features I miss most from Sass are mixins and the ability to use variables in media queries for consistent breakpoints.
I don’t know if you really want variable breakpoints, breakpoints defined through CSS variables, or if you just want to be able to define reusable breakpoints. What LightningCSS supports is the Custom Media Query draft, though:
This works nice for reusable media query definitions in my experience, although the spec is not finalized.
I I’m not sure what I read. There is @import on css native…
And it’s fast and reliable. I use it in my entire projects and that’s how I get cached by the browser.
CSS @import URL() has a massive performance impact, as CSS is render blocking multiple network requests are needed before content can be shown.
So I don’t really recommend using it or at least be aware of performance impact.
I also this could be easy solution but it makes extra http request and blocks rendering as explained by other answer.
Unfortunately we still need processors for it.
Hmmmm, I dont know about ditching pre-css but can’t we just combine the usage of both pre-css and post-css together?
So we can still have the SCSS then to lightning?
Yes we can. But it’s rare to require both nowadays imo.
Is speed the only thing that matters?
Yeah, lighting is faster but can we leverage everything we’ve already done in PostCSS (the styles we wrote and the custom plugins we created) or do we need to rewrite it so it’ll work with Lightning
If we can reuse PostCSS in Lightning, why should be bother and not keep using PostCSS?