Skip to content

Fix: UnoCSS Not Working — Classes Not Generating, Presets Missing, or Attributify Mode Broken

FixDevs · (Updated: )

Part of:  React & Frontend Errors

Quick Answer

How to fix UnoCSS issues — Vite plugin setup, preset configuration, attributify mode, icons preset, shortcuts, custom rules, and integration with Next.js, Nuxt, and Astro.

The Problem

UnoCSS utility classes render in the HTML but have no effect:

<div class="p-4 bg-blue-500 text-white rounded-lg">
  No styles applied
</div>

Or the @unocss/preset-icons icons don’t show:

<div class="i-mdi-home w-6 h-6" />
<!-- Empty div — no icon -->

Or attributify mode types don’t work:

<div bg="blue-500" p="4" text="white">
  TypeScript error: Property 'bg' does not exist on type...
</div>

Or generated CSS is empty even though classes are used:

[UnoCSS] No CSS generated. Make sure the content is scanned correctly.

Why This Happens

UnoCSS is an on-demand atomic CSS engine. It scans your source files for class names and generates only the CSS for classes you actually use. That model is fast at runtime but unforgiving at setup: if the scanner can’t find your files, no CSS is generated, and the page renders the class names as plain unstyled markup.

The single most common cause of “classes do nothing” is the content scan path. With the Vite plugin, UnoCSS scans whatever Vite serves — usually fine. With the webpack plugin (Next.js, CRA), or in monorepos where components live in a separate package, you must declare content.filesystem explicitly so the scanner walks those directories. Without that declaration, your file isn’t scanned and your bg-blue-500 produces no rule.

A second source of trouble is the attributify transformer. presetAttributify() rewrites HTML attribute syntax (bg="blue-500") into class names that the rest of UnoCSS understands. The transformer must run before any framework-specific preprocessing (Vue, Svelte) that would turn those attributes into props. In Vue/Svelte that means making sure the UnoCSS plugin is registered before the framework plugin in vite.config.ts. In TypeScript/JSX, the attributes also fail typecheck unless you augment HTMLAttributes with AttributifyAttributes.

A third trap is preset-icons collection imports. The class i-mdi-home is parsed as “collection = mdi, icon = home” and UnoCSS tries to load the icon data from @iconify-json/mdi. If that package isn’t installed, UnoCSS emits an empty class — no warning by default. Install the matching @iconify-json/<collection> package for every icon collection you use.

  • UnoCSS generates nothing by default — without presets, UnoCSS has zero built-in utility classes.
  • The scanner must find your files — UnoCSS reads files listed in its content configuration or detected by the Vite plugin.
  • Icon classes need icon data installed@unocss/preset-icons generates SVG icons as CSS masks. The actual SVG data comes from @iconify-json/* packages.
  • Attributify mode requires explicit opt-in — the @unocss/preset-attributify preset enables HTML attribute syntax.

Diagnostic Timeline

A senior dev’s first guess for “my UnoCSS isn’t working” is usually “check the class names for typos.” That fix occasionally helps. The real cause is almost always configuration.

Minute 0 — Inspect the generated stylesheet. Open view-source:http://localhost:5173/__uno.css (Vite) or look for the CSS bundle in the network tab. If it’s empty or only contains preflight, no classes were scanned. If it contains classes but they don’t match what you typed, you have a typo or a dynamic class that UnoCSS can’t see.

Minute 3 — Check content paths. In uno.config.ts, log whether content.filesystem is set. With the Vite plugin you usually don’t need it. With webpack, Next.js, or any setup that imports components from outside the Vite root, set content: { filesystem: ['src/**/*.{html,tsx,ts,vue,svelte}', '../packages/ui/**/*.tsx'] } explicitly.

Minute 6 — Check transformer-attributify order. If bg="blue-500" produces no styles but class="bg-blue-500" works, the attributify transformer isn’t running. Verify both presetAttributify() is in presets AND that the UnoCSS plugin appears before the Vue/Svelte/React plugin in vite.config.ts. In Vue, the framework plugin strips unknown attributes before UnoCSS can read them.

Minute 10 — Check preset-icons collection imports. Run npm ls @iconify-json/mdi (or whichever collection you use). If it’s missing, install it. Restart the dev server after installing — UnoCSS only checks the package list at boot.

