Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Set up Tailwind CSS v4 in Expo with react-native-css and NativeWind v5 for universal styling
.claude/skills/lingxling-expo-tailwind-setup/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-10 | ✗→✓ | ▲ Improved | 93% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 58% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 38% | 0% |
| case-03 | ✗→✓ | ▲ Improved | -16% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 58% | 0% |
This guide covers setting up Tailwind CSS v4 in Expo using react-native-css and NativeWind v5 for universal styling across iOS, Android, and Web.
react-native-css and NativeWind v5.This setup uses:
bash# Install dependencies npx expo install tailwindcss@^4 nativewind@5.0.0-preview.2 react-native-css@0.0.0-nightly.5ce6396 @tailwindcss/postcss tailwind-merge clsx
Add resolutions for lightningcss compatibility:
json// package.json { "resolutions": { "lightningcss": "1.30.1" } }
Create or update metro.config.js:
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, { // inline variables break PlatformColor in CSS variables inlineVariables: false, // We add className support manually globalClassNamePolyfill: false, });
Create postcss.config.mjs:
js// postcss.config.mjs export default { plugins: { "@tailwindcss/postcss": {}, }, };
Create src/global.css:
css@import "tailwindcss/theme.css" layer(theme); @import "tailwindcss/preflight.css" layer(base); @import "tailwindcss/utilities.css"; /* Platform-specific font families */ @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; } }
With Tailwind v4 and NativeWind v5, you do NOT need a babel.config.js for Tailwind. Remove any NativeWind babel presets if present:
js// DELETE babel.config.js if it only contains NativeWind config // The following is NO LONGER needed: // module.exports = function (api) { // api.cache(true); // return { // presets: [ // ["babel-preset-expo", { jsxImportSource: "nativewind" }], // "nativewind/babel", // ], // }; // };
Since react-native-css requires explicit CSS element wrapping, create reusable components:
src/tw/index.tsx)tsximport { 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"; // CSS-enabled Link 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; // CSS Variable hook 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 with underlayColor extraction 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)";
src/tw/image.tsx)tsximport { 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: Remap objectFit style to contentFit property 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: Style is remapped above style={style} /> ); } export const Image = ( props: React.ComponentProps<typeof CSSImage> & { className?: string } ) => { return useCssElement(CSSImage, props, { className: "style" }); }; Image.displayName = "CSS(Image)";
src/tw/animated.tsx)tsximport * as TW from "./index"; import RNAnimated from "react-native-reanimated"; export const Animated = { ...RNAnimated, View: RNAnimated.createAnimatedComponent(TW.View), };
Import CSS-wrapped components from your tw directory:
tsximport { 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">Hello Tailwind!</Text> <Image className="w-full h-48 rounded-lg object-cover" source={{ uri: "https://example.com/image.jpg" }} /> </View> </ScrollView> ); }
Add custom theme variables in your global.css using @theme:
css@layer theme { @theme { /* Custom fonts */ --font-rounded: "SF Pro Rounded", sans-serif; /* Custom line heights */ --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); /* Custom leading scales */ --leading-tight: 1.25em; --leading-snug: 1.375em; --leading-normal: 1.5em; } }
Use platform media queries for platform-specific styling:
css@media ios { :root { --font-sans: system-ui; --font-rounded: ui-rounded; } } @media android { :root { --font-sans: normal; --font-rounded: normal; } }
Create a CSS file for Apple semantic colors:
css/* src/css/sf.css */ @layer base { html { color-scheme: light; } } :root { /* Accent colors with light/dark mode */ --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)); /* Gray scales */ --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)); /* Text colors */ --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)); /* Background colors */ --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 native colors via 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); } } /* Register as Tailwind theme colors */ @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); } }
Then use in components:
tsx<Text className="text-sf-text">Primary text</Text> <Text className="text-sf-text-2">Secondary text</Text> <View className="bg-sf-bg">...</View>
Use the useCSSVariable hook:
tsximport { useCSSVariable } from "@/tw"; function MyComponent() { const blue = useCSSVariable("--sf-blue"); return <View style={{ borderColor: blue }} />; }
@tailwindcss/postcss instead of tailwindcss@import "tailwindcss/..." instead of @tailwind directives@theme in CSS instead of tailwind.config.jsuseCssElement for className supportwithNativewind with different options (inlineVariables: false)useCssElementwithNativewind appliedplatformColor() in @media ios blockslight-dark() for web/AndroidAdd className to component props:
tsxtype Props = React.ComponentProps<typeof RNView> & { className?: string };
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-10 | fail→pass | 14,217 | 7,050 | -50% | 1 | 1 | 0% | 2,706 | 5,218 | +93% | 0 | 0 | — |
case-20 | pass→pass | 21,916 | 16,628 | -24% | 1 | 1 | 0% | 4,395 | 7,531 | +71% | 0 | 0 | — |
case-01 | fail→pass | 23,351 | 23,723 | +2% | 1 | 1 | 0% | 4,668 | 7,396 | +58% | 0 | 0 | — |
case-02 | fail→pass | 20,844 | 11,138 | -47% | 1 | 1 | 0% | 4,486 | 6,208 | +38% | 0 | 0 | — |
case-03 | fail→pass | 33,102 | 13,461 | -59% | 1 | 1 | 0% | 8,246 | 6,888 | -16% | 0 | 0 | — |
case-04 | pass→pass | 10,940 | 5,632 | -49% | 1 | 1 | 0% | 1,923 | 4,862 | +153% | 0 | 0 | — |
case-05 | fail→pass | 15,446 | 5,957 | -61% | 1 | 1 | 0% | 3,041 | 4,811 | +58% | 0 | 0 | — |
case-06 | pass→pass | 10,318 | 3,521 | -66% | 1 | 1 | 0% | 1,696 | 4,424 | +161% | 0 | 0 | — |
case-07 | fail→pass | 7,582 | 4,248 | -44% | 1 | 1 | 0% | 1,313 | 4,557 | +247% | 0 | 0 | — |
case-08 | fail→pass | 14,183 | 8,684 | -39% | 1 | 1 | 0% | 2,523 | 5,330 | +111% | 0 | 0 | — |
case-09 | pass→pass | 11,778 | 4,893 | -58% | 1 | 1 | 0% | 2,329 | 4,701 | +102% | 0 | 0 | — |
case-11 | pass→pass | 16,096 | 7,429 | -54% | 1 | 1 | 0% | 2,861 | 5,117 | +79% | 0 | 0 | — |
case-12 | fail→pass | 19,079 | 7,418 | -61% | 1 | 1 | 0% | 3,381 | 5,142 | +52% | 0 | 0 | — |
case-13 | fail→pass | 21,718 | 7,490 | -66% | 1 | 1 | 0% | 4,058 | 5,366 | +32% | 0 | 0 | — |
case-14 | fail→pass | 16,344 | 5,539 | -66% | 1 | 1 | 0% | 3,020 | 4,857 | +61% | 0 | 0 | — |
case-19 | fail→pass | 12,088 | 7,153 | -41% | 1 | 1 | 0% | 2,250 | 5,198 | +131% | 0 | 0 | — |
case-15 | fail→pass | 14,528 | 8,454 | -42% | 1 | 1 | 0% | 2,638 | 5,549 | +110% | 0 | 0 | — |
case-16 | fail→pass | 12,388 | 4,568 | -63% | 1 | 1 | 0% | 2,336 | 4,681 | +100% | 0 | 0 | — |
case-17 | fail→pass | 26,469 | 3,913 | -85% | 1 | 1 | 0% | 4,406 | 4,490 | +2% | 0 | 0 | — |
case-18 | fail→pass | 12,883 | 10,471 | -19% | 1 | 1 | 0% | 2,318 | 5,905 | +155% | 0 | 0 | — |
case-21 | pass→pass | 6,706 | 4,468 | -33% | 1 | 1 | 0% | 1,271 | 4,573 | +260% | 0 | 0 | — |
case-22 | pass→pass | 16,643 | 17,460 | +5% | 1 | 1 | 0% | 3,385 | 7,542 | +123% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +68 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.