Reputation: 6791
I have been given an html file that is written using TailwindCSS and I am trying to figure out how to convert all there stuff to native CSS.
I have found a convert that will just do the class but that requires me to extract thousands of classes from the code manually and then repast it. Is there some tool where I can just upload the whole html file and it spit out a CSS version of it for me or do I have to manually do this whole conversion?
I would prefer something online as I don't want to go though having to install a bunch of 3rd party tools, learning there system, do the convert, and then uninstall everything.
Upvotes: 12
Views: 17712
Reputation: 8923
TL;DR: I had to write the first answer with PurgeCSS because many unused variables and classes ended up in the build.
.gitignore
file (see the Update section).So, the code in the first part of the answer works with PurgeCSS, while the updated version at the end of the answer works without PurgeCSS.
In both TailwindCSS v3 and v4, it is possible to generate classes via the CLI. However, this approach injects a lot of "unnecessary" classes and styling rules into the final output, which could already be used in production. I wanted to find a solution where only the actually used classes and variables are kept in the output.
For this, in TailwindCSS v4, we will need PostCSS and the @tailwindcss/postcss
plugin. Additionally, we will need to use @fullhuman/postcss-purgecss
and write two custom PostCSS plugins. My answer includes all of this. The solution sounds complicated, but it is actually simple. In the final result, we will only see the essential CSS variables and classes.
Note: You will need to specify in PurgeCSS where your CSS classes are used in your files, so it can filter out unnecessary styling rules.
Note: Starting from TailwindCSS v4.0.5 and using a .gitignore
file, PurgeCSS is no longer necessary, see the "Without PurgeCSS" section below.
npm install tailwindcss @tailwindcss/postcss postcss @fullhuman/postcss-purgecss
The different PostCSS plugins will run sequentially, utilizing the results of each other. First, we start the process with .process
and inject the CSS code needed for TailwindCSS, so there's no need for a separate style.css
where you'd have to do this manually.
Why don't we use the @import "tailwindcss"
recommended in the documentation? Because this brings in three different imports, one of which is preflight.css
. However, preflight.css
isn't necessary because it injects a CSS reset solution into the final result. See here: Preflight - TailwindCSS v4 Docs
First, the TailwindCSS Plugin runs, collecting all the used classes and variables, and passes them along with its own default parameters.
(Until v4.0.4) It is necessary to filter out the unnecessary classes (e.g., h-auto
, w-auto
, which are not present in the example file). To do this, we use the PurgeCSS Plugin. We simply configure it to search for CSS class usage in specified files. Based on this, it will retain only the used classes and variables.
TailwindCSS also adds comments to the result. We remove these using our own plugins: @layer
, @property
directives andremoveLayerRules
, removePropertyRules
andremoveCommentRules
. (See: "@layer
and @property
supports" section below)
Extra: The prettier()
function is necessary because the TailwindCSS PostCSS-plugin returns incorrectly indented results with the optimize: true
and minify: false
settings. It can be omitted if you don't mind the incorrect line indentation.
Finally, we output the result to the output.css
file.
// convert-tailwind-to-css.js
import fs from 'fs';
import postcss from 'postcss';
import tailwindcssPlugin from '@tailwindcss/postcss';
import { purgeCSSPlugin } from '@fullhuman/postcss-purgecss';
// Minify?
const args = process.argv.slice(2);
let minify = false;
if (args.includes('--minify')) {
minify = true;
}
// Generated CSS indent spaces count
const indentSpaces = 2;
// Generated CSS output file
const outputCSS = './output.css';
// Custom PostCSS plugin to remove comments
const removeCommentRules = (root) => {
root.walkComments((comment) => {
comment.remove();
});
};
// CSS Prettier (TailwindCSS with LightningCSS returns incorrectly formatted results with the settings optimize: true and minify: false)
const prettier = (css, indent = 2) => {
const lines = css.split('\n');
let indentLevel = 0;
return lines
.map((line) => {
const trimmed = line.trim();
if (trimmed.endsWith('}')) {
indentLevel = Math.max(indentLevel - 1, 0);
}
const formattedLine = ' '.repeat(indentLevel * indent) + trimmed;
if (trimmed.endsWith('{')) {
indentLevel++;
}
return formattedLine;
})
.join('\n');
};
// Convert Tailwind CSS to native CSS
postcss([
tailwindcssPlugin({
optimize: {
minify, // minify or not?
},
}),
purgeCSSPlugin({
content: [
'./**/*.html',
'./**/*.js',
'./**/*.jsx',
'./**/*.ts',
'./**/*.tsx',
'./**/*.vue',
],
defaultExtractor: content => content.match(/[\w-/:.\[\]\(\)_]+(?<!:)/g) || [],
variables: true, // Remove unused CSS variables
keyframes: true, // Remove unused animations
fontFace: true, // Remove unused font faces
}),
removeCommentRules,
])
.process(`
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
/* preflight: Not required, it only creates rules for CSS reset. */
/* @import "tailwindcss/preflight.css" layer(base); */
@import "tailwindcss/utilities.css" layer(utilities);
`, { from: './src' })
.then((result) => {
let formattedCSS;
if (! minify) {
// Format CSS (The optimize result returns the output with incorrect indentation.)
formattedCSS = prettier(result.css, indentSpaces);
}
// Write the generated CSS to a file
fs.writeFileSync(outputCSS, formattedCSS || result.css, 'utf8');
console.log(`Native CSS generated: ${outputCSS}`);
})
.catch((err) => console.error('An error occurred:', err));
For my example, I used the following index.html
:
<!-- index.html -->
<div class="text-center text-red-500 font-bold text-[2rem] lg:text-[4rem] lg:text-left md:hover:first:text-blue-500">
Hello, World!
</div>
The expected result with my solution:
node convert-tailwind-to-css.js
@layer theme {
:root, :host {
--color-red-500: oklch(.637 .237 25.331);
--color-blue-500: oklch(.623 .214 259.815);
--spacing: .25rem;
--font-weight-bold: 700;
}
}
@layer base, components;
@layer utilities {
:where(.space-y-6 > :not(:last-child)) {
--tw-space-y-reverse: 0;
margin-block-start: calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));
margin-block-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)));
}
.text-center {
text-align: center;
}
.text-\[2rem\] {
font-size: 2rem;
}
.font-bold {
--tw-font-weight: var(--font-weight-bold);
font-weight: var(--font-weight-bold);
}
.text-red-500 {
color: var(--color-red-500);
}
@media (width >= 48rem) {
@media (hover: hover) {
.md\:hover\:first\:text-blue-500:hover:first-child {
color: var(--color-blue-500);
}
}
}
@media (width >= 64rem) {
.lg\:text-left {
text-align: left;
}
.lg\:text-\[4rem\] {
font-size: 4rem;
}
}
}
@property --tw-space-y-reverse {
syntax: "*";
inherits: false;
initial-value: 0;
}
@property --tw-font-weight {
syntax: "*";
inherits: false
}
And minified result with --minify
flag:
node convert-tailwind-to-css.js --minify
@layer theme{:root,:host{--color-red-500:oklch(.637 .237 25.331);--color-blue-500:oklch(.623 .214 259.815);--spacing:.25rem;--font-weight-bold:700}}@layer base,components;@layer utilities{:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}.text-center{text-align:center}.text-\[2rem\]{font-size:2rem}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.text-red-500{color:var(--color-red-500)}@media (width>=48rem){@media (hover:hover){.md\:hover\:first\:text-blue-500:hover:first-child{color:var(--color-blue-500)}}}@media (width>=64rem){.lg\:text-left{text-align:left}.lg\:text-\[4rem\]{font-size:4rem}}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-font-weight{syntax:"*";inherits:false}
Starting from v4, there's no need to specify the sources, as the TailwindCSS engine automatically detects them, even in the node_modules
folder. This is how unused classes might end up being included in the build. Using PurgeCSS to avoid unused classes can almost entirely be avoided by adding the exclusion of the node_modules
folder to the .gitignore
. The v4 engine takes the contents of the .gitignore
into account when searching for sources.
But you can still specify individual sources using the @source
directive.
With the fix released in version v4.0.5, unused CSS variables are no longer included in the output generated by Tailwind CSS. However, PurgeCSS is still necessary to remove completely unused classes that are unnecessarily included in the output.
.gitignore
/node_modules/
convert-tailwind-to-css.js
// convert-tailwind-to-css.js
import fs from 'fs';
import postcss from 'postcss';
import tailwindcssPlugin from '@tailwindcss/postcss';
// Minify?
const args = process.argv.slice(2);
let minify = false;
if (args.includes('--minify')) {
minify = true;
}
// Generated CSS indent spaces count
const indentSpaces = 2;
// Generated CSS output file
const outputCSS = './output.css';
// Custom PostCSS plugin to remove comments
const removeCommentRules = (root) => {
root.walkComments((comment) => {
comment.remove();
});
};
// CSS Prettier (TailwindCSS with LightningCSS returns incorrectly formatted results with the settings optimize: true and minify: false)
const prettier = (css, indent = 2) => {
const lines = css.split('\n');
let indentLevel = 0;
return lines
.map((line) => {
const trimmed = line.trim();
if (trimmed.endsWith('}')) {
indentLevel = Math.max(indentLevel - 1, 0);
}
const formattedLine = ' '.repeat(indentLevel * indent) + trimmed;
if (trimmed.endsWith('{')) {
indentLevel++;
}
return formattedLine;
})
.join('\n');
};
// Convert Tailwind CSS to native CSS
postcss([
tailwindcssPlugin({
optimize: {
minify, // minify or not?
},
}),
removeCommentRules,
])
.process(`
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
/* preflight: Not required, it only creates rules for CSS reset. */
/* @import "tailwindcss/preflight.css" layer(base); */
@import "tailwindcss/utilities.css" layer(utilities);
`, { from: './src' })
.then((result) => {
let formattedCSS;
if (! minify) {
// Format CSS (The optimize result returns the output with incorrect indentation.)
formattedCSS = prettier(result.css, indentSpaces);
}
// Write the generated CSS to a file
fs.writeFileSync(outputCSS, formattedCSS || result.css, 'utf8');
console.log(`Native CSS generated: ${outputCSS}`);
})
.catch((err) => console.error('An error occurred:', err));
--font-sans
, --font-mono
.@layer
and @property
supportsAlthough in the first version of my answer I ignored these CSS at-rules, I later thought that anyone actually using v4 wouldn't need to ignore them. Originally, I decided to remove these for optimal size minimization, which was achieved by these two very simple PostCSS plugins:
// Custom PostCSS plugin to remove `@layer` rules but keep the CSS inside
const removeLayerRules = (root) => {
root.walkAtRules('layer', (rule) => {
rule.replaceWith(rule.nodes);
});
};
// Custom PostCSS plugin to remove `@property` rules
const removePropertyRules = (root) => {
root.walkAtRules('property', (rule) => {
rule.remove();
});
};
Upvotes: 2
Reputation: 8923
I didn't include the v4 use case in this answer as it would be too lengthy, but you can find it at the following link:
I see that only online tools are available. I created a PostCSS script that generates the required output.css
from TailwindCSS without needing any input.css
. For my example, I used the following index.html
:
<!-- index.html -->
<div class="text-center text-red-500 font-bold text-[2rem] lg:text-[4rem] lg:text-left">
Hello, World!
</div>
Since we will be using PostCSS and TailwindCSS, let's install them in the root of the project.
npm install tailwindcss@3 postcss
Create your own tailwind.config.js
and customize it.
// tailwind.config.js
module.exports = {
content: ['./src/**/*.{js,ts,vue}', './index.html'],
theme: {
extend: {},
},
plugins: [],
};
And let's create our convert-tailwind-to-css.js
file, in which PostCSS will generate our file output.css based on the TailwindCSS properties.
By default, this would be generated with 4 spaces, but I integrated a feature that allows you to replace it with any number of spaces (in my case: 2
). Of course, if you plan to minify it later, this could be considered an unnecessary step.
// convert-tailwind-to-css.js
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
import fs from 'fs';
import postcss from 'postcss';
import tailwindcss from 'tailwindcss';
// Generated CSS indent spaces count
const indentSpaces = 2;
// Generated CSS output file
const outputCSS = './output.css';
// Load tailwind.config.js
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const configPath = resolve(__dirname, './tailwind.config.js');
// Convert Tailwind CSS to native CSS
postcss([
tailwindcss(configPath),
])
.process('@tailwind utilities; @tailwind components;', { from: undefined })
.then((result) => {
// Format and write the CSS output
const formattedCSS = result.css
.replaceAll(' '.repeat(4), ' '.repeat(indentSpaces)) // Handle indentation
.replace(/([^{;\s]+:[^;}]+)(\s*?)\n(\s*})/g, '$1;\n$3'); // Insert semicolon before newline and closing brace, preserving indentation
fs.writeFileSync(outputCSS, formattedCSS, 'utf8');
console.log(`Native CSS generated: ${outputCSS}`);
})
.catch((err) => console.error('An error occurred:', err));
node convert-tailwind-to-css.js
The solution aligns with the premise of the question:
tailwind.config.js
.text-center {
text-align: center;
}
.text-\[2rem\] {
font-size: 2rem;
}
.font-bold {
font-weight: 700;
}
.text-red-500 {
--tw-text-opacity: 1;
color: rgb(239 68 68 / var(--tw-text-opacity, 1));
}
@media (min-width: 1024px) {
.lg\:text-left {
text-align: left;
}
.lg\:text-\[4rem\] {
font-size: 4rem;
}
}
Upvotes: 1
Reputation: 25080
html code
in tailwind-playground
generated css files
tab.Example
According to tailwind-css docs
Tailwind automatically injects these styles when you include @tailwind base in your CSS (the first 552 lines of generated css)
You can remove those base classes by setting preflight = false
in tailwind.config.cs
module.exports = {
corePlugins: {
preflight: false,
}
}
Upvotes: 11
Reputation: 143
You may use this tool: https://tailwind-to-css.vercel.app/ for converting the tailwind CSS class to normal CSS.
Upvotes: 3