Communitygithub.com

Samueladewole/claude-skills

Configurar Tailwind CSS v4 en Expo con react-native-css y NativeWind v5 para estilos universales

¿Qué es claude-skills?

Esta guía detalla la configuración de Tailwind CSS v4 en proyectos Expo modernos utilizando react-native-css y NativeWind v5. Permite aplicar estilos universales con la potencia de Tailwind v4 en plataformas iOS, Android y Web. La configuración es CSS-first, eliminando la necesidad del babel.config.js tradicional, y utiliza el plugin PostCSS `@tailwindcss/postcss` junto con importaciones CSS (`@import "tailwindcss/..."`). En lugar de los wrappers automáticos de NativeWind v4, ahora debes usar `useCssElement` de react-native-css en componentes como View, Text, Image y ScrollView. Además, se incluyen ejemplos para variables de tema personalizadas (Apple SF Colors, fuentes) y estilos específicos por plataforma mediante media queries como `@media ios` y `@media android`. Este setup es ideal para desarrolladores que buscan escalar sus aplicaciones Expo con Tailwind v4 manteniendo rendimiento nativo.

Compatible con✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Samueladewole/claude-skills/tree/HEAD/expo-tailwind-setup

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Configuración de Tailwind CSS para Expo con react-native-css

Esta guía cubre la configuración de Tailwind CSS v4 en Expo usando react-native-css y NativeWind v5 para estilos universales en iOS, Android y Web.

Resumen

Esta configuración utiliza:

  • Tailwind CSS v4 - Configuración moderna CSS-first
  • react-native-css - Entorno de ejecución CSS para React Native
  • NativeWind v5 - Transformador Metro para Tailwind en React Native
  • @tailwindcss/postcss - Plugin PostCSS para Tailwind v4

Instalación

# Instalar dependencias
npx expo install tailwindcss@^4 [email protected] [email protected] @tailwindcss/postcss tailwind-merge clsx

Agrega resoluciones para compatibilidad con lightningcss:

// package.json
{
  "resolutions": {
    "lightningcss": "1.30.1"
  }
}
  • autoprefixer no es necesario en Expo debido a lightningcss
  • postcss está incluido por defecto en expo

Archivos de Configuración

Configuración de Metro

Crea o actualiza metro.config.js:

// metro.config.js
const { getDefaultConfig } = require("expo/metro-config");
const { withNativewind } = require("nativewind/metro");

/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);

module.exports = withNativewind(config, {
  // las variables inline rompen PlatformColor en variables CSS
  inlineVariables: false,
  // Agregamos el soporte de className manualmente
  globalClassNamePolyfill: false,
});

Configuración de PostCSS

Crea postcss.config.mjs:

// postcss.config.mjs
export default {
  plugins: {
    "@tailwindcss/postcss": {},
  },
};

CSS Global

Crea src/global.css:

@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/preflight.css" layer(base);
@import "tailwindcss/utilities.css";

/* Familias de fuente específicas para cada plataforma */
@media android {
  :root {
    --font-mono: monospace;
    --font-rounded: normal;
    --font-serif: serif;
    --font-sans: normal;
  }
}

@media ios {
  :root {
    --font-mono: ui-monospace;
    --font-serif: ui-serif;
    --font-sans: system-ui;
    --font-rounded: ui-rounded;
  }
}

IMPORTANTE: No se necesita Configuración de Babel

Con Tailwind v4 y NativeWind v5, NO necesitas un babel.config.js para Tailwind. Elimina cualquier preset de NativeWind babel si está presente:

// BORRA babel.config.js si solo contiene la configuración de NativeWind
// Lo siguiente YA NO es necesario:
// module.exports = function (api) {
//   api.cache(true);
//   return {
//     presets: [
//       ["babel-preset-expo", { jsxImportSource: "nativewind" }],
//       "nativewind/babel",
//     ],
//   };
// };

Wrappers de Componentes CSS

Dado que react-native-css requiere un envoltorio explícito de elementos CSS, crea componentes reutilizables:

Componentes Principales (src/tw/index.tsx)

import {
  useCssElement,
  useNativeVariable as useFunctionalVariable,
} from "react-native-css";

import { Link as RouterLink } from "expo-router";
import Animated from "react-native-reanimated";
import React from "react";
import {
  View as RNView,
  Text as RNText,
  Pressable as RNPressable,
  ScrollView as RNScrollView,
  TouchableHighlight as RNTouchableHighlight,
  TextInput as RNTextInput,
  StyleSheet,
} from "react-native";

// Link habilitado para CSS
export const Link = (
  props: React.ComponentProps<typeof RouterLink> & { className?: string }
) => {
  return useCssElement(RouterLink, props, { className: "style" });
};

Link.Trigger = RouterLink.Trigger;
Link.Menu = RouterLink.Menu;
Link.MenuAction = RouterLink.MenuAction;
Link.Preview = RouterLink.Preview;

// Hook de variable CSS
export const useCSSVariable =
  process.env.EXPO_OS !== "web"
    ? useFunctionalVariable
    : (variable: string) => `var(${variable})`;

// View
export type ViewProps = React.ComponentProps<typeof RNView> & {
  className?: string;
};

export const View = (props: ViewProps) => {
  return useCssElement(RNView, props, { className: "style" });
};
View.displayName = "CSS(View)";

// Text
export const Text = (
  props: React.ComponentProps<typeof RNText> & { className?: string }
) => {
  return useCssElement(RNText, props, { className: "style" });
};
Text.displayName = "CSS(Text)";

// ScrollView
export const ScrollView = (
  props: React.ComponentProps<typeof RNScrollView> & {
    className?: string;
    contentContainerClassName?: string;
  }
) => {
  return useCssElement(RNScrollView, props, {
    className: "style",
    contentContainerClassName: "contentContainerStyle",
  });
};
ScrollView.displayName = "CSS(ScrollView)";

// Pressable
export const Pressable = (
  props: React.ComponentProps<typeof RNPressable> & { className?: string }
) => {
  return useCssElement(RNPressable, props, { className: "style" });
};
Pressable.displayName = "CSS(Pressable)";

// TextInput
export const TextInput = (
  props: React.ComponentProps<typeof RNTextInput> & { className?: string }
) => {
  return useCssElement(RNTextInput, props, { className: "style" });
};
TextInput.displayName = "CSS(TextInput)";

// AnimatedScrollView
export const AnimatedScrollView = (
  props: React.ComponentProps<typeof Animated.ScrollView> & {
    className?: string;
    contentClassName?: string;
    contentContainerClassName?: string;
  }
) => {
  return useCssElement(Animated.ScrollView, props, {
    className: "style",
    contentClassName: "contentContainerStyle",
    contentContainerClassName: "contentContainerStyle",
  });
};

// TouchableHighlight con extracción de underlayColor
function XXTouchableHighlight(
  props: React.ComponentProps<typeof RNTouchableHighlight>
) {
  const { underlayColor, ...style } = StyleSheet.flatten(props.style) || {};
  return (
    <RNTouchableHighlight
      underlayColor={underlayColor}
      {...props}
      style={style}
    />
  );
}

export const TouchableHighlight = (
  props: React.ComponentProps<typeof RNTouchableHighlight>
) => {
  return useCssElement(XXTouchableHighlight, props, { className: "style" });
};
TouchableHighlight.displayName = "CSS(TouchableHighlight)";

Componente de Imagen (src/tw/image.tsx)

import { useCssElement } from "react-native-css";
import React from "react";
import { StyleSheet } from "react-native";
import Animated from "react-native-reanimated";
import { Image as RNImage } from "expo-image";

const AnimatedExpoImage = Animated.createAnimatedComponent(RNImage);

export type ImageProps = React.ComponentProps<typeof Image>;

function CSSImage(props: React.ComponentProps<typeof AnimatedExpoImage>) {
  // @ts-expect-error: Remapea el estilo objectFit a la propiedad contentFit
  const { objectFit, objectPosition, ...style } =
    StyleSheet.flatten(props.style) || {};

  return (
    <AnimatedExpoImage
      contentFit={objectFit}
      contentPosition={objectPosition}
      {...props}
      source={
        typeof props.source === "string" ? { uri: props.source } : props.source
      }
      // @ts-expect-error: El estilo se remapea arriba
      style={style}
    />
  );
}

export const Image = (
  props: React.ComponentProps<typeof CSSImage> & { className?: string }
) => {
  return useCssElement(CSSImage, props, { className: "style" });
};

Image.displayName = "CSS(Image)";

Componentes Animados (src/tw/animated.tsx)

import * as TW from "./index";
import RNAnimated from "react-native-reanimated";

export const Animated = {
  ...RNAnimated,
  View: RNAnimated.createAnimatedComponent(TW.View),
};

Uso

Importa los componentes envueltos con CSS desde tu directorio tw:

import { View, Text, ScrollView, Image } from "@/tw";

export default function MyScreen() {
  return (
    <ScrollView className="flex-1 bg-white">
      <View className="p-4 gap-4">
        <Text className="text-xl font-bold text-gray-900">¡Hola Tailwind!</Text>
        <Image
          className="w-full h-48 rounded-lg object-cover"
          source={{ uri: "https://example.com/image.jpg" }}
        />
      </View>
    </ScrollView>
  );
}

Variables de Tema Personalizadas

Agrega variables de tema personalizadas en tu global.css usando @theme:

@layer theme {
  @theme {
    /* Fuentes personalizadas */
    --font-rounded: "SF Pro Rounded", sans-serif;

    /* Alturas de línea personalizadas */
    --text-xs--line-height: calc(1em / 0.75);
    --text-sm--line-height: calc(1.25em / 0.875);
    --text-base--line-height: calc(1.5em / 1);

    /* Escalas de leading personalizadas */
    --leading-tight: 1.25em;
    --leading-snug: 1.375em;
    --leading-normal: 1.5em;
  }
}

Estilos Específicos por Plataforma

Usa media queries de plataforma para estilos específicos:

@media ios {
  :root {
    --font-sans: system-ui;
    --font-rounded: ui-rounded;
  }
}

@media android {
  :root {
    --font-sans: normal;
    --font-rounded: normal;
  }
}

Colores del Sistema Apple con Variables CSS

Crea un archivo CSS para colores semánticos de Apple:

/* src/css/sf.css */
@layer base {
  html {
    color-scheme: light;
  }
}

:root {
  /* Colores de acento con modo claro/oscuro */
  --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));

  /* Escalas de grises */
  --sf-gray: light-dark(rgb(142 142 147), rgb(142 142 147));
  --sf-gray-2: light-dark(rgb(174 174 178), rgb(99 99 102));

  /* Colores de texto */
  --sf-text: light-dark(rgb(0 0 0), rgb(255 255 255));
  --sf-text-2: light-dark(rgb(60 60 67 / 0.6), rgb(235 235 245 / 0.6));

  /* Colores de fondo */
  --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));
}

/* Colores nativos de iOS mediante platformColor */
@media ios {
  :root {
    --sf-blue: platformColor(systemBlue);
    --sf-green: platformColor(systemGreen);
    --sf-red: platformColor(systemRed);
    --sf-gray: platformColor(systemGray);
    --sf-text: platformColor(label);
    --sf-text-2: platformColor(secondaryLabel);
    --sf-bg: platformColor(systemBackground);
    --sf-bg-2: platformColor(secondarySystemBackground);
  }
}

/* Registrar como colores del tema de Tailwind */
@layer theme {
  @theme {
    --color-sf-blue: var(--sf-blue);
    --color-sf-green: var(--sf-green);
    --color-sf-red: var(--sf-red);
    --color-sf-gray: var(--sf-gray);
    --color-sf-text: var(--sf-text);
    --color-sf-text-2: var(--sf-text-2);
    --color-sf-bg: var(--sf-bg);
    --color-sf-bg-2: var(--sf-bg-2);
  }
}

Luego úsalos en componentes:

<Text className="text-sf-text">Texto primario</Text>
<Text className="text-sf-text-2">Texto secundario</Text>
<View className="bg-sf-bg">...</View>

Usar Variables CSS en JavaScript

Usa el hook useCSSVariable:

import { useCSSVariable } from "@/tw";

function MyComponent() {
  const blue = useCSSVariable("--sf-blue");

  return <View style={{ borderColor: blue }} />;
}

Diferencias Clave con NativeWind v4 / Tailwind v3

  1. Sin babel.config.js - La configuración ahora es CSS-first
  2. Plugin PostCSS - Usa @tailwindcss/postcss en lugar de tailwindcss
  3. Importaciones CSS - Usa @import "tailwindcss/..." en lugar de directivas @tailwind
  4. Configuración de tema - Usa @theme en CSS en lugar de tailwind.config.js
  5. Wrappers de componentes - Debes envolver componentes con useCssElement para soporte de className
  6. Configuración de Metro - Usa withNativewind con opciones diferentes (inlineVariables: false)

Solución de Problemas

Los estilos no se aplican

  1. Asegúrate de haber importado el archivo CSS en el punto de entrada de tu aplicación
  2. Verifica que los componentes estén envueltos con useCssElement
  3. Comprueba que la configuración de Metro tenga withNativewind aplicado

Los colores de plataforma no funcionan

  1. Usa platformColor() en bloques @media ios
  2. Recurre a light-dark() para web/Android

Errores de TypeScript

Agrega className a las props del componente:

type Props = React.ComponentProps<typeof RNView> & { className?: string };

Individual skills in this repo

This repo contains 5 individual skills — each has its own dedicated page.

Samueladewole/claude-skills

Create API handoff documentation for frontend developers. Use when backend work is complete and needs to be documented for frontend integration, or user says 'create handoff', 'document API', 'frontend handoff', or 'API documentation'.

Samueladewole/claude-skills

Build accessible, responsive, and performant frontend components with design system best practices, modern CSS, and framework-agnostic patterns.

Samueladewole/claude-skills

Document frontend data needs for backend developers. Use when frontend needs to communicate API requirements to backend, or user says 'backend requirements', 'what data do I need', 'API requirements', or is describing data needs for a UI.

Samueladewole/claude-skills

Master Next.js 14+ App Router with Server Components, streaming, parallel routes, and advanced data fetching. Use when building Next.js applications, implementing SSR/SSG, or optimizing React Server Components.

Samueladewole/claude-skills

Analyze a short-form video (TikTok, Instagram Reel, YouTube Short, or any MP4) to extract its full transcript, understand why it performed, and generate concrete repurposing ideas. Use this skill whenever the user asks to: analyze a video, understand why something went viral, reverse-engineer a creator's content, extract a transcript from a video, figure out what makes a video perform, or get repurposing ideas from a video. Also trigger when the user says things like "why did this perform", "what's the hook here", "can I repurpose this", "analyze this TikTok/Reel", or drops a video file path or URL.

Skills relacionados