Minute 13 — Check for dynamic class strings. UnoCSS scans source files statically. className={`text-${color}-500`} won’t be detected because the scanner sees a literal template, not the runtime value. Either add the possible classes to safelist, or refactor to use a complete class string: className={color === 'red' ? 'text-red-500' : 'text-blue-500'}.

Minute 16 — Check virtual module import. In Vite, you must import virtual:uno.css in your entry file. In webpack/Next, the import is uno.css. If you forget this, classes are generated but never injected into the page. Open the page source and grep for uno; if no link or style tag references it, you missed the import.

Minute 18 — Check className vs class in JSX. React uses className, Vue and Svelte use class. UnoCSS scans for both, but a TypeScript build can drop unknown JSX attributes if you typed class="..." in JSX. The fix is to use className in .tsx, not class.

Fix 1: Set Up UnoCSS with Vite

npm install -D unocss
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import UnoCSS from 'unocss/vite';

export default defineConfig({
  plugins: [
    UnoCSS(),  // Must be before framework plugin
    react(),
  ],
});
// uno.config.ts — configuration file
import {
  defineConfig,
  presetUno,           // Default preset (Tailwind + Windi CSS compatible)
  presetAttributify,   // Optional: attribute mode
  presetIcons,         // Optional: icon support
  presetTypography,    // Optional: prose styles
  presetWebFonts,      // Optional: web font loading
  transformerDirectives,    // Optional: @apply in CSS
  transformerVariantGroup,  // Optional: hover:(bg-blue text-white)
} from 'unocss';

export default defineConfig({
  presets: [
    presetUno(),           // Core utilities (Tailwind-like)
    presetAttributify(),   // <div text="sm white" bg="blue-500">
    presetIcons({
      scale: 1.2,
      cdn: 'https://esm.sh/',  // Fallback CDN if icon package isn't installed
      extraProperties: {
        'display': 'inline-block',
        'vertical-align': 'middle',
      },
    }),
    presetTypography(),
    presetWebFonts({
      fonts: {
        sans: 'Inter:400,500,600,700',
        mono: 'Fira Code:400,500',
      },
    }),
  ],
  transformers: [
    transformerDirectives(),    // Enable @apply in CSS files
    transformerVariantGroup(),  // Enable variant groups: hover:(bg-blue text-white)
  ],
  // Explicit content paths — required for monorepos or webpack
  content: {
    filesystem: [
      'src/**/*.{html,tsx,ts,vue,svelte}',
      '../packages/ui/**/*.{tsx,vue,svelte}',
    ],
  },
});
// src/main.tsx — import the virtual CSS file
import 'virtual:uno.css';

// Optional: import reset
import '@unocss/reset/tailwind.css';

Fix 2: Use Utility Classes

With presetUno(), you get Tailwind-compatible utilities:

// All standard Tailwind classes work
function Card() {
  return (
    <div className="max-w-md mx-auto bg-white rounded-xl shadow-md overflow-hidden md:max-w-2xl">
      <div className="md:flex">
        <div className="p-8">
          <h2 className="text-2xl font-bold text-gray-900 mb-2">Card Title</h2>
          <p className="text-gray-600 leading-relaxed">
            Card content with UnoCSS utility classes.
          </p>
          <button className="mt-4 px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
            Action
          </button>
        </div>
      </div>
    </div>
  );
}

// Variant groups — group hover, focus, etc.
// Requires transformerVariantGroup
<button className="hover:(bg-blue-600 text-white scale-105) transition-all">
  Hover me
</button>

// Arbitrary values — same as Tailwind
<div className="w-[300px] h-[200px] bg-[#1a1a2e] text-[14px]">
  Custom values
</div>

// Dark mode
<div className="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
  Adapts to dark mode
</div>

Fix 3: Icons with preset-icons

# Install icon collections you need
npm install -D @iconify-json/mdi         # Material Design Icons (7000+ icons)
npm install -D @iconify-json/lucide      # Lucide icons
npm install -D @iconify-json/heroicons   # Heroicons
npm install -D @iconify-json/logos       # Technology logos
npm install -D @iconify-json/ph          # Phosphor icons

# Or install ALL iconify icons (large)
# npm install -D @iconify/json
// uno.config.ts
import { defineConfig, presetUno, presetIcons } from 'unocss';

