Skip to main content

Why Your Styles Broke in Tailwind CSS v4: The @apply and Config Shift

 You upgraded to Tailwind CSS v4 to leverage the new Oxide engine, expecting a silent performance boost. Instead, your build pipeline is screaming about unresolved classes, or worse—the build succeeds, but your custom components relying on @apply have completely lost their styling.

The issue isn't just a syntax change; it is an architectural inversion. Tailwind v4 moves the source of truth from JavaScript (tailwind.config.js) to CSS. This shift fundamentally alters how the engine resolves custom values and processes the @apply directive.

The Root Cause: The Death of the JavaScript AST

In v2 and v3, Tailwind functioned as a JavaScript-heavy PostCSS plugin. When you defined a color in tailwind.config.js, the engine built a massive internal JavaScript object representing your theme. When you used @apply bg-primary, Tailwind looked up primary in that JS object to generate the CSS.

In v4, the engine is native (Rust). It does not want to read a JavaScript configuration file if it can avoid it. It treats CSS variables as the first-class citizens for configuration.

Your styles are breaking because:

  1. Missing Utility Generation: If you are still relying entirely on a legacy JS config without explicitly binding it to the new CSS-first detection, v4 may fail to generate the utility classes (e.g., bg-brand) required for @apply to work.
  2. @apply Timing: In v4, @apply is no longer a "magic" pre-processor step that happens before CSS generation. It is tightly coupled with the existence of CSS variables defined in the @theme block. If the variable isn't defined, the utility class doesn't exist, and @apply throws an error.

The Fix: Porting Config to CSS Variables

To fix this, we must align with the v4 architecture. We will migrate custom theme values from the JavaScript config directly into the CSS entry point using the new @theme directive. This ensures both standard utilities and @apply directives can resolve your custom values.

1. The Broken State (Legacy)

You likely have a setup resembling this, which causes friction in v4:

// tailwind.config.js (Legacy)
module.exports = {
  theme: {
    extend: {
      colors: {
        neon: '#ccff00',
      },
      fontFamily: {
        sans: ['Inter', 'sans-serif'],
      }
    },
  },
};
/* main.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

.btn-primary {
  /* This often fails or behaves unexpectedly in v4 migration */
  @apply bg-neon text-black font-sans py-2 px-4 rounded;
}

2. The Solution (Modern v4)

Delete tailwind.config.js (unless you have complex plugin logic) and move the configuration to your CSS entry point. Use the @import "tailwindcss" syntax to load the framework, and the @theme block to define your tokens.

/* main.css */

/* 1. Import Tailwind using the modern directive */
@import "tailwindcss";

/* 2. Define your theme using CSS variables inside the @theme block */
@theme {
  /* 
     Tailwind v4 automatically maps these variables to utility classes.
     --color-neon becomes .text-neon, .bg-neon, .border-neon, etc.
  */
  --color-neon: #ccff00;
  
  /* 
     Standardize fonts. 
     --font-sans becomes .font-sans
  */
  --font-sans: "Inter", "ui-sans-serif", system-ui;
  
  /* 
     You can even override spacing or breakpoints here
  */
  --spacing-4-5: 1.125rem;
}

/* 3. Apply now works because the utilities are generated from the variables above */
.btn-primary {
  @apply bg-neon font-sans py-2 px-4 rounded text-black;
  
  /* 
     Bonus: You can now use the variables natively without @apply 
     if you need specific CSS manipulation.
  */
  box-shadow: 0 4px 14px var(--color-neon);
}

3. Handling Alpha Modifiers

One specific reason @apply breaks during migration is the handling of color opacity modifiers (e.g., bg-neon/50).

In v3, this was calculated via JS. In v4, for this to work with custom colors, you should ideally use modern color spaces (like oklch) or ensure your hex definitions are clean. The oklch format is preferred in v4 for better interpolation.

@theme {
  /* Using oklch allows Tailwind to handle opacity modifiers natively */
  --color-neon: oklch(0.85 0.29 110); 
}

.btn-glass {
  /* This works perfectly in v4 */
  @apply bg-neon/50 backdrop-blur-md;
}

Why This Works

The magic lies in how the @theme directive functions. When you define --color-neon inside @theme:

  1. Variable Injection: Tailwind injects --color-neon into the :root of your CSS.
  2. Utility Generation: The engine scans the @theme block and automatically generates the corresponding utility families (bg-*text-*border-*) linked to that variable.
  3. Resolution: When @apply bg-neon runs, it looks for the .bg-neon class. Because the @theme block registered it, the class exists.

If you stick to tailwind.config.js without properly configuring the v4 PostCSS plugin or the CLI, the engine might not inject the utilities into the CSS context where @apply is running, resulting in the "class does not exist" error.

Conclusion

Tailwind CSS v4 is not just an upgrade; it is a shift toward native CSS architecture. By moving your configuration from JavaScript objects to CSS variables within the @theme block, you align your project with the new engine's logic. This fixes your broken @apply directives and future-proofs your stack for the CSS-first era.

Popular posts from this blog

Restricting Jetpack Compose TextField to Numeric Input Only

Jetpack Compose has revolutionized Android development with its declarative approach, enabling developers to build modern, responsive UIs more efficiently. Among the many components provided by Compose, TextField is a critical building block for user input. However, ensuring that a TextField accepts only numeric input can pose challenges, especially when considering edge cases like empty fields, invalid characters, or localization nuances. In this blog post, we'll explore how to restrict a Jetpack Compose TextField to numeric input only, discussing both basic and advanced implementations. Why Restricting Input Matters Restricting user input to numeric values is a common requirement in apps dealing with forms, payment entries, age verifications, or any data where only numbers are valid. Properly validating input at the UI level enhances user experience, reduces backend validation overhead, and minimizes errors during data processing. Compose provides the flexibility to implement ...

jetpack compose - TextField remove underline

Compose TextField Remove Underline The TextField is the text input widget of android jetpack compose library. TextField is an equivalent widget of the android view system’s EditText widget. TextField is used to enter and modify text. The following jetpack compose tutorial will demonstrate to us how we can remove (actually hide) the underline from a TextField widget in an android application. We have to apply a simple trick to remove (hide) the underline from the TextField. The TextField constructor’s ‘colors’ argument allows us to set or change colors for TextField’s various components such as text color, cursor color, label color, error color, background color, focused and unfocused indicator color, etc. Jetpack developers can pass a TextFieldDefaults.textFieldColors() function with arguments value for the TextField ‘colors’ argument. There are many arguments for this ‘TextFieldDefaults.textFieldColors()’function such as textColor, disabledTextColor, backgroundColor, cursorC...

jetpack compose - Image clickable

Compose Image Clickable The Image widget allows android developers to display an image object to the app user interface using the jetpack compose library. Android app developers can show image objects to the Image widget from various sources such as painter resources, vector resources, bitmap, etc. Image is a very essential component of the jetpack compose library. Android app developers can change many properties of an Image widget by its modifiers such as size, shape, etc. We also can specify the Image object scaling algorithm, content description, etc. But how can we set a click event to an Image widget in a jetpack compose application? There is no built-in property/parameter/argument to set up an onClick event directly to the Image widget. This android application development tutorial will demonstrate to us how we can add a click event to the Image widget and make it clickable. Click event of a widget allow app users to execute a task such as showing a toast message by cli...