원클릭으로
expo-tailwind-setup
Set up Tailwind CSS v4 in Expo with react-native-css and NativeWind v5 for universal styling
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Set up Tailwind CSS v4 in Expo with react-native-css and NativeWind v5 for universal styling
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
When the user wants to set up, improve, or audit analytics tracking and measurement. Also use when the user mentions "set up tracking," "GA4," "Google Analytics," "conversion tracking," "event tracking," "UTM parameters," "tag manager," "GTM," "analytics implementation," "tracking plan," "how do I measure this," "track conversions," "attribution," "Mixpanel," "Segment," "are my events firing," or "analytics isn't working." Use this whenever someone asks how to know if something is working or wants to measure marketing results. For A/B test measurement, see ab-test-setup.
Complete guide for building beautiful apps with Expo Router. Covers fundamentals, styling, components, navigation, animations, patterns, and native tabs.
Production animation patterns for React Native using Reanimated 4, Skia, WebGPU, and TypeGPU. Covers CSS transitions, CSS animations, shared value animations, canvas animations with react-native-skia, GPU shader animations, layout animations, scroll-driven animations, interpolation, particle systems, procedural noise, SDF rendering, performance tuning, and accessibility. Trigger on: Reanimated, useSharedValue, useAnimatedStyle, withSpring, withTiming, withDecay, withRepeat, withSequence, CSS transition, CSS animation, layout animation, FadeIn, SlideIn, ZoomIn, LinearTransition, keyframe, interpolate, scrollTo, useFrameCallback, react-native-skia, Skia Canvas, Atlas, usePathInterpolation, usePathValue, useClock, useTexture, SKSL, interpolateColors, Picture API, canvas animation, sprite animation, WebGPU, react-native-wgpu, TypeGPU, GPU shader, WGSL, particle system, Perlin noise, SDF, Three.js, react-three-fiber, animation performance, or any request to animate UI in React Native.
Software Mansion's best practices for audio in React Native using react-native-audio-api. Covers audio playback (buffer sources, oscillators, streaming), recording (file, callback, graph), audio effects (gain, filters, delay, convolver, panner, waveshaper), real-time analysis and visualization, audio worklets (custom synthesis, UIRuntime/AudioRuntime), system integration (sessions, interruptions, permissions), and testing. Trigger on: react-native-audio-api, AudioContext, AudioRecorder, AudioBuffer, AudioBufferSourceNode, AudioBufferQueueSourceNode, OscillatorNode, StreamerNode, GainNode, BiquadFilterNode, DelayNode, ConvolverNode, StereoPannerNode, WaveShaperNode, AnalyserNode, AudioParam, AudioManager, PlaybackNotificationManager, RecordingNotificationManager, WorkletNode, WorkletSourceNode, WorkletProcessingNode, audio playback, record audio, microphone, waveform, audio visualization, audio streaming, audio worklet, or any React Native feature that captures, processes, or emits sound.
Software Mansion's best practices for gestures in React Native apps using React Native Gesture Handler. Use when implementing tap, pan, pinch, rotation, swipe, long press, fling, hover, drag, or any touch interaction. Trigger on: 'gesture handler', 'GestureDetector', 'tap gesture', 'pan gesture', 'pinch gesture', 'rotation gesture', 'long press', 'fling', 'hover gesture', 'swipe', 'pinch to zoom', 'drag', 'touch handling', 'Pressable', 'RectButton', 'Swipeable', 'DrawerLayout', 'VirtualGestureDetector', or any request to handle user touch input in a React Native app.
Software Mansion's best practices for multithreading in React Native apps using react-native-worklets. Use when running JavaScript on multiple threads, offloading heavy computation from the JS thread, communicating between runtimes, or sharing data across threads. Trigger on: 'worklet', 'worklets', 'react-native-worklets', 'runOnUI', 'runOnJS', 'scheduleOnUI', 'scheduleOnRN', 'scheduleOnRuntime', 'createWorkletRuntime', 'background thread', 'UI thread', 'worker runtime', 'Serializable', 'Synchronizable', 'multithreading', 'parallel execution', 'offload computation', 'background processing', 'Bundle Mode', or any request to move work off the JS thread in a React Native app.
| name | expo-tailwind-setup |
| description | Set up Tailwind CSS v4 in Expo with react-native-css and NativeWind v5 for universal styling |
| version | 1.0.0 |
| license | MIT |
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.
This setup uses:
# 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:
// package.json
{
"resolutions": {
"lightningcss": "1.30.1"
}
}
Create or update 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, {
// inline variables break PlatformColor in CSS variables
inlineVariables: false,
// We add className support manually
globalClassNamePolyfill: false,
});
Create postcss.config.mjs:
// postcss.config.mjs
export default {
plugins: {
'@tailwindcss/postcss': {},
},
};
Create src/global.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:
// 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)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';
// 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)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: 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)import * 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:
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">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:
@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:
@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:
/* 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:
<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:
import { 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:
type Props = React.ComponentProps<typeof RNView> & { className?: string };