export default defineConfig({
  presets: [
    presetUno(),
    presetIcons({
      scale: 1.2,
      extraProperties: {
        'display': 'inline-block',
        'vertical-align': 'middle',
      },
      // Custom icon collections
      collections: {
        custom: {
          // Inline SVG icons
          'circle': '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="currentColor"/></svg>',
        },
      },
    }),
  ],
});
// Usage — class name format: i-{collection}-{name}
function IconExamples() {
  return (
    <div className="flex gap-4 items-center">
      {/* Material Design Icons */}
      <span className="i-mdi-home text-2xl text-blue-500" />
      <span className="i-mdi-account text-2xl text-green-500" />
      <span className="i-mdi-settings text-2xl" />

      {/* Lucide icons */}
      <span className="i-lucide-search text-xl" />
      <span className="i-lucide-menu text-xl" />

      {/* Heroicons */}
      <span className="i-heroicons-bell-solid text-xl text-red-500" />

      {/* Technology logos */}
      <span className="i-logos-react text-3xl" />
      <span className="i-logos-vue text-3xl" />

      {/* Custom icon */}
      <span className="i-custom-circle text-xl text-purple-500" />

      {/* Icon button */}
      <button className="p-2 rounded-lg hover:bg-gray-100 transition-colors">
        <span className="i-mdi-delete text-xl text-red-500" />
      </button>
    </div>
  );
}

Fix 4: Shortcuts and Custom Rules

// uno.config.ts
import { defineConfig, presetUno } from 'unocss';

export default defineConfig({
  presets: [presetUno()],

  // Shortcuts — alias multiple classes to one name
  shortcuts: {
    // Static shortcut
    'btn': 'px-4 py-2 rounded-lg font-semibold inline-flex items-center justify-center transition-colors cursor-pointer',
    'btn-primary': 'btn bg-blue-500 text-white hover:bg-blue-600',
    'btn-secondary': 'btn bg-gray-200 text-gray-800 hover:bg-gray-300',
    'btn-danger': 'btn bg-red-500 text-white hover:bg-red-600',

    // Card shortcuts
    'card': 'bg-white rounded-xl shadow-md p-6 dark:bg-gray-800',
    'card-hover': 'card hover:shadow-lg transition-shadow',

    // Layout shortcuts
    'flex-center': 'flex items-center justify-center',
    'flex-between': 'flex items-center justify-between',

    // Dynamic shortcuts (regex)
    [/^badge-(.*)$/, ([, c]) => `px-2 py-0.5 rounded-full text-xs font-bold bg-${c}-100 text-${c}-800`],
  },

  // Custom rules — define your own utilities
  rules: [
    // Static rule
    ['custom-scrollbar', {
      'scrollbar-width': 'thin',
      'scrollbar-color': '#888 transparent',
    }],

    // Dynamic rule with regex
    [/^text-shadow-(.+)$/, ([, value]) => ({
      'text-shadow': value === 'sm' ? '0 1px 2px rgba(0,0,0,0.1)'
        : value === 'md' ? '0 2px 4px rgba(0,0,0,0.15)'
        : value === 'lg' ? '0 4px 8px rgba(0,0,0,0.2)'
        : undefined,
    })],

    // Grid auto-fill
    [/^grid-auto-fill-(\d+)$/, ([, minWidth]) => ({
      'display': 'grid',
      'grid-template-columns': `repeat(auto-fill, minmax(${minWidth}px, 1fr))`,
    })],
  ],

  // Theme overrides
  theme: {
    colors: {
      brand: {
        50: '#eff6ff',
        100: '#dbeafe',
        500: '#3b82f6',
        600: '#2563eb',
        900: '#1e3a5f',
      },
    },
    breakpoints: {
      sm: '640px',
      md: '768px',
      lg: '1024px',
      xl: '1280px',
    },
  },

  // Safelist — force these classes into the output even if not scanned
  safelist: [
    'text-red-500',
    'text-blue-500',
    'text-green-500',
    ...Array.from({ length: 10 }, (_, i) => `gap-${i}`),
  ],
});
// Usage
<button className="btn-primary">Submit</button>
<span className="badge-green">Active</span>
<div className="grid-auto-fill-250 gap-4">
  <div className="card-hover">Card 1</div>
  <div className="card-hover">Card 2</div>
</div>

