Matthew Verstraete
Matthew Verstraete

Reputation: 6791

Convert TailwindCSS to native CSS?

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

Answers (4)

rozsazoltan
rozsazoltan

Reputation: 8923

TL;DR: I had to write the first answer with PurgeCSS because many unused variables and classes ended up in the build.

  • The unused classes are removed by using the .gitignore file (see the Update section).
  • The unused CSS variables are removed starting from version 4.0.5 (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.

Solution with Tailwind CSS v4

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.

Install dependencies

npm install tailwindcss @tailwindcss/postcss postcss @fullhuman/postcss-purgecss

Create a converter solution

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 @layer, @property directives and comments to the result. We remove these using our own plugins: removeLayerRules, removePropertyRules and removeCommentRules. (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}

Update

Refactor in automatic-source-detection

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.

Tailwind CSS v4.0.5

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.


Without PurgeCSS

.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));

Known issues

  • Without PurgeCSS, two unused variables still appear: --font-sans, --font-mono.

@layer and @property supports

Although 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

rozsazoltan
rozsazoltan

Reputation: 8923

Solution with Tailwind CSS v4

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:

Solution with Tailwind CSS v3

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

Why?

The solution aligns with the premise of the question:

  • It requires minimal extra knowledge (in fact, if you copy the JavaScript code, install the 2 dependencies, and run the command, it will do its job)
  • You don't need to create your own input.css or add extra classes
  • It's easily configurable through the TailwindCSS Docs, in the default tailwind.config.js
  • Unlike all online solutions, this can be automated and requires minimal manual effort

The expected result with my solution:

.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

krishnaacharyaa
krishnaacharyaa

Reputation: 25080

Paste your html code in tailwind-playground

You can access the css in the generated css files tab.

Example

enter image description here

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

Foysal imran
Foysal imran

Reputation: 143

You may use this tool: https://tailwind-to-css.vercel.app/ for converting the tailwind CSS class to normal CSS.

Upvotes: 3

Related Questions