Tailwind CSS v4: What to Look Forward to.

Tailwind CSS v4: What to Look Forward to.

Tailwind CSS v4.0 arrived last week as a game-changing evolution of the beloved framework, delivering performance gains, cutting-edge CSS integration, and a radically improved developer workflow. We'll unpack how this release redefines frontend development efficiency and why it represents a major leap from earlier iterations.

Finally, A Painless Setup

One of the most immediate and welcome changes in Tailwind v4.0 is its dramatically simplified installation process. Where Tailwind v3's installation process was notoriously cumbersome that I wrote the below Bash alias to automate Vite project configurations, v4.0 introduces a more intuitive approach.

alias tailwind="npm install -D tailwindcss postcss autoprefixer && npx tailwindcss init -p && echo -e \"/** @type {import('tailwindcss').Config} */
\n\nexport default {\n\tcontent: [\n\t\t'./index.html',\n\t\t'./src/**/*.{js,ts,jsx,tsx}',\n\t],\n\ttheme: {\n\t\textend: {}\n\t},\n\tplugins:[],\n}\" > tailwind.config.js && echo -e '@tailwind base;\n@tailwind components;\n@tailwind utilities;' > ./src/index.css && echo -e 'export default function App() {\n\treturn (\n\t\t<h1 className=\"text-3xl text-red-400 font-bold underline\">\n\t\t\tTailwind successfully added!\n\t\t</h1>\n\t)\n}' > ./src/App.jsx"

The new way of setting up Tailwind v4

  1. Install tailwindcss and @tailwindcss/vite via npm.

     npm install tailwindcss @tailwindcss/vite
    
  2. Add the @tailwindcss/vite plugin to your Vite configuration file (vite.config.js).

     import { defineConfig } from 'vite'
     import react from '@vitejs/plugin-react'
     import tailwindcss from '@tailwindcss/vite';
    
     // https://vite.dev/config/
     export default defineConfig({
         plugins: [react(), tailwindcss()],
     });
    
  3. Add an @import to your CSS file that imports Tailwind CSS.

     @import "tailwindcss";
    

That’s it! Run your development server and start using Tailwind to style your content.

CSS-First Configuration Approach

Tailwind v4 departs from the traditional JavaScript-based configuration and adopts a CSS-first configuration methodology; aligning Tailwind more closely with standard CSS practices.

Developers can now define design tokens, breakpoints, and utilities directly within CSS files using the @theme directive.

Let’s dissect how adding a custom font in your project differs between versions 3 and 4.

Adding a custom font in Tailwind v3

  1. Add a fontFace in your primary CSS file:

     @font-face {
         font-family: "Playwrite IN";
         src: url("../src/assets/fonts/PlaywriteIN-VariableFont_wght.ttf");
     }
    
  2. Go to tailwind.config.js file and extend the fontFamily object inside the theme object like so:

     export default {
       content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
       theme: {
           extend: {
               fontFamily: {
                   primary: ["Playwrite IN", "sans"]
               }
           }
       },
     };
    
  3. You can then use this custom font by passing the font-primary utility class in your text-wrapping elements.

     const App = () => {
         return <div className="font-primary">This is a custom font</div>;
     };
    
     export default App;
    

Tailwind v4 gets rid of the tailwind.config.js file and all configurations are done in your entry point CSS file.

Import the font face as you would in Tailwind v3 above but instead of extending a JavaScript Font Family object, use @theme directive to declare a font variable:

@theme{
    --font-primary: "Playwrite IN", serif;
}

That’s all. Pass the font-primary class in your text or text-wrapping elements just as you would in Tailwind v3.

Scaling Customization: Fonts to Colors

Let’s extend our font example above to include custom colors and border radii to illustrate how the v4 approach scales. Say we want to introduce custom colors and a custom border radius in our project, how would we implement this in both the old and new Tailwind versions?

In Tailwind v3:

export default {
  content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
  theme: {
      extend: {
          fontFamily: {
              primary: ["Playwrite IN", "sans"]
          },
        colors: {
              fancy: 'green',
          },
        borderRadius: {
              huge: '300px',
      },
};

In Tailwind v4:

A single declarative block replaces scattered JavaScript objects.

@theme {
    --font-primary: 'Playwrite IN', serif;
    --color-fancy: green;
    --radius-huge: 300px;
}

The Naming Convention
Note Tailwind’s intuitive variable prefix system:

  • --font-* auto-maps to font-{key} classes

  • --color-* generates text-{key}, bg-{key} utilities

  • --radius-* creates rounded-{key} modifiers

This convention-over-configuration design maintains consistency while removing guesswork. The framework handles the heavy lifting – you focus on shipping features.

const App = () => {
    return (
        <div className="font-primary text-fancy border rounded-huge w-fit mx-auto p-4 mt-4">
            CSS Customization in Tailwind.
        </div>
    );
};

export default App;

Result:

Advantages of a CSS config over a JavaScript config.

Consider this example component that uses Framer Motion to create an animated box:

import { useState } from 'react';
import { motion } from 'framer-motion';

export default function AnimatedBox() {
    const [isToggled, setIsToggled] = useState(false);

    return (
        <div className="flex justify-center items-center h-screen bg-gray-100">
            <motion.div
                className="w-24 h-24 rounded-lg"
                animate={{
                    x: isToggled ? 100 : -100,
                    backgroundColor: isToggled ? '#ff6347' : '#1e90ff',
                }}
                transition={{ duration: 0.5 }}
                onClick={() => setIsToggled(!isToggled)}
            />
        </div>
    );
}

When clicked, this box moves left or right while changing colors. In previous Tailwind versions, even if these colors were defined in your tailwind.config.js, you still had to hardcode them in the component's animation properties.

This creates multiple problems:

  1. If your brand colors need to change, you'd have to update both the config file and each component

  2. With 50 components using these animated colors, you'd end up with 51 sources of truth (50 components plus the config file)

  3. Sure, you could use CSS variables in your entry point file which you’d pass to the animation object, but you'd still maintain two sources of truth (the CSS file and the config file)

Tailwind v4 solves this by moving configuration into the CSS file, creating a single source of truth. Here's how.

If your CSS file looks like this:

@import 'tailwindcss';

@font-face {
    font-family: 'Playwrite IN';
    src: url('../src/assets/fonts/PlaywriteIN-VariableFont_wght.ttf');
}

@theme {
    --font-primary: 'Playwrite IN', serif;
    --color-fancy: lime;
    --color-another-color: orange;
    --radius-huge: 400px;
}

Your component can reference these CSS variables in the Framer Motion animation object like this:

import { useState } from 'react';
import { motion } from 'framer-motion';

export default function AnimatedBox() {
    const [isToggled, setIsToggled] = useState(false);

    return (
        <div className="flex justify-center items-center h-screen bg-gray-100">
            <motion.div
                className="w-24 h-24 rounded-lg"
                animate={{
                    x: isToggled ? 100 : -100,
                    backgroundColor: isToggled
                        ? 'var(--color-fancy)'
                        : 'var(--color-another-color)',
                }}
                transition={{ duration: 0.5 }}
                onClick={() => setIsToggled(!isToggled)}
            />
        </div>
    );
}

Benefits of this approach:

  1. Single Source of Truth: When brand colors need to change, you only update the CSS variables in one place

  2. Maintainability: Changes are automatically reflected system-wide

  3. Consistency: Eliminates the risk of color mismatches across components

  4. Developer Experience: Follows the best practice of having a single source of truth (SSOT)

Dynamic Utilities

Tailwind CSS v4.0 fundamentally rethinks how spacing utilities are generated and departs from a static configuration model to a dynamic, variable-driven system. Spacing utilities such as those used to specify paddings, margins, widths, heights, spacing, and gaps now accept any value out of the box, dynamically generating utilities on demand.

From Manual Labor to Automatic Magic

To support a class like grid-cols-73 in Tailwind v3, you had to manually extend the theme in your config file:

extend: {
  gridTemplateColumns: {
    '73': 'repeat(73, minmax(0, 1fr))',
  }
}

However in v4, the same will generate 73 equally-spaced columns out of the box.

Numeric utilities now scale infinitely – p-11 generates precise padding, h-63 crafts exact heights, and space-y-17 handles niche spacing without configuration battles.

These are values that the v3 engine would ignore and one would have to use verbose escape hatches such as the one below for ‘unconventional’ utility classes like mt-21 and p-13 for example.

<div className="mt-[84px] p-[52px]">...</div>

Automatic Utility Generation

In Tailwind CSS v3.0, utilities like p-4 (a padding of 16px) were pre-generated up to hard-coded limits because the framework relied on a static design system defined in JavaScript. The spacing system was manually mapped in tailwind.config.js like this:

// Default v3 spacing scale (abbreviated)  
spacing: {  
  0: "0px",  
  1: "4px",  
  2: "8px",  
  // ...  
  96: "384px" // Hard upper limit  
}

p-4 mapped directly to padding: 16px because 4 was a key in this object.

Utilities beyond p-96 didn’t exist unless manually added.

While this approach supported predictability in the codebase, it was also too rigid and broke the design system consistency whenever one needed to introduce arbitrary values such as a padding of 400px. Since p-100 did not exist, one would need to use p-[400px].

The new engine treats utilities as dynamic equations rather than static lookups.

Developers can decide what they want p-1 means in their codebase by setting it in the configuration (@theme directive).

@theme {
  --spacing: 0.25rem; /* Change base unit to 4px */
}

Doing this, for instance, updates the spacing utilities (margin, padding, gap, grid) automatically across the project. p-4 in this codebase means a padding of 4px and not a static 16px. As does m-4 and space-x-4.

This shift:

  • Unlocks infinite scaling without config bloat

  • Enables base unit adjustments that cascade globally

  • Eliminates arbitrary value syntax for numeric utilities

  • Frees developers from memorizing framework-specific scales. You no longer need to remember whether or not p-14, m-26 or h-46 are valid utility classes.

Conclusion: A New Chapter

Tailwind CSS v4.0 represents a monumental leap forward in utility-first CSS. While we’ve explored some of the most impactful upgrades from v3 in this article, I’ll be diving deeper into other exciting and advanced features in the coming months.

To those embracing v4.0: may your builds be faster, your config files lighter, and your designs more expressive than ever.

Happy coding — and stay tuned for the next installment! 🎨🚀