Skip to main content

Tailwind CSS v4 Upgrade: Fixing 'Unknown At Rule' and Missing Styles

 You’ve upgraded your dependencies to Tailwind CSS v4, expecting the promised performance boost from the new Oxide engine. Instead, your build finishes instantly but your styles are missing, or your console is flooded with Unknown at rule @tailwind errors.

Tailwind v4 represents a paradigm shift from a JavaScript-configured utility library to a native CSS-first build tool. The breakage occurs because v4 deprecates the proprietary directives and JavaScript configuration reliance you’ve used for years.

Here is why your build is breaking and the rigorous path to fixing it.

The Root Cause: Architecture Shift

In v3, Tailwind relied on PostCSS to scan your tailwind.config.js, generate an Abstract Syntax Tree (AST), and inject CSS via the @tailwind directives (basecomponentsutilities).

In v4, the architecture is inverted:

  1. Native CSS Resolution: The core directives are replaced by standard CSS @import statements.
  2. No Default JS Config: The engine no longer automatically looks for tailwind.config.js for theme values. It expects configuration to exist within the CSS itself via CSS variables.
  3. Linter Lag: Tools like VS Code’s native CSS validator and Stylelint do not inherently recognize the new v4 syntax or the legacy @tailwind directives if the PostCSS context is missing.

The Fix

We will migrate a standard Vite-based project to the v4 architecture. This eliminates the "Unknown at rule" errors and restores your styles.

1. Update the Build Pipeline

Tailwind v4 is optimized to run as a dedicated Vite plugin rather than a generic PostCSS plugin. This provides the performance gains associated with the new engine.

File: vite.config.ts

import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react'; // assuming React, irrelevant for the fix

export default defineConfig({
  plugins: [
    // The Tailwind plugin must come before framework plugins
    tailwindcss(),
    react(),
  ],
});

2. The CSS-First Entry Point

Delete your old directives. The @tailwind at-rule is the primary trigger for "Unknown at rule" errors in v4 if not strictly shimmed. We switch to the native CSS import syntax.

File: src/index.css (or app.css)

/* ❌ DELETE THIS (v3 Syntax) */
/* @tailwind base; */
/* @tailwind components; */
/* @tailwind utilities; */

/* ✅ ADD THIS (v4 Syntax) */
@import "tailwindcss";

/* 
  Configuration is now handled here, not in tailwind.config.js.
  The @theme block exposes standard CSS variables as Tailwind utilities.
*/
@theme {
  /* This replaces theme.extend.fontFamily */
  --font-display: "Satoshi", "sans-serif";
  
  /* This replaces theme.extend.colors */
  --color-brand-primary: oklch(59.69% 0.156 49.77);
  --color-brand-secondary: oklch(85.32% 0.12 49.77);

  /* This replaces theme.extend.spacing */
  --spacing-128: 32rem;
}

/* 
  Usage in HTML remains the same: 
  <div class="font-display text-brand-primary p-128"> 
*/

3. Fixing the IDE Warnings ("Unknown At Rule")

Even with the build working, VS Code may flag @import "tailwindcss" or @theme as errors because standard CSS specs don't officially support them yet.

Option A: The VS Code Setting Fix (Recommended) Force VS Code to ignore unknown CSS rules rather than validating strict W3C compliance for Tailwind files.

Create or update .vscode/settings.json:

{
  "css.lint.unknownAtRules": "ignore",
  "scss.lint.unknownAtRules": "ignore",
  "files.associations": {
    "*.css": "tailwindcss"
  }
}

Option B: Stylelint Configuration If you enforce linting in your CI/CD pipeline, you must update your Stylelint configuration to allow the v4 specifics.

File: .stylelintrc.json

{
  "extends": ["stylelint-config-standard"],
  "rules": {
    "at-rule-no-unknown": [
      true,
      {
        "ignoreAtRules": ["import", "theme", "utility", "apply"]
      }
    ],
    "import-notation": null
  }
}

4. Handling Legacy JavaScript Config

If you have a massive tailwind.config.js and cannot migrate everything to CSS variables immediately, you can import the JS config into your CSS. This bridges the gap between v3 and v4.

File: src/index.css

@import "tailwindcss";

/* Point strictly to your legacy config */
@config "../tailwind.config.js";

Note: This detects the file, but v4 is stricter. Ensure your content array in the JS config is clean, though v4 mostly detects content files automatically now.

Why This Works

The Move to @import

By using @import "tailwindcss";, we leverage the CSS loader's native ability to resolve node modules. The @tailwindcss/vite plugin intercepts this import. Instead of reading a JS file to determine what CSS to generate, it treats the CSS file as the source of truth.

The @theme Block

In v3, configuration was opaque JavaScript. In v4, configuration is CSS variables. When you define --color-brand: red inside @theme, Tailwind's internal engine registers brand as a token in its utility generator. This creates text-brandbg-brand, and border-brand automatically.

This removes the serialization overhead of passing data between Node.js (config) and the CSS generation engine, resulting in the sub-millisecond hot module replacement (HMR) times seen in v4.

Conclusion

The "Unknown At Rule" error is a symptom of tooling expecting the old PostCSS workflow. By switching to the @tailwindcss/vite plugin and adopting the CSS-first @theme configuration, you align your project with the intended architecture of v4. Treat your CSS file as the configuration engine, and the errors will resolve.

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