Fix 5: Attributify Mode

Write utilities as HTML attributes instead of class strings:

// uno.config.ts
import { defineConfig, presetUno, presetAttributify } from 'unocss';

export default defineConfig({
  presets: [
    presetUno(),
    presetAttributify({
      prefix: 'un-',     // Optional: prefix to avoid conflicts (un-bg="blue-500")
      prefixedOnly: false, // Set true to require prefix on all attributes
    }),
  ],
});
// Attributify syntax — group by property
<div
  bg="blue-500 hover:blue-600 dark:blue-800"
  text="white sm lg:xl"
  p="4 md:6"
  m="2 auto"
  border="~ rounded-lg gray-200"
  flex="~ col md:row"
  gap="4"
>
  Styled with attributes
</div>

// Equivalent class-based syntax:
<div className="bg-blue-500 hover:bg-blue-600 dark:bg-blue-800 text-white text-sm lg:text-xl p-4 md:p-6 m-2 mx-auto border border-rounded-lg border-gray-200 flex flex-col md:flex-row gap-4">

TypeScript support for attributify:

// src/attributes.d.ts — or wherever your type declarations go
import type { AttributifyAttributes } from '@unocss/preset-attributify';

declare module 'react' {
  interface HTMLAttributes<T> extends AttributifyAttributes {}
}
// For Vue: declare module '@vue/runtime-dom' { ... }

Fix 6: Framework Integration

Next.js:

npm install -D @unocss/webpack
// next.config.mjs
import UnoCSS from '@unocss/webpack';

export default {
  webpack: (config) => {
    config.plugins.push(UnoCSS());
    return config;
  },
};
// app/layout.tsx
import '@unocss/reset/tailwind.css';
import 'uno.css';

Nuxt:

npm install -D @unocss/nuxt
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@unocss/nuxt'],
});

Astro:

npm install -D unocss
// astro.config.mjs
import { defineConfig } from 'astro/config';
import UnoCSS from 'unocss/astro';

export default defineConfig({
  integrations: [UnoCSS({ injectReset: true })],
});

Still Not Working?

“No CSS generated” warning — UnoCSS scans files based on the plugin’s content configuration. For Vite, it auto-detects files served by Vite. For webpack/Next.js, you may need to specify content.filesystem in uno.config.ts to point to your source files. Also verify that virtual:uno.css (Vite) or uno.css (webpack) is imported in your entry file.

Classes work in HTML but not in JSX — React uses className, not class. UnoCSS scans for both, but make sure your JSX uses className. Also check that string interpolation in template literals is handled: className={`text-${color}-500`} won’t be detected because UnoCSS can’t evaluate runtime expressions. Use safelist for dynamic classes.

Icons are invisible (element has correct class but no visual) — the icon collection package isn’t installed. i-mdi-home requires @iconify-json/mdi. Check the collection name matches the package: i-lucide-* needs @iconify-json/lucide, i-heroicons-* needs @iconify-json/heroicons. After installing, restart the dev server.

Attributify classes conflict with existing HTML attributes — if text="white" conflicts with a component’s text prop, use the prefix option: presetAttributify({ prefix: 'un-' }). Then use un-text="white" instead.

Classes from a monorepo package don’t generate — UnoCSS’s Vite plugin only scans files inside the current Vite root by default. If your components live in ../packages/ui/, they’re never scanned. Add content: { filesystem: ['../packages/ui/**/*.{tsx,vue,svelte}'] } to uno.config.ts and restart the dev server. The filesystem array uses glob patterns relative to the config file.

presetAttributify() works in dev but disappears in production build — the attributify transformer runs at build time. If you re-order plugins in vite.config.ts and place the framework plugin before UnoCSS, the framework will strip unknown attributes before UnoCSS sees them. Move UnoCSS() to the top of the plugins array.

Classes regenerate every save, slowing HMR — turn off content.pipeline.exclude: [] overrides and let UnoCSS use its default ignore list. Scanning node_modules is the usual cause. If you have icon-heavy SVG sprites being scanned by mistake, exclude them: content: { pipeline: { exclude: ['**/icons/sprites/**'] } }.

For related CSS utility issues, see Fix: Tailwind v4 Not Working, Fix: Panda CSS Not Working, Fix: vanilla-extract Not Working, and Fix: NativeWind Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles