migrate-nativewind-to-uniwind

star 1

Migrate a React Native project from NativeWind to Uniwind. Use when the user wants to replace NativeWind with Uniwind, upgrade from NativeWind, switch to Uniwind, or mentions NativeWind-to-Uniwind migration. Handles package removal, config migration, Tailwind 4 upgrade, cssInterop removal, theme conversion, and all breaking changes.

involvex By involvex schedule Updated 4/22/2026

name: migrate-nativewind-to-uniwind

description: >

Migrate a React Native project from NativeWind to Uniwind. Use when the user wants to

replace NativeWind with Uniwind, upgrade from NativeWind, switch to Uniwind, or mentions

NativeWind-to-Uniwind migration. Handles package removal, config migration, Tailwind 4

upgrade, cssInterop removal, theme conversion, and all breaking changes.


Migrate NativeWind to Uniwind

Uniwind replaces NativeWind with better performance and stability. It requires Tailwind CSS 4 and uses CSS-based theming instead of JS config.

Pre-Migration Checklist

Before starting, read the project's existing config files to understand the current setup:

  • package.json (NativeWind version, dependencies)

  • tailwind.config.js / tailwind.config.ts

  • metro.config.js

  • babel.config.js

  • global.css or equivalent CSS entry file

  • nativewind-env.d.ts or nativewind.d.ts

  • Any file using cssInterop or remapProps from nativewind

  • Any file importing from react-native-css-interop

  • Any ThemeProvider from NativeWind (vars() usage)

Step 1: Remove NativeWind and Related Packages

Uninstall ALL of these packages (if present):


npm uninstall nativewind react-native-css-interop

# or

yarn remove nativewind react-native-css-interop

# or

bun remove nativewind react-native-css-interop

CRITICAL: react-native-css-interop is a NativeWind dependency that must be removed. It is commonly missed during migration. Search the entire codebase for any imports from it:


rg "react-native-css-interop" -g "*.{ts,tsx,js,jsx}"

Remove every import and usage found.

Step 2: Install Uniwind and Tailwind 4


npm install uniwind tailwindcss@latest

# or

yarn add uniwind tailwindcss@latest

# or

bun add uniwind tailwindcss@latest

Ensure tailwindcss is version 4+.

Step 3: Update babel.config.js

Remove the NativeWind babel preset:


// REMOVE this line from presets array:

// 'nativewind/babel'

No Uniwind babel preset is needed.

Step 4: Update metro.config.js

Replace NativeWind's metro config with Uniwind's. withUniwindConfig must be the outermost wrapper.

Before (NativeWind):


const { withNativeWind } = require('nativewind/metro');

module.exports = withNativeWind(config, { input: './global.css' });

After (Uniwind):


const { getDefaultConfig } = require('expo/metro-config');

// For bare RN: const { getDefaultConfig } = require('@react-native/metro-config');

const { withUniwindConfig } = require('uniwind/metro');



const config = getDefaultConfig(__dirname);



module.exports = withUniwindConfig(config, {

  cssEntryFile: './global.css',

  polyfills: { rem: 14 },

});

cssEntryFile must be a relative path string from project root (e.g. ./global.css or ./app/global.css).

Do not use absolute paths or path.resolve(...) / path.join(...) for this option.


// ❌ Broken

cssEntryFile: path.resolve(__dirname, 'app', 'global.css')



// ✅ Correct

cssEntryFile: './app/global.css'

Always set polyfills.rem to 14 to match NativeWind's default rem value and prevent spacing/sizing differences after migration.

If the project uses custom themes beyond light/dark (e.g. defined via NativeWind's vars() or a custom ThemeProvider), register them with extraThemes. Do NOT include light or dark — they are added automatically:


module.exports = withUniwindConfig(config, {

  cssEntryFile: './global.css',

  polyfills: { rem: 14 },

  extraThemes: ['ocean', 'sunset', 'premium'],

});

Options:

  • cssEntryFile (required): relative path string to CSS entry file (from project root)

  • polyfills.rem (required for migration): set to 14 to match NativeWind's rem base

  • extraThemes (required if project has custom themes): array of custom theme names — do NOT include light/dark

  • dtsFile (optional): path for generated TypeScript types, defaults to ./uniwind-types.d.ts

  • debug (optional): log unsupported CSS properties during dev

Step 5: Update global.css

Replace NativeWind's Tailwind 3 directives with Tailwind 4 imports:

Before:


@tailwind base;

@tailwind components;

@tailwind utilities;

After:


@import 'tailwindcss';

@import 'uniwind';

Step 6: Update CSS Entry Import

Ensure global.css is imported in your main App component (e.g., App.tsx), NOT in the root index.ts/index.js where you register the app — importing there breaks hot reload.

Step 7: Delete NativeWind Type Definitions

Delete nativewind-env.d.ts or nativewind.d.ts. Uniwind auto-generates its own types at the path specified by dtsFile.

Step 8: Delete tailwind.config.js

Remove tailwind.config.js / tailwind.config.ts entirely. All theme config moves to CSS using Tailwind 4's @theme directive.

Migrate custom theme values to global.css:

Before (tailwind.config.js):


module.exports = {

  theme: {

    extend: {

      colors: {

        primary: '#00a8ff',

        secondary: '#273c75',

      },

      fontFamily: {

        normal: ['Roboto-Regular'],

        bold: ['Roboto-Bold'],

      },

    },

  },

};

After (global.css):


@import 'tailwindcss';

@import 'uniwind';



@theme {

  --color-primary: #00a8ff;

  --color-secondary: #273c75;

  --font-normal: 'Roboto-Regular';

  --font-bold: 'Roboto-Bold';

}

Font families must specify a single font — React Native doesn't support font fallbacks.

Step 9: Remove ALL cssInterop and remapProps Usage

This is the most commonly missed step. Search the entire codebase:


rg "cssInterop|remapProps" -g "*.{ts,tsx,js,jsx}"

Replace every cssInterop() / remapProps() call with Uniwind's withUniwind():

Before (NativeWind):


import { cssInterop } from 'react-native-css-interop';

import { Image } from 'expo-image';



cssInterop(Image, { className: 'style' });

After (Uniwind):


import { withUniwind } from 'uniwind';

import { Image as ExpoImage } from 'expo-image';



export const Image = withUniwind(ExpoImage);

withUniwind automatically maps classNamestyle and other common props. For custom prop mappings:


const StyledProgressBar = withUniwind(ProgressBar, {

  width: {

    fromClassName: 'widthClassName',

    styleProperty: 'width',

  },

});

Define wrapped components at module level (not inside render functions). Each component should only be wrapped once:

  • Used in one file only — define the wrapped component in that same file:

    
    // screens/ProfileScreen.tsx
    
    import { withUniwind } from 'uniwind';
    
    import { BlurView as RNBlurView } from '@react-native-community/blur';
    
    
    
    const BlurView = withUniwind(RNBlurView);
    
    
    
    export function ProfileScreen() {
    
      return <BlurView className="flex-1" />;
    
    }
    
  • Used across multiple files — wrap once in a shared module and re-export:

    
    // components/styled.ts
    
    import { withUniwind } from 'uniwind';
    
    import { Image as ExpoImage } from 'expo-image';
    
    import { LinearGradient as RNLinearGradient } from 'expo-linear-gradient';
    
    
    
    export const Image = withUniwind(ExpoImage);
    
    export const LinearGradient = withUniwind(RNLinearGradient);
    

    Then import from the shared module everywhere:

    
    import { Image, LinearGradient } from '@/components/styled';
    

Never call withUniwind on the same component in multiple files — wrap once, import everywhere.

IMPORTANT: Do NOT wrap components from react-native or react-native-reanimated with withUniwind — they already support className out of the box. This includes View, Text, Image, ScrollView, FlatList, Pressable, TextInput, Animated.View, etc. Only use withUniwind for third-party components (e.g. expo-image, expo-linear-gradient, @react-native-community/blur).

IMPORTANT — accent- prefix for non-style color props: React Native components have props like color, tintColor, backgroundColor that are NOT part of the style object. To set these via Tailwind classes, use the accent- prefix with the corresponding *ClassName prop:


// color prop → colorClassName with accent- prefix

<ActivityIndicator

    className="m-4"

    size="large"

    colorClassName="accent-blue-500 dark:accent-blue-400"

/>



// color prop on Button

<Button

    colorClassName="accent-background"

    title="Press me"

/>



// tintColor prop → tintColorClassName with accent- prefix

<Image

    className="w-6 h-6"

    tintColorClassName="accent-red-500"

    source={icon}

/>

Rule: className accepts any Tailwind utility for style-based props. For non-style props (color, tintColor, etc.), use {propName}ClassName with the accent- prefix. This applies to all built-in React Native components.

Step 10: Migrate NativeWind Theme Variables

Before (NativeWind JS themes with vars()):


import { vars } from 'nativewind';



export const themes = {

  light: vars({

    '--color-primary': '#00a8ff',

    '--color-typography': '#000',

  }),

  dark: vars({

    '--color-primary': '#273c75',

    '--color-typography': '#fff',

  }),

};



// In JSX:

<View style={themes[colorScheme]}>

After (Uniwind CSS themes):


@layer theme {

  :root {

    @variant light {

      --color-primary: #00a8ff;

      --color-typography: #000;

    }

    @variant dark {

      --color-primary: #273c75;

      --color-typography: #fff;

    }

  }

}

IMPORTANT: All theme variants must define the exact same set of CSS variables. If light defines --color-primary and --color-typography, then dark (and any custom theme) must also define both. Mismatched variables will cause a Uniwind runtime error.

No ThemeProvider wrapper needed. Remove the NativeWind <ThemeProvider> or vars() wrapper from JSX. Keep React Navigation's <ThemeProvider> if used.

If the project has custom themes beyond light/dark (e.g. ocean, premium), you must:

  1. Define them in CSS using @variant:

@layer theme {

  :root {

    @variant ocean {

      --color-primary: #0ea5e9;

      --color-background: #0c4a6e;

    }

  }

}
  1. Register them in metro.config.js via extraThemes (skip light/dark — they are auto-added):

module.exports = withUniwindConfig(config, {

  cssEntryFile: './global.css',

  polyfills: { rem: 14 },

  extraThemes: ['ocean', 'premium'],

});

Step 11: Migrate Safe Area Utilities

NativeWind's safe area classes need explicit setup in Uniwind:


import { SafeAreaProvider, SafeAreaListener } from 'react-native-safe-area-context';

import { Uniwind } from 'uniwind';



export default function App() {

  return (

    <SafeAreaProvider>

      <SafeAreaListener

        onChange={({ insets }) => {

          Uniwind.updateInsets(insets);

        }}

      >

        <View className="pt-safe px-safe">

          {/* content */}

        </View>

      </SafeAreaListener>

    </SafeAreaProvider>

  );

}

Step 12: Verify rem Value

NativeWind uses 14px as the base rem, Uniwind defaults to 16px. Step 4 already sets polyfills: { rem: 14 } in metro config to preserve NativeWind's spacing. If the user explicitly wants Uniwind's default (16px), they can remove the polyfill — but warn them that all spacing/sizing will shift.

Step 13: Handle className Deduplication

Uniwind does NOT auto-deduplicate conflicting classNames (NativeWind did). If your codebase relies on override patterns like className={`p-4 ${overrideClass}`}, set up a cn utility.

First, check if the project already has a cn helper (common in shadcn/ui projects):


rg "export function cn|export const cn" -g "*.{ts,tsx,js}"

If it exists, keep it as-is. If not, install dependencies and create it:


npm install tailwind-merge clsx

Create lib/cn.ts (or wherever utils live in the project):


import { type ClassValue, clsx } from 'clsx';

import { twMerge } from 'tailwind-merge';



export function cn(...inputs: ClassValue[]) {

    return twMerge(clsx(inputs));

}

Usage:


import { cn } from '@/lib/cn';



<View className={cn('p-4 bg-white', props.className)} />

<Text className={cn('text-base', isActive && 'text-blue-500', disabled && 'opacity-50')} />

Use cn instead of raw twMerge — it handles conditional classes, arrays, and falsy values via clsx before deduplicating with tailwind-merge.

Step 14: Update Animated Class Names

If the project used NativeWind animated-* / transition class patterns, migrate those to explicit react-native-reanimated usage. Uniwind OSS does not provide NativeWind-style animated class behavior.

Use this migration guide section as the source of truth:

Step 15: Clean Up Remaining NativeWind References

Final sweep — search for and remove any remaining references:


rg "nativewind|NativeWind|native-wind" -g "*.{ts,tsx,js,jsx,json,css}"

Check for:

  • NativeWind imports in any file

  • nativewind in package.json (devDependencies too)

  • react-native-css-interop in package.json

  • NativeWind babel preset in babel.config.js

  • NativeWind metro wrapper in metro.config.js

  • nativewind-env.d.ts or nativewind.d.ts files

  • Any cssInterop() or remapProps() calls

  • Any vars() imports from nativewind

Uniwind APIs & Patterns

useUniwind — Theme Access (re-renders on change)

Docs: https://docs.uniwind.dev/api/use-uniwind


import { useUniwind } from 'uniwind';



const { theme, hasAdaptiveThemes } = useUniwind();

// theme: current theme name — "light", "dark", "system", or custom

// hasAdaptiveThemes: true if app follows system color scheme

Use for: displaying theme name in UI, conditional rendering by theme, side effects on theme change.

Uniwind Static API — Theme Access (no re-render)

Access theme info without causing re-renders:


import { Uniwind } from 'uniwind';



Uniwind.currentTheme    // "light", "dark", "system", or custom

Uniwind.hasAdaptiveThemes // true if following system color scheme

Use for: logging, analytics, imperative logic outside render.

useResolveClassNames — Convert classNames to Style Objects

Docs: https://docs.uniwind.dev/api/use-resolve-class-names

Converts Tailwind classes into React Native style objects. Use when working with components that don't support className and can't be wrapped with withUniwind (e.g. react-navigation theme config):


import { useResolveClassNames } from 'uniwind';



const headerStyle = useResolveClassNames('bg-blue-500');

const cardStyle = useResolveClassNames('bg-white dark:bg-gray-900');



<Stack.Navigator

  screenOptions={{

    headerStyle: headerStyle,

    cardStyle: cardStyle,

  }}

/>

useCSSVariable — Access CSS Variables in JS

Docs: https://docs.uniwind.dev/api/use-css-variable

Retrieve CSS variable values programmatically. Variable must be prefixed with -- and match a variable defined in global.css:


import { useCSSVariable } from 'uniwind';



const primaryColor = useCSSVariable('--color-primary');

const spacing = useCSSVariable('--spacing-4');

Use for: animations, third-party library configs, calculations with design tokens.

CSS Functions — Custom Utilities

Docs: https://docs.uniwind.dev/api/css-functions

Define custom utilities using device-aware CSS functions like hairlineWidth(), fontScale(), pixelRatio():


@theme {

  --width-hairline: hairlineWidth();

}

Then use as: <View className="w-hairline" />

Platform Selectors

Docs: https://docs.uniwind.dev/api/platform-select

Apply styles conditionally per platform using ios:, android:, web:, native: prefixes:


<View className="ios:bg-red-500 android:bg-blue-500 web:bg-green-500">

  <Text className="ios:text-white android:text-white web:text-black">

    Platform-specific styles

  </Text>

</View>

Theme Switching

Docs: https://docs.uniwind.dev/theming/basics

By default Uniwind follows the system color scheme (adaptive themes). To switch themes programmatically:


import { Uniwind } from 'uniwind';



Uniwind.setTheme('dark');     // force dark

Uniwind.setTheme('light');    // force light

Uniwind.setTheme('system');   // follow system (default)

Uniwind.setTheme('ocean');    // custom theme (must be in extraThemes)

Style Based on Themes — Prefer CSS Variables

Docs: https://docs.uniwind.dev/theming/style-based-on-themes

Prefer using CSS variable-based classes over explicit dark:/light: variants. Instead of:


// Avoid this pattern

<View className="light:bg-white dark:bg-black" />

Define a CSS variable and use it directly:


@layer theme {

  :root {

    @variant light { --color-background: #ffffff; }

    @variant dark { --color-background: #000000; }

  }

}

// Preferred — automatically adapts to theme

<View className="bg-background" />

This is cleaner, easier to maintain, and works automatically with custom themes too.

Runtime CSS Variable Updates

Docs: https://docs.uniwind.dev/theming/update-css-variables

Update theme variables at runtime, e.g. based on user preferences or API responses:


import { Uniwind } from 'uniwind';



// Preconfigure theme based on user input or API response

Uniwind.updateCSSVariables('light', {

  '--color-primary': '#ff6600',

  '--color-background': '#1a1a2e',

});

This pattern should be used only when the app has real runtime theming needs (for example, user-selected brand colors or API-driven themes).

Variants with tailwind-variants

Docs: https://docs.uniwind.dev/tailwind-basics#advanced-pattern-variants-and-compound-variants

For component variants and compound variants, use the tailwind-variants library:


import { tv } from 'tailwind-variants';



const button = tv({

  base: 'px-4 py-2 rounded-lg',

  variants: {

    color: {

      primary: 'bg-primary text-white',

      secondary: 'bg-secondary text-white',

    },

    size: {

      sm: 'text-sm',

      lg: 'text-lg px-6 py-3',

    },

  },

});



<Pressable className={button({ color: 'primary', size: 'lg' })} />

Monorepo Support

Docs: https://docs.uniwind.dev/monorepos

If the project is a monorepo, add @source directives in global.css so Tailwind scans packages outside the CSS entry file's directory (only if that directory has components with Tailwind classes):


@import 'tailwindcss';

@import 'uniwind';

@source "../../packages/ui/src";

@source "../../packages/shared/src";

FAQ

Docs: https://docs.uniwind.dev/faq

Custom Fonts: Uniwind maps className to font-family only — font files must be loaded separately (expo-font plugin in app.json or react-native-asset for bare RN). Font family names in @theme must exactly match filenames (without extension). Use platform media queries for per-platform fonts:


@media ios { --font-sans: 'SF Pro Text'; }

@media android { --font-sans: 'Roboto-Regular'; }

Data Selectors: Use data-[prop=value]:utility for prop-based styling. Only equality checks supported:


<View data-state={isOpen ? 'open' : 'closed'} className="data-[state=open]:bg-muted/50" />

global.css Location in Expo Router: Place at project root and import in root layout (app/_layout.tsx). If placed in app/, components outside need @source directives. Tailwind scans from global.css location.

Full App Reloads on CSS Changes: Metro can't hot-reload files with many providers. Move global.css import deeper in the component tree (e.g. navigation root or home screen) to fix.

Gradients: Built-in support, no extra deps needed. Use bg-gradient-to-r from-red-500 via-yellow-500 to-green-500. For expo-linear-gradient, use useCSSVariable to get colors — withUniwind won't work since gradient props are arrays.

Style Specificity: Inline style always overrides className. Use className for static styles, inline only for truly dynamic values. Avoid mixing both for the same property.

Serialization Errors (Failed to serialize javascript object): Clear caches: watchman watch-del-all 2>/dev/null; rm -rf node_modules/.cache && npx expo start --clear. Common causes: complex @theme configs, circular CSS variable references.

Metro unstable_enablePackageExports Conflicts: Some apps (crypto etc.) disable this, breaking Uniwind. Use selective resolver:


config.resolver.unstable_enablePackageExports = false;

config.resolver.resolveRequest = (context, moduleName, platform) => {

  if (['uniwind', 'culori'].some((prefix) => moduleName.startsWith(prefix))) {

    return context.resolveRequest({ ...context, unstable_enablePackageExports: true }, moduleName, platform);

  }

  return context.resolveRequest(context, moduleName, platform);

};

Safe Area Classes: p-safe, pt-safe, pb-safe, px-safe, py-safe, m-safe, mt-safe, etc. Also supports -or-{value} (min spacing) and -offset-{value} (extra spacing) variants.

Next.js: Not officially supported. Uniwind is for Metro and Vite. Community plugin: uniwind-plugin-next. For Next.js, use standard Tailwind CSS and share design tokens.

Vite: Supported since v1.2.0. Use uniwind/vite plugin alongside @tailwindcss/vite.

UI Kits: HeroUI Native, react-native-reusables and Gluestack 4.1+ works great with Uniwind

Known Issues & Gotchas

  1. data- attributes*: Uniwind supports data-[prop=value]:utility syntax for conditional styling, similar to NativeWind.

  2. Animated styles: Migrate NativeWind animated classes to react-native-reanimated directly. Uniwind Pro has built-in Reanimated support.

Verification

After migration, verify:

  1. npx react-native start --reset-cache (clear Metro cache) or with expo npx expo start -c

  2. All screens render correctly on iOS and Android

  3. Theme switching works (light/dark)

  4. Custom fonts load correctly

  5. Safe area insets apply properly

  6. No console warnings about missing styles

  7. No remaining imports from nativewind or react-native-css-interop

IMPORTANT: Do NOT guess Uniwind APIs. If you are unsure about any Uniwind API, hook, component, or configuration option, fetch and verify against the official docs: https://docs.uniwind.dev/llms-full.txt

Install via CLI
npx skills add https://github.com/involvex/syncstuff --skill migrate-nativewind-to-uniwind
Repository Details
star Stars 1
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator