Claude Agent Skill · by Uni Stack

Migrate Nativewind To Uniwind

Install Migrate Nativewind To Uniwind skill for Claude Code from uni-stack/uniwind.

Install
Terminal · npx
$npx skills add https://github.com/uni-stack/uniwind --skill migrate-nativewind-to-uniwind
Works with Paperclip

How Migrate Nativewind To Uniwind fits into a Paperclip company.

Migrate Nativewind To Uniwind drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.

S
SaaS FactoryPaired

Pre-configured AI company — 18 agents, 18 skills, one-time purchase.

$27$59
Explore pack
Source file
SKILL.md725 lines
Expand
---name: migrate-nativewind-to-uniwinddescription: >  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):```bashnpm uninstall nativewind react-native-css-interop# oryarn remove nativewind react-native-css-interop# orbun 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:```bashrg "react-native-css-interop" -g "*.{ts,tsx,js,jsx}"``` Remove every import and usage found. ## Step 2: Install Uniwind and Tailwind 4 ```bashnpm install uniwind tailwindcss@latest# oryarn add uniwind tailwindcss@latest# orbun add uniwind tailwindcss@latest``` Ensure `tailwindcss` is version 4+. ## Step 3: Update babel.config.js Remove the NativeWind babel preset:```js// 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):**```jsconst { withNativeWind } = require('nativewind/metro');module.exports = withNativeWind(config, { input: './global.css' });``` **After (Uniwind):**```jsconst { 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. ```js// ❌ BrokencssEntryFile: path.resolve(__dirname, 'app', 'global.css') // ✅ CorrectcssEntryFile: './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: ```jsmodule.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:**```css@tailwind base;@tailwind components;@tailwind utilities;``` **After:**```css@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):**```jsmodule.exports = {  theme: {    extend: {      colors: {        primary: '#00a8ff',        secondary: '#273c75',      },      fontFamily: {        normal: ['Roboto-Regular'],        bold: ['Roboto-Bold'],      },    },  },};``` **After (global.css):**```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: ```bashrg "cssInterop|remapProps" -g "*.{ts,tsx,js,jsx}"``` Replace every `cssInterop()` / `remapProps()` call with Uniwind's `withUniwind()`: **Before (NativeWind):**```tsximport { cssInterop } from 'react-native-css-interop';import { Image } from 'expo-image'; cssInterop(Image, { className: 'style' });``` **After (Uniwind):**```tsximport { withUniwind } from 'uniwind';import { Image as ExpoImage } from 'expo-image'; export const Image = withUniwind(ExpoImage);``` `withUniwind` automatically maps `className` → `style` and other common props. For custom prop mappings: ```tsxconst 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:  ```tsx  // 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:  ```tsx  // 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:  ```tsx  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: ```tsx// 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()`):**```tsximport { 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):**```css@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 used nested theme wrappers to preview or force a theme for a specific subtree (for example a demo card, settings preview, or side-by-side theme comparison), use Uniwind Pro's `ScopedTheme` instead of changing the global theme:```tsximport { ScopedTheme } from 'uniwind'; <ScopedTheme theme="dark">  <PreviewCard /></ScopedTheme>``` If the project has **custom themes beyond light/dark** (e.g. `ocean`, `premium`), you must:1. Define them in CSS using `@variant`:```css@layer theme {  :root {    @variant ocean {      --color-primary: #0ea5e9;      --color-background: #0c4a6e;    }  }}```2. Register them in metro.config.js via `extraThemes` (skip `light`/`dark` — they are auto-added):```jsmodule.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: ```tsximport { 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):```bashrg "export function cn|export const cn" -g "*.{ts,tsx,js}"``` If it exists, keep it as-is. If not, install dependencies and create it: ```bashnpm install tailwind-merge clsx``` Create `lib/cn.ts` (or wherever utils live in the project):```tsimport { type ClassValue, clsx } from 'clsx';import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) {    return twMerge(clsx(inputs));}``` Usage:```tsximport { 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:- https://docs.uniwind.dev/migration-from-nativewind ## Step 15: Clean Up Remaining NativeWind References Final sweep — search for and remove any remaining references: ```bashrg "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 ```tsximport { 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:```tsximport { Uniwind } from 'uniwind'; Uniwind.currentTheme    // "light", "dark", "system", or customUniwind.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): ```tsximport { 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`: ```tsximport { 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()`. These can be used everywhere (custom CSS classes, `@utility`, etc.) — but NOT inside `@theme {}` (which only accepts static values). Use `@utility` to create reusable Tailwind-style classes: ```css@utility w-hairline { width: hairlineWidth(); }@utility h-hairline { height: hairlineWidth(); }@utility border-hairline { border-width: hairlineWidth(); }@utility text-scaled { font-size: fontScale(); }``` Then use as: `<View className="w-hairline h-hairline" />` ### Platform Selectors Docs: https://docs.uniwind.dev/api/platform-select Apply styles conditionally per platform using `ios:`, `android:`, `web:`, `native:` prefixes: ```tsx<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: ```tsximport { Uniwind } from 'uniwind'; Uniwind.setTheme('dark');     // force darkUniwind.setTheme('light');    // force lightUniwind.setTheme('system');   // follow system (default)Uniwind.setTheme('ocean');    // custom theme (must be in extraThemes)``` ### ScopedTheme — Theme a Subtree Only Docs: https://docs.uniwind.dev/api/scoped-themes Use `ScopedTheme` when the project needs a different theme for only part of the UI (component previews, themed sections, nested demos) without changing the app-wide theme: ```tsximport { ScopedTheme } from 'uniwind'; <View className="gap-3">  <PreviewCard />   <ScopedTheme theme="light">    <PreviewCard />  </ScopedTheme>   <ScopedTheme theme="dark">    <PreviewCard />  </ScopedTheme></View>``` Important behavior:- Nearest `ScopedTheme` wins (nested scopes are supported)- Hooks like `useUniwind`, `useResolveClassNames`, and `useCSSVariable` resolve against the nearest scoped theme- `withUniwind`-wrapped third-party components inside the scope also resolve themed values from that scope- Custom theme names can be used in `ScopedTheme` (must be defined 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:```tsx// Avoid this pattern<View className="light:bg-white dark:bg-black" />``` Define a CSS variable and use it directly:```css@layer theme {  :root {    @variant light { --color-background: #ffffff; }    @variant dark { --color-background: #000000; }  }}``````tsx// 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: ```tsximport { Uniwind } from 'uniwind'; // Preconfigure theme based on user input or API responseUniwind.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: ```tsximport { 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): ```css@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 `@variant` for per-platform fonts (must be inside `@layer theme { :root { } }`):```css@layer theme {  :root {    @variant ios { --font-sans: 'SF Pro Text'; }    @variant android { --font-sans: 'Roboto-Regular'; }    @variant web { --font-sans: 'system-ui'; }  }}``` **Data Selectors**: Use `data-[prop=value]:utility` for prop-based styling. Only equality checks supported:```tsx<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:```jsconfig.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 Android3. Theme switching works (light/dark)4. Custom fonts load correctly5. Safe area insets apply properly6. No console warnings about missing styles7. 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](https://docs.uniwind.dev/llms-full.txt)