iOS
SF Colors, platformColor(), ui-rounded, ui-serif
Android
System fonts, lightningcss, media queries @media android
Web
CSS nativo, light-dark(), PostCSS + @tailwindcss/postcss
1
Instalación de dependencias
Paquetes necesarios + resolución de lightningcss
IMPORTANTE — lightningcss 1.30.1
Fijar la resolución en package.json para evitar incompatibilidades de build. autoprefixer NO es necesario (lo gestiona lightningcss en Expo).
terminal
bash
# Instalar con el resolver de Expo para versiones correctas npx expo install tailwindcss@^4 \ nativewind@5.0.0-preview.2 \ react-native-css@0.0.0-nightly.5ce6396 \ @tailwindcss/postcss \ tailwind-merge \ clsx
package.json
json
{ "name": "cultiva-tools", "resolutions": { "lightningcss": "1.30.1" // ← forzar versión compatible } }
2
Archivos de configuración
Metro · PostCSS · CSS Global
Estructura de archivos nuevos / modificados
cultiva-tools/
├── metro.config.js ← modificar
├── postcss.config.mjs ← nuevo
├── src/
│ ├── global.css ← nuevo (imports TW)
│ ├── css/
│ │ └── sf.css ← colores iOS nativos
│ └── tw/
│ ├── index.tsx ← wrappers principales
│ ├── image.tsx ← wrapper Image
│ └── animated.tsx ← Animated.View
└── app/
└── ...
metro.config.js
js
const { getDefaultConfig } = require( "expo/metro-config" ); const { withNativewind } = require( "nativewind/metro" ); const config = getDefaultConfig( __dirname ); module.exports = withNativewind( config, { inlineVariables: false, globalClassNamePolyfill: false, } );
postcss.config.mjs
js
export default { plugins: { "@tailwindcss/postcss": {}, }, }; // ✓ NO autoprefixer necesario // lightningcss lo gestiona internamente
src/global.css
css
@import "tailwindcss/theme.css" layer(theme); @import "tailwindcss/preflight.css" layer(base); @import "tailwindcss/utilities.css"; @import "./css/sf.css"; /* Platform-specific font families */ @media android { :root { --font-mono: monospace; --font-sans: normal; } } @media ios { :root { --font-mono: ui-monospace; --font-sans: system-ui; --font-rounded: ui-rounded; } }
3
CSS Component Wrappers
src/tw/index.tsx — wrappers con useCssElement
¿Por qué wrappers?
react-native-css requiere que cada componente nativo esté envuelto con
useCssElement para mapear la prop className al sistema de estilos de React Native. Sin esto, las clases CSS no se aplican.
src/tw/index.tsx
tsx
import { useCssElement, useNativeVariable as useFunctionalVariable, } from "react-native-css"; import { Link as RouterLink } from "expo-router"; import { View as RNView, Text as RNText, Pressable as RNPressable, ScrollView as RNScrollView, TextInput as RNTextInput, } from "react-native"; // CSS Variable hook — nativo en RN, var() en web export const useCSSVariable = process.env.EXPO_OS !== "web" ? useFunctionalVariable : (variable: string) => `var(${variable})`; // ── Componentes envueltos ────────────────────── export const View = ( props: React.ComponentProps<typeof RNView> & { className?: string } ) => useCssElement(RNView, props, { className: "style" } ); export const Text = ( props: React.ComponentProps<typeof RNText> & { className?: string } ) => useCssElement(RNText, props, { className: "style" } ); export const ScrollView = ( props: React.ComponentProps<typeof RNScrollView> & { className?: string; contentContainerClassName?: string } ) => useCssElement(RNScrollView, props, { className: "style", contentContainerClassName: "contentContainerStyle", }); export const Pressable = ( props: React.ComponentProps<typeof RNPressable> & { className?: string } ) => useCssElement(RNPressable, props, { className: "style" } ); export const TextInput = ( props: React.ComponentProps<typeof RNTextInput> & { className?: string } ) => useCssElement(RNTextInput, props, { className: "style" } );
4
Apple SF Colors — iOS nativos + dark mode
src/css/sf.css — PlatformColor en iOS, light-dark() en Web/Android
src/css/sf.css
css
@layer base { html { color-scheme: light; } } /* ── Web + Android: light-dark() fallback ── */ :root { --sf-blue: light-dark(rgb(0 122 255), rgb(10 132 255)); --sf-green: light-dark(rgb(52 199 89), rgb(48 209 89)); --sf-red: light-dark(rgb(255 59 48), rgb(255 69 58)); --sf-text: light-dark(rgb(0 0 0), rgb(255 255 255)); --sf-bg: light-dark(rgb(255 255 255), rgb(0 0 0)); --sf-bg-2: light-dark(rgb(242 242 247), rgb(28 28 30)); } /* ── iOS: colores nativos del sistema ── */ @media ios { :root { --sf-blue: platformColor(systemBlue); --sf-green: platformColor(systemGreen); --sf-red: platformColor(systemRed); --sf-text: platformColor(label); --sf-bg: platformColor(systemBackground); --sf-bg-2: platformColor(secondarySystemBackground); } } /* ── Registrar como clases Tailwind ── */ @layer theme { @theme { --color-sf-blue: var(--sf-blue); --color-sf-green: var(--sf-green); --color-sf-red: var(--sf-red); --color-sf-text: var(--sf-text); --color-sf-bg: var(--sf-bg); --color-sf-bg-2: var(--sf-bg-2); } }
5
Migración desde NativeWind v4 + Tailwind v3
5 diferencias clave — guía antes/después
| Aspecto | v3 / NW v4 (antes) | v4 / NW v5 (ahora) |
|---|---|---|
| Config principal | tailwind.config.js | @theme {} en CSS |
| Babel config | nativewind/babel preset | NO babel.config.js |
| Imports CSS | @tailwind base/utilities | @import "tailwindcss/..." |
| PostCSS plugin | tailwindcss (v3) | @tailwindcss/postcss (v4) |
| Metro config | withNativewind() básico | inlineVariables: false |
| Componentes | className directo | useCssElement wrapper |
babel.config.js — eliminar bloque NativeWind
module.exports = function (api) {
api.cache(true);
return {
presets: [
["babel-preset-expo", { jsxImportSource: "nativewind" }],
"nativewind/babel",
],
};
};
// ✓ babel.config.js ya NO necesario para NativeWind v5
// La configuración es ahora 100% CSS-first vía PostCSS
6
Ejemplo real — Pantalla CULTIVA Tools
app/(tabs)/projects.tsx usando los wrappers
app/(tabs)/projects.tsx
tsx
import { View, Text, ScrollView, Pressable } from "@/tw"; import { Image } from "@/tw/image"; import { useCSSVariable } from "@/tw"; export default function ProjectsScreen() { const blue = useCSSVariable("--sf-blue"); return ( <ScrollView className="flex-1 bg-sf-bg"> <View className="p-6 gap-4"> <Text className="text-2xl font-bold text-sf-text"> Proyectos activos </Text> {PROJECTS.map(project => ( <Pressable key={project.id} className="bg-sf-bg-2 rounded-2xl p-4 active:opacity-70" > <Text className="font-semibold text-sf-text"> {project.name} </Text> <Text className="text-sm text-sf-text-2 mt-1"> {project.status} </Text> <View style={{ borderColor: blue }} className="mt-3 border rounded-full px-3 py-1 self-start" > <Text className="text-xs text-sf-blue"> {project.type} </Text> </View> </Pressable> ))} </View> </ScrollView> ); }
!
Troubleshooting frecuente
Problemas habituales y sus soluciones
Estilos no se aplican
1. Verificar que global.css está importado en el entry point de la app
2. Comprobar que el componente usa el wrapper de
3. Revisar que Metro tiene
2. Comprobar que el componente usa el wrapper de
src/tw/, no el nativo directamente3. Revisar que Metro tiene
withNativewind(config, { inlineVariables: false })
PlatformColor no funciona en iOS
Usar
En Android/Web usar siempre el fallback
platformColor() SOLO dentro de bloques @media ios.En Android/Web usar siempre el fallback
light-dark().
Errores TypeScript con className
Extender los tipos del componente:
type Props = React.ComponentProps<typeof RNView> & { className?: string }