mobile-styling-nativewind
NativeWind v4+ - Tailwind CSS utility classes for React Native, className prop, CSS variables, dark mode, platform prefixes, animations, theming, third-party component integration
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/mobile-styling-nativewind/skills/mobile-styling-nativewind
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
NativeWind Patterns
Quick Guide: NativeWind brings Tailwind CSS utility classes to React Native via
classNameprop. Styles compile toStyleSheet.createat build time with a lightweight runtime for conditional logic (dark mode, hover, focus). Always declare both light AND dark styles (no CSS cascade in RN). Usevars()for runtime theming with CSS variables. Platform prefixes (ios:,android:,native:) replacePlatform.selectfor styling. UseremapPropsfor third-party components with multiple style props; reservecssInteropfor components needing style-to-prop extraction.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST always declare BOTH light and dark styles -- className="text-black dark:text-white" not just className="dark:text-white" -- React Native has no CSS cascade)
(You MUST use remapProps for third-party components with multiple style props and cssInterop ONLY when style attributes need extraction to props -- NEVER use either for your own custom components)
(You MUST import "./global.css" at your app entry point -- without it no styles render)
(You MUST add /// <reference types="nativewind/types" /> in a nativewind-env.d.ts file for TypeScript className support)
(You MUST use nativewind/preset in tailwind.config.js presets -- without it platform-specific features break)
</critical_requirements>
Auto-detection: NativeWind, nativewind, className on React Native components, nativewind/preset, nativewind/babel, nativewind/metro, withNativeWind, cssInterop, remapProps, vars(), useColorScheme from nativewind, useUnstableNativeVariable, dark: prefix in React Native, ios: prefix, android: prefix, native: prefix, global.css tailwind directives, nativewind-env.d.ts
When to use:
- Styling React Native components with Tailwind CSS utility classes
- Implementing dark mode with automatic system detection or manual toggle
- Creating dynamic themes with CSS variables via
vars() - Applying platform-specific styles with
ios:/android:/native:prefixes - Integrating className support with third-party React Native libraries
- Adding transitions and animations to React Native components
Key patterns covered:
- className prop usage and custom component patterns
- Dark mode with
useColorScheme(system preference and manual toggle) - CSS variables for runtime theming via
vars()anduseUnstableNativeVariable() - Platform prefixes (
ios:,android:,web:,native:) for cross-platform styling - Third-party component integration (
remapPropsvscssInterop) - Animations and transitions (experimental, powered by react-native-reanimated)
- Variant components with class merging libraries
When NOT to use:
- Web-only React projects (use standard Tailwind CSS)
- Projects that need zero runtime overhead (use
StyleSheet.createdirectly) - Apps on legacy React Native architecture that cannot adopt New Architecture dependencies
Detailed Resources:
- examples/core.md - className usage, custom components, variants, conditional styling
- examples/theming.md - Dark mode, CSS variables, theme switching, useColorScheme
- examples/platform-and-interop.md - Platform prefixes, cssInterop, remapProps, third-party integration
- reference.md - Decision frameworks, API cheat sheet, migration notes
<decision_framework>
Decision Framework
Styling Approach
Need Tailwind utility classes in React Native?
├─ YES → NativeWind
└─ NO → StyleSheet.create (zero overhead)
Need zero runtime overhead?
├─ YES → StyleSheet.create (0ms)
├─ Acceptable ~2ms → NativeWind (compiled)
└─ Runtime parsing OK → twrnc (~8-15ms, pure runtime)
Third-Party Component Integration
Does the component accept className already?
├─ YES → Use it directly (no setup needed)
└─ NO → Does it have multiple style props (style, contentContainerStyle)?
├─ YES → remapProps (lightweight, zero overhead)
└─ NO → Does a style attribute need to become a prop?
├─ YES → cssInterop (extracts style attributes to props)
└─ NO → remapProps with simple mapping
Theming Strategy
Static theme (compile-time)?
├─ YES → Customize tailwind.config.js theme.extend
└─ NO → Need runtime theme switching?
├─ YES → vars() with CSS variables
└─ Need multiple brand themes?
└─ Combine vars() + useColorScheme for brand + light/dark matrix
See reference.md for full API cheat sheet, dark mode strategy tree, and migration notes.
</decision_framework>
<red_flags>
RED FLAGS
High Priority Issues:
- Declaring only dark styles without light counterpart (
dark:text-whitewithouttext-black) -- React Native has no CSS cascade, so the light variant will have no text color - Using
cssInteroporremapPropson your own custom components -- these are exclusively for third-party components. Your own components should accept and mergeclassNamedirectly - Missing
import "./global.css"at app entry point -- no styles will render without it - Missing
nativewind/presetin tailwind.config.js presets -- platform prefixes, CSS variable support, and other NativeWind-specific features will not work - Using
useColorSchemefromreact-nativeinstead ofnativewind-- the nativewind version providessetColorSchemeandtoggleColorScheme
Medium Priority Issues:
- Using
cssInteropwhenremapPropswould suffice --cssInterophas runtime overhead for style resolution, event handlers, and context injection - Naming the TypeScript declaration file
nativewind.d.ts-- it conflicts with the package's own types. Usenativewind-env.d.ts - Not setting
userInterfaceStyle: "automatic"in Expo app.json -- system dark mode preference will not be detected - Using web-designed breakpoints (
sm:,md:,lg:) without customizing for mobile -- NativeWind's default breakpoints are web-centric (640px, 768px, 1024px) and may not match mobile screen sizes
Gotchas & Edge Cases:
- Inline
styleprop takes precedence overclassNamestyles due to CSS specificity --<Text className="text-white" style={{ color: "black" }} />renders black remunits differ between platforms: 14 on native (RN default font size), 16 on web -- use px values in theme config for consistency- Color opacity is disabled by default for performance on native -- enable via
corePluginsin tailwind.config.js if you needbg-blue-500/50syntax vars()values propagate via React Context, not actual CSS -- they only flow to React children, not portal-rendered contentuseUnstableNativeVariableAPI may change in future versions (prefixed "unstable" intentionally)- Animations and transitions are experimental on native --
transition-shadowis web-only, and animation performance is actively being improved gap-compiles to nativecolumnGap/rowGapin v4 (v2 used a polyfill) -- verify your React Native version supports gap layout propsdivide-andspace-utilities are temporarily unavailable in v4- NativeWind v5 (in preview) deprecates
cssInterop/remapPropsin favor ofstyled(), andvars()in favor ofVariableContextProvider-- check migration guide when upgrading - Tailwind CSS v4 is NOT yet supported by NativeWind v4 -- NativeWind v4 uses Tailwind CSS v3.4 config format
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md
(You MUST always declare BOTH light and dark styles -- className="text-black dark:text-white" not just className="dark:text-white" -- React Native has no CSS cascade)
(You MUST use remapProps for third-party components with multiple style props and cssInterop ONLY when style attributes need extraction to props -- NEVER use either for your own custom components)
(You MUST import "./global.css" at your app entry point -- without it no styles render)
(You MUST add /// <reference types="nativewind/types" /> in a nativewind-env.d.ts file for TypeScript className support)
(You MUST use nativewind/preset in tailwind.config.js presets -- without it platform-specific features break)
Failure to follow these rules will cause invisible styles, broken dark mode, TypeScript errors on className props, and platform-specific rendering failures.
</critical_reminders>
Files (skills)
-
examples
-
core.md 9.1 KB
# NativeWind - Core Patterns > className usage, custom components, variants, and conditional styling. See [SKILL.md](../SKILL.md) for decision guidance. **Prerequisites**: Familiarity with React Native components and Tailwind CSS utility class syntax. --- ## Pattern 1: Basic className Usage ```tsx import { View, Text, Pressable, Image, ScrollView } from "react-native"; export function ProfileCard({ name, bio, avatarUrl, onMessage, }: { name: string; bio: string; avatarUrl: string; onMessage: () => void; }) { return ( <View className="mx-4 rounded-xl bg-white p-4 shadow-md dark:bg-gray-800"> <View className="flex-row items-center gap-3"> <Image source={{ uri: avatarUrl }} className="h-12 w-12 rounded-full" /> <View className="flex-1"> <Text className="text-lg font-bold text-gray-900 dark:text-white"> {name} </Text> <Text className="text-sm text-gray-500 dark:text-gray-400"> {bio} </Text> </View> </View> <Pressable className="mt-4 rounded-lg bg-blue-500 px-4 py-3 active:bg-blue-600" onPress={onMessage} > <Text className="text-center font-semibold text-white">Message</Text> </Pressable> </View> ); } ``` **Why good:** Both light and dark variants declared on every text/background, `active:` for press feedback, `gap-3` for spacing (compiles to native columnGap/rowGap), no style objects needed --- ## Pattern 2: Custom Component with className Prop Custom components should accept and merge a `className` prop. Never use `cssInterop` or `remapProps` on your own components. ```tsx import { View, Text, type ViewStyle } from "react-native"; interface SectionProps { title: string; children: React.ReactNode; className?: string; } export function Section({ title, children, className }: SectionProps) { return ( <View className={`mb-6 ${className ?? ""}`}> <Text className="mb-2 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"> {title} </Text> {children} </View> ); } // Usage <Section title="Account" className="px-4"> <Text className="text-gray-900 dark:text-white">Settings content</Text> </Section>; ``` **Why good:** className prop enables external customization, default styles set on the component, caller can override layout/spacing --- ## Pattern 3: Multiple className Props Complex components can expose multiple className props for different internal elements. ```tsx interface ListItemProps { title: string; subtitle?: string; onPress: () => void; className?: string; titleClassName?: string; subtitleClassName?: string; } export function ListItem({ title, subtitle, onPress, className, titleClassName, subtitleClassName, }: ListItemProps) { return ( <Pressable className={`flex-row items-center px-4 py-3 active:bg-gray-100 dark:active:bg-gray-800 ${className ?? ""}`} onPress={onPress} > <View className="flex-1"> <Text className={`text-base text-gray-900 dark:text-white ${titleClassName ?? ""}`} > {title} </Text> {subtitle && ( <Text className={`mt-0.5 text-sm text-gray-500 dark:text-gray-400 ${subtitleClassName ?? ""}`} > {subtitle} </Text> )} </View> </Pressable> ); } ``` **Why good:** Each internal element is independently customizable, defaults cover light and dark, no cssInterop needed --- ## Pattern 4: Variants with a Class Merging Library For components with multiple variant dimensions, use a class merging library to handle conditional classes and resolve conflicts. ### With clsx ```tsx import clsx from "clsx"; import { Pressable, Text } from "react-native"; type ButtonVariant = "primary" | "secondary" | "ghost"; type ButtonSize = "sm" | "md" | "lg"; interface ButtonProps { label: string; variant?: ButtonVariant; size?: ButtonSize; disabled?: boolean; onPress: () => void; className?: string; } const VARIANT_CLASSES: Record<ButtonVariant, string> = { primary: "bg-blue-500 active:bg-blue-600", secondary: "bg-gray-200 active:bg-gray-300 dark:bg-gray-700 dark:active:bg-gray-600", ghost: "bg-transparent active:bg-gray-100 dark:active:bg-gray-800", }; const SIZE_CLASSES: Record<ButtonSize, string> = { sm: "px-3 py-1.5", md: "px-4 py-2.5", lg: "px-6 py-3.5", }; const TEXT_VARIANT_CLASSES: Record<ButtonVariant, string> = { primary: "text-white", secondary: "text-gray-900 dark:text-white", ghost: "text-blue-500 dark:text-blue-400", }; const TEXT_SIZE_CLASSES: Record<ButtonSize, string> = { sm: "text-sm", md: "text-base", lg: "text-lg", }; export function Button({ label, variant = "primary", size = "md", disabled = false, onPress, className, }: ButtonProps) { return ( <Pressable className={clsx( "items-center rounded-lg", VARIANT_CLASSES[variant], SIZE_CLASSES[size], disabled && "opacity-50", className, )} disabled={disabled} onPress={onPress} > <Text className={clsx( "font-semibold", TEXT_VARIANT_CLASSES[variant], TEXT_SIZE_CLASSES[size], )} > {label} </Text> </Pressable> ); } ``` **Why good:** clsx handles conditional class concatenation cleanly, variant maps are named constants, disabled state is a simple conditional, caller can override via className ### With tailwind-variants ```tsx import { tv } from "tailwind-variants"; import { Pressable, Text } from "react-native"; const button = tv({ base: "items-center rounded-lg", variants: { variant: { primary: "bg-blue-500 active:bg-blue-600", secondary: "bg-gray-200 active:bg-gray-300 dark:bg-gray-700", ghost: "bg-transparent active:bg-gray-100", }, size: { sm: "px-3 py-1.5", md: "px-4 py-2.5", lg: "px-6 py-3.5", }, }, defaultVariants: { variant: "primary", size: "md", }, }); const buttonText = tv({ base: "font-semibold", variants: { variant: { primary: "text-white", secondary: "text-gray-900 dark:text-white", ghost: "text-blue-500", }, size: { sm: "text-sm", md: "text-base", lg: "text-lg", }, }, defaultVariants: { variant: "primary", size: "md", }, }); interface ButtonProps { label: string; variant?: "primary" | "secondary" | "ghost"; size?: "sm" | "md" | "lg"; onPress: () => void; className?: string; } export function Button({ label, variant, size, onPress, className, }: ButtonProps) { return ( <Pressable className={button({ variant, size, className })} onPress={onPress} > <Text className={buttonText({ variant, size })}>{label}</Text> </Pressable> ); } ``` **Why good:** tailwind-variants handles conflict resolution, default variants are declarative, className passthrough enables caller overrides --- ## Pattern 5: Conditional Styling ```tsx import clsx from "clsx"; import { View, Text } from "react-native"; interface StatusIndicatorProps { status: "online" | "offline" | "busy"; unreadCount: number; } const MAX_DISPLAY_COUNT = 99; const STATUS_COLORS = { online: "bg-green-500", offline: "bg-gray-400", busy: "bg-red-500", } as const; export function StatusIndicator({ status, unreadCount }: StatusIndicatorProps) { const hasUnread = unreadCount > 0; const displayCount = unreadCount > MAX_DISPLAY_COUNT ? `${MAX_DISPLAY_COUNT}+` : String(unreadCount); return ( <View className="flex-row items-center gap-2"> <View className={clsx("h-3 w-3 rounded-full", STATUS_COLORS[status])} /> {hasUnread && ( <View className="min-w-[20px] items-center rounded-full bg-red-500 px-1.5 py-0.5"> <Text className="text-xs font-bold text-white">{displayCount}</Text> </View> )} </View> ); } ``` **Why good:** Status colors are a named constant map, conditional rendering for badge, arbitrary value `min-w-[20px]` for minimum badge width, named constant for display limit --- ## Pattern 6: Inline Style Merging Inline `style` props merge with className-based styles. Inline properties take precedence. ```tsx import { View, Text, type ViewStyle } from "react-native"; interface ProgressBarProps { progress: number; // 0-1 className?: string; } export function ProgressBar({ progress, className }: ProgressBarProps) { // Dynamic width requires inline style -- className can't do runtime percentages const fillStyle: ViewStyle = { width: `${progress * 100}%` }; return ( <View className={`h-2 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700 ${className ?? ""}`} > <View className="h-full rounded-full bg-blue-500" style={fillStyle} /> </View> ); } ``` **Why good:** Static styles in className (background, height, border-radius), dynamic value in inline style (width percentage), inline style takes precedence over className **When to use inline style:** Runtime-computed values (dynamic widths, calculated positions, values from gestures). For everything else, prefer className. -
platform-and-interop.md 7.8 KB
# NativeWind - Platform Prefixes and Third-Party Interop > Platform-specific styling, cssInterop vs remapProps, and third-party component integration. See [core.md](core.md) for basic className patterns. **Prerequisites**: Understand [Pattern 5: Platform Prefixes](../SKILL.md) and [Pattern 6: Third-Party Integration](../SKILL.md) from SKILL.md. --- ## Pattern 1: Platform-Specific Styling Use `ios:`, `android:`, `web:`, and `native:` prefixes to handle platform differences declaratively. ```tsx import { View, Text, Pressable } from "react-native"; export function PlatformCard({ title, onPress, }: { title: string; onPress: () => void; }) { return ( <View className={` m-4 rounded-xl bg-white p-4 dark:bg-gray-800 ios:shadow-lg android:elevation-4 `} > <Text className={` text-lg text-gray-900 dark:text-white ios:font-semibold android:font-bold `} > {title} </Text> {/* Haptic-style feedback differs by platform */} <Pressable className={` mt-3 rounded-lg bg-blue-500 px-4 py-3 active:bg-blue-600 android:active:bg-blue-700 `} onPress={onPress} > <Text className="text-center font-medium text-white">Action</Text> </Pressable> </View> ); } ``` **Why good:** Shadows handled correctly per platform (iOS uses shadow-_, Android uses elevation-_), font weights adjusted for platform rendering, no Platform.select boilerplate --- ## Pattern 2: Native-Only and Web-Only Styles The `native:` prefix targets iOS + Android + all other native platforms (not web). Useful for cross-platform apps. ```tsx <View className="p-4 native:pt-12 web:pt-4"> {/* Extra top padding on native for status bar area */} <Text className="text-xl font-bold text-gray-900 dark:text-white native:text-lg web:text-2xl"> Responsive Heading </Text> {/* Hover only works on web (pointer devices) */} <Pressable className="rounded-lg bg-gray-100 p-3 active:bg-gray-200 web:hover:bg-gray-150"> <Text className="text-gray-900 dark:text-white">Interactive Item</Text> </Pressable> </View> ``` **Why good:** `native:` avoids repeating `ios: android:` for shared native behavior, `web:hover:` applies only where pointer events exist --- ## Pattern 3: remapProps for Multi-Style Components Use `remapProps` to map className props to style props on third-party components. This is lightweight -- no style resolution overhead. ```tsx import { FlatList, ScrollView, SectionList } from "react-native"; import { remapProps } from "nativewind"; // FlatList: maps className to multiple style props remapProps(FlatList, { className: "style", contentContainerClassName: "contentContainerStyle", columnWrapperClassName: "columnWrapperStyle", ListHeaderComponentClassName: "ListHeaderComponentStyle", ListFooterComponentClassName: "ListFooterComponentStyle", }); // ScrollView: contentContainerStyle is common remapProps(ScrollView, { className: "style", contentContainerClassName: "contentContainerStyle", indicatorClassName: "indicatorStyle", }); // SectionList: similar to FlatList remapProps(SectionList, { className: "style", contentContainerClassName: "contentContainerStyle", }); ``` ```tsx // Usage -- className props map to the corresponding style props <FlatList className="flex-1 bg-gray-50 dark:bg-gray-900" contentContainerClassName="p-4 gap-3" data={items} renderItem={renderItem} keyExtractor={keyExtractor} /> <ScrollView className="flex-1" contentContainerClassName="p-4 pb-20" > {children} </ScrollView> ``` **Why good:** Zero style resolution overhead, maps className strings to the component's existing style props, type-safe with declaration merging --- ## Pattern 4: cssInterop for Style-to-Prop Extraction Use `cssInterop` when a component needs style properties extracted as individual props. This has runtime cost -- use only when necessary. ```tsx import { TextInput, StatusBar } from "react-native"; import { cssInterop } from "nativewind"; // TextInput: extract textAlign from style, map placeholder color cssInterop(TextInput, { className: { target: "style", nativeStyleToProp: { textAlign: true, // Extracts textAlign from style to a prop }, }, placeholderClassName: { target: false, // Don't merge into any style prop nativeStyleToProp: { color: "placeholderTextColor", // Extract color -> placeholderTextColor prop }, }, }); ``` ```tsx // Usage -- className drives both style and extracted props <TextInput className="rounded-lg border border-gray-300 p-3 text-base text-gray-900 text-center dark:border-gray-600 dark:text-white" placeholderClassName="text-gray-400 dark:text-gray-500" placeholder="Search..." /> ``` **Why good:** `textAlign` extracted from style to its own prop (required by TextInput), `placeholderTextColor` derived from className instead of hardcoded color string --- ## Pattern 5: TypeScript Declarations for Third-Party Props After calling `remapProps` or `cssInterop`, add TypeScript declarations so the new props are type-safe. ```typescript // nativewind-env.d.ts or a dedicated declarations file /// <reference types="nativewind/types" /> import type { FlatListProps, ScrollViewProps, TextInputProps, } from "react-native"; declare module "react-native" { interface FlatListProps<ItemT> { contentContainerClassName?: string; columnWrapperClassName?: string; ListHeaderComponentClassName?: string; ListFooterComponentClassName?: string; } interface ScrollViewProps { contentContainerClassName?: string; indicatorClassName?: string; } // TextInput already handled by nativewind/types, but for custom mappings: interface TextInputProps { placeholderClassName?: string; } } ``` **Why good:** TypeScript knows about the new className props, autocomplete works, type errors caught at compile time --- ## Pattern 6: Integrating SVG Components SVG libraries (react-native-svg) often need `cssInterop` because they use non-standard style props. ```tsx import Svg, { Circle, Path } from "react-native-svg"; import { cssInterop } from "nativewind"; // Map className to SVG-specific props cssInterop(Svg, { className: { target: "style", nativeStyleToProp: { width: true, height: true, }, }, }); cssInterop(Circle, { className: { target: "style", nativeStyleToProp: { width: true, height: true, fill: "fill", stroke: "stroke", strokeWidth: "strokeWidth", }, }, }); ``` ```tsx // Usage <Svg className="h-6 w-6"> <Circle className="fill-blue-500 stroke-blue-700" cx="12" cy="12" r="10" /> </Svg> ``` **Why good:** SVG dimensions and colors driven by Tailwind classes, nativeStyleToProp extracts the right attributes to SVG-specific props **When to use:** Only for SVG or similar components where style attributes must become element-specific props. For components with standard style props, prefer `remapProps`. --- ## Anti-Pattern: Using cssInterop/remapProps on Custom Components ```tsx // BAD -- never use cssInterop/remapProps on your own components import { cssInterop } from "nativewind"; function MyCard({ style, children }) { return <View style={style}>{children}</View>; } cssInterop(MyCard, { className: "style" }); // WRONG // GOOD -- accept and merge className directly function MyCard({ className, children, }: { className?: string; children: React.ReactNode; }) { return ( <View className={`rounded-xl bg-white p-4 dark:bg-gray-800 ${className ?? ""}`} > {children} </View> ); } ``` **Why bad:** cssInterop adds runtime overhead (style resolution, event handlers, context injection) that is completely unnecessary for your own components. Your own components can accept className directly because the JSX transform handles it. -
theming.md 8.7 KB
# NativeWind - Theming Patterns > Dark mode, CSS variables, theme switching, and multi-brand theming. See [core.md](core.md) for basic className patterns. **Prerequisites**: Understand [Pattern 3: Dark Mode](../SKILL.md) and [Pattern 4: CSS Variables](../SKILL.md) from SKILL.md. --- ## Pattern 1: Dark Mode with System Preference By default, NativeWind follows the device color scheme. Apply `dark:` prefix to all conditional styles. ```tsx import { View, Text, ScrollView, Pressable } from "react-native"; export function SettingsScreen() { return ( <ScrollView className="flex-1 bg-gray-50 dark:bg-gray-900"> <View className="p-4"> <Text className="mb-4 text-2xl font-bold text-gray-900 dark:text-white"> Settings </Text> {/* Card with proper light + dark styles */} <View className="rounded-xl bg-white p-4 shadow-sm dark:bg-gray-800"> <Text className="text-base text-gray-900 dark:text-white"> Notifications </Text> <Text className="mt-1 text-sm text-gray-500 dark:text-gray-400"> Manage your notification preferences </Text> </View> {/* Divider */} <View className="my-4 h-px bg-gray-200 dark:bg-gray-700" /> {/* Secondary action */} <Pressable className="rounded-lg bg-gray-100 px-4 py-3 active:bg-gray-200 dark:bg-gray-800 dark:active:bg-gray-700"> <Text className="text-center text-gray-700 dark:text-gray-300"> Sign Out </Text> </Pressable> </View> </ScrollView> ); } ``` **Why good:** Every element has both light and dark variants, `active:` and `dark:active:` for press states in both modes, no conditional JS logic needed --- ## Pattern 2: Manual Theme Toggle with Persistence ```tsx import { useEffect, useCallback } from "react"; import { View, Text, Pressable } from "react-native"; import { useColorScheme } from "nativewind"; import AsyncStorage from "@react-native-async-storage/async-storage"; const THEME_STORAGE_KEY = "user-theme-preference"; type ThemeOption = "light" | "dark" | "system"; export function ThemeSelector() { const { colorScheme, setColorScheme } = useColorScheme(); // Restore persisted theme on mount useEffect(() => { const restoreTheme = async () => { const saved = await AsyncStorage.getItem(THEME_STORAGE_KEY); if (saved === "light" || saved === "dark" || saved === "system") { setColorScheme(saved); } }; restoreTheme(); }, [setColorScheme]); const selectTheme = useCallback( async (theme: ThemeOption) => { setColorScheme(theme); await AsyncStorage.setItem(THEME_STORAGE_KEY, theme); }, [setColorScheme], ); const options: ThemeOption[] = ["light", "dark", "system"]; return ( <View className="gap-2 p-4"> <Text className="mb-2 text-lg font-bold text-gray-900 dark:text-white"> Appearance </Text> {options.map((option) => { const isActive = option === "system" ? colorScheme === undefined : option === colorScheme; return ( <Pressable key={option} className={`rounded-lg px-4 py-3 ${ isActive ? "bg-blue-500" : "bg-gray-100 active:bg-gray-200 dark:bg-gray-800 dark:active:bg-gray-700" }`} onPress={() => selectTheme(option)} > <Text className={`text-center font-medium capitalize ${ isActive ? "text-white" : "text-gray-900 dark:text-white" }`} > {option} </Text> </Pressable> ); })} </View> ); } ``` **Why good:** Three-way toggle (light/dark/system), persists to AsyncStorage, restores on mount, `setColorScheme("system")` returns to device preference --- ## Pattern 3: Runtime Theme Switching with vars() Use `vars()` for brand-level theming that goes beyond light/dark. CSS variables flow through React Context to all children. ```tsx import { View, Text, Pressable } from "react-native"; import { vars, useColorScheme } from "nativewind"; // Define theme objects as named constants const THEMES = { ocean: { light: vars({ "--color-primary": "#0ea5e9", "--color-primary-text": "#ffffff", "--color-surface": "#f0f9ff", "--color-surface-text": "#0c4a6e", }), dark: vars({ "--color-primary": "#38bdf8", "--color-primary-text": "#ffffff", "--color-surface": "#0c4a6e", "--color-surface-text": "#e0f2fe", }), }, forest: { light: vars({ "--color-primary": "#16a34a", "--color-primary-text": "#ffffff", "--color-surface": "#f0fdf4", "--color-surface-text": "#14532d", }), dark: vars({ "--color-primary": "#4ade80", "--color-primary-text": "#ffffff", "--color-surface": "#14532d", "--color-surface-text": "#dcfce7", }), }, } as const; type ThemeName = keyof typeof THEMES; interface ThemeProviderProps { theme: ThemeName; children: React.ReactNode; } export function ThemeProvider({ theme, children }: ThemeProviderProps) { const { colorScheme } = useColorScheme(); const mode = colorScheme === "dark" ? "dark" : "light"; const themeVars = THEMES[theme][mode]; return ( <View style={themeVars} className="flex-1"> {children} </View> ); } // Components reference CSS variables -- no prop drilling needed export function ThemedButton({ label, onPress, }: { label: string; onPress: () => void; }) { return ( <Pressable className="rounded-lg bg-[--color-primary] px-4 py-3 active:opacity-80" onPress={onPress} > <Text className="text-center font-semibold text-[--color-primary-text]"> {label} </Text> </Pressable> ); } export function ThemedCard({ title, body }: { title: string; body: string }) { return ( <View className="rounded-xl bg-[--color-surface] p-4"> <Text className="text-lg font-bold text-[--color-surface-text]"> {title} </Text> <Text className="mt-1 text-[--color-surface-text]">{body}</Text> </View> ); } ``` **Why good:** Brand themes compose with light/dark mode (2x2 matrix), children reference variables without knowing the theme, switching theme re-renders only via Context change --- ## Pattern 4: Accessing CSS Variables in JavaScript Use `useUnstableNativeVariable()` when a third-party component needs a theme color as a direct prop value (not via className). ```tsx import { ActivityIndicator, View, Text } from "react-native"; import { vars, useUnstableNativeVariable } from "nativewind"; const theme = vars({ "--color-primary": "#3b82f6", "--color-accent": "#f59e0b", }); export function LoadingScreen() { return ( <View style={theme} className="flex-1 items-center justify-center bg-white dark:bg-gray-900" > <ThemedLoader /> <Text className="mt-4 text-[--color-primary]">Loading...</Text> </View> ); } function ThemedLoader() { // ActivityIndicator.color doesn't accept className -- read variable directly const primaryColor = useUnstableNativeVariable("--color-primary"); return <ActivityIndicator size="large" color={primaryColor} />; } ``` **Why good:** `useUnstableNativeVariable` bridges CSS variables to props that only accept string/number values, keeps theme centralized in vars() object **Gotcha:** The `useUnstableNativeVariable` API is marked unstable and may change in future NativeWind versions. Use it sparingly -- only when a component genuinely cannot accept className for a color/value prop. --- ## Pattern 5: Tailwind Config Theme Extension Extend the default theme in `tailwind.config.js` for compile-time tokens. These are resolved at build time with zero runtime cost. ```javascript /** @type {import('tailwindcss').Config} */ module.exports = { content: ["./app/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}"], presets: [require("nativewind/preset")], theme: { extend: { colors: { brand: { 50: "#eff6ff", 100: "#dbeafe", 500: "#3b82f6", 600: "#2563eb", 700: "#1d4ed8", 900: "#1e3a5a", }, }, spacing: { "safe-top": "env(safe-area-inset-top)", "safe-bottom": "env(safe-area-inset-bottom)", }, borderRadius: { card: "12px", }, }, }, plugins: [], }; ``` ```tsx // Usage -- custom tokens work like built-in Tailwind classes <View className="rounded-card bg-brand-50 p-4 dark:bg-brand-900"> <Text className="text-brand-700 dark:text-brand-100">Branded content</Text> </View> ``` **Why good:** Custom tokens resolved at compile time (zero runtime cost), consistent naming across components, safe area insets as spacing values
-
-
reference.md 6.8 KB
# NativeWind Quick Reference > Decision frameworks, API cheat sheet, and migration notes. See [SKILL.md](SKILL.md) for red flags and anti-patterns. --- ## Decision Framework ### When to Use NativeWind ``` Need Tailwind CSS utility classes in React Native? ├─ YES → NativeWind └─ NO → StyleSheet.create (zero overhead) Need zero runtime overhead? ├─ YES → StyleSheet.create (0ms) ├─ Acceptable ~2ms → NativeWind (compiled) └─ Runtime parsing OK → twrnc (~8-15ms, pure runtime) Need web + native from same codebase? ├─ YES → NativeWind (CSS on web, StyleSheet on native) └─ NO → Either NativeWind or StyleSheet.create ``` ### Styling Third-Party Components ``` Does the component accept a className prop already? ├─ YES → Use it directly (no setup needed) └─ NO → Does it have multiple style props (style, contentContainerStyle)? ├─ YES → remapProps (lightweight, maps className to style props) └─ NO → Does a style attribute need to become a prop? ├─ YES → cssInterop (extracts style attributes to props) │ Example: TextInput placeholderTextColor from className └─ NO → remapProps with simple mapping ``` ### Dark Mode Strategy ``` Follow system preference? ├─ YES → Use dark: prefix classes (automatic) │ └─ Expo: Ensure userInterfaceStyle: "automatic" in app.json └─ Need manual toggle? ├─ Import useColorScheme from "nativewind" ├─ Call toggleColorScheme() or setColorScheme("dark"|"light"|"system") └─ Persist choice to AsyncStorage ``` ### Theming Strategy ``` Static theme (compile-time)? ├─ YES → Customize tailwind.config.js theme.extend └─ NO → Need runtime theme switching? ├─ YES → vars() with CSS variables │ ├─ Define theme objects: vars({ "--color-primary": "#3b82f6" }) │ ├─ Apply to ancestor: <View style={brandTheme}> │ ├─ Reference in children: className="text-[--color-primary]" │ └─ Read in JS: useUnstableNativeVariable("--color-primary") └─ Need multiple brand themes? └─ Combine vars() + useColorScheme for brand + light/dark matrix ``` --- ## API Cheat Sheet ### Core APIs | API | Import | Purpose | | ----------------------------- | ------------------ | ------------------------------------------------------ | | `useColorScheme()` | `nativewind` | Read/set color scheme (light/dark/system) | | `vars()` | `nativewind` | Set CSS variables as style object | | `useUnstableNativeVariable()` | `nativewind` | Read resolved CSS variable value in JS | | `cssInterop()` | `nativewind` | Tag third-party component for full style interop | | `remapProps()` | `nativewind` | Map className props to style props (lightweight) | | `colorScheme` | `nativewind` | Module-level color scheme control (outside components) | | `withNativeWind()` | `nativewind/metro` | Metro config wrapper | ### useColorScheme Return Values ```typescript const { colorScheme, // "light" | "dark" setColorScheme, // (scheme: "light" | "dark" | "system") => void toggleColorScheme, // () => void -- switches between light and dark } = useColorScheme(); ``` ### Platform Prefixes | Prefix | Target | | ---------- | ------------------------ | | `ios:` | iOS only | | `android:` | Android only | | `web:` | Web only | | `windows:` | Windows only | | `osx:` | macOS only | | `native:` | All platforms except web | ### State Prefixes | Prefix | Behavior | | --------- | --------------------------------------- | | `dark:` | Dark color scheme active | | `active:` | Component being pressed | | `hover:` | Pointer hovering (web, pointer devices) | | `focus:` | Component focused | --- ## Configuration Reference ### tailwind.config.js ```javascript /** @type {import('tailwindcss').Config} */ module.exports = { content: [ "./App.tsx", "./app/**/*.{js,jsx,ts,tsx}", "./components/**/*.{js,jsx,ts,tsx}", "./screens/**/*.{js,jsx,ts,tsx}", ], presets: [require("nativewind/preset")], theme: { extend: { // Custom values here }, }, plugins: [], }; ``` ### Peer Dependencies (v4) ``` nativewind tailwindcss ^3.4.17 react-native-reanimated react-native-safe-area-context ``` ### TypeScript Declaration ```typescript // nativewind-env.d.ts (do NOT name it nativewind.d.ts) /// <reference types="nativewind/types" /> ``` --- ## Migration Notes ### From v2 to v4 Key breaking changes: | v2 | v4 | | ------------------------------------------ | ---------------------------------------- | | `styled()` wrapper | Removed -- className works directly | | Babel plugin approach | JSX import source transform | | `NativeWindStyleSheet` | Renamed to `StyleSheet` | | `gap-` polyfill | Compiles to native `columnGap`/`rowGap` | | rem = 16 everywhere | rem = 14 on native, 16 on web | | `divide-` / `space-` | Temporarily unavailable | | className not accessible inside components | className accessible (enables clsx, cva) | ### From v4 to v5 (Preview) When NativeWind v5 stabilizes: | v4 | v5 | | ------------------------------- | ----------------------------------------- | | `cssInterop()` / `remapProps()` | Unified `styled()` API | | `vars()` for theming | `VariableContextProvider` component | | Custom JSX transform | Import rewrite system | | Tailwind CSS v3.4 config | Tailwind CSS v4.1+ with new import syntax | | Requires RN 0.73+ | Requires RN 0.81+ | | `platformSelect()` JS function | CSS media queries | ### From StyleSheet.create to NativeWind ```tsx // Before: StyleSheet.create const styles = StyleSheet.create({ container: { flex: 1, padding: 16, backgroundColor: "#fff" }, title: { fontSize: 18, fontWeight: "bold", color: "#111" }, }); <View style={styles.container}> <Text style={styles.title}>Hello</Text> </View> // After: NativeWind className <View className="flex-1 bg-white p-4"> <Text className="text-lg font-bold text-gray-900">Hello</Text> </View> ``` -
SKILL.md 19.1 KB
--- name: mobile-styling-nativewind description: NativeWind v4+ - Tailwind CSS utility classes for React Native, className prop, CSS variables, dark mode, platform prefixes, animations, theming, third-party component integration --- # NativeWind Patterns > **Quick Guide:** NativeWind brings Tailwind CSS utility classes to React Native via `className` prop. Styles compile to `StyleSheet.create` at build time with a lightweight runtime for conditional logic (dark mode, hover, focus). Always declare both light AND dark styles (no CSS cascade in RN). Use `vars()` for runtime theming with CSS variables. Platform prefixes (`ios:`, `android:`, `native:`) replace `Platform.select` for styling. Use `remapProps` for third-party components with multiple style props; reserve `cssInterop` for components needing style-to-prop extraction. --- <critical_requirements> ## CRITICAL: Before Using This Skill > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST always declare BOTH light and dark styles -- `className="text-black dark:text-white"` not just `className="dark:text-white"` -- React Native has no CSS cascade)** **(You MUST use `remapProps` for third-party components with multiple style props and `cssInterop` ONLY when style attributes need extraction to props -- NEVER use either for your own custom components)** **(You MUST import `"./global.css"` at your app entry point -- without it no styles render)** **(You MUST add `/// <reference types="nativewind/types" />` in a `nativewind-env.d.ts` file for TypeScript className support)** **(You MUST use `nativewind/preset` in `tailwind.config.js` presets -- without it platform-specific features break)** </critical_requirements> --- **Auto-detection:** NativeWind, nativewind, className on React Native components, nativewind/preset, nativewind/babel, nativewind/metro, withNativeWind, cssInterop, remapProps, vars(), useColorScheme from nativewind, useUnstableNativeVariable, dark: prefix in React Native, ios: prefix, android: prefix, native: prefix, global.css tailwind directives, nativewind-env.d.ts **When to use:** - Styling React Native components with Tailwind CSS utility classes - Implementing dark mode with automatic system detection or manual toggle - Creating dynamic themes with CSS variables via `vars()` - Applying platform-specific styles with `ios:`/`android:`/`native:` prefixes - Integrating className support with third-party React Native libraries - Adding transitions and animations to React Native components **Key patterns covered:** - className prop usage and custom component patterns - Dark mode with `useColorScheme` (system preference and manual toggle) - CSS variables for runtime theming via `vars()` and `useUnstableNativeVariable()` - Platform prefixes (`ios:`, `android:`, `web:`, `native:`) for cross-platform styling - Third-party component integration (`remapProps` vs `cssInterop`) - Animations and transitions (experimental, powered by react-native-reanimated) - Variant components with class merging libraries **When NOT to use:** - Web-only React projects (use standard Tailwind CSS) - Projects that need zero runtime overhead (use `StyleSheet.create` directly) - Apps on legacy React Native architecture that cannot adopt New Architecture dependencies **Detailed Resources:** - [examples/core.md](examples/core.md) - className usage, custom components, variants, conditional styling - [examples/theming.md](examples/theming.md) - Dark mode, CSS variables, theme switching, useColorScheme - [examples/platform-and-interop.md](examples/platform-and-interop.md) - Platform prefixes, cssInterop, remapProps, third-party integration - [reference.md](reference.md) - Decision frameworks, API cheat sheet, migration notes --- <philosophy> ## Philosophy NativeWind bridges Tailwind CSS and React Native by compiling utility classes into `StyleSheet.create` objects at build time and providing a runtime for conditional style logic (dark mode, hover states, focus). The `className` prop works directly on React Native core components via a JSX transform -- no wrapper components needed. **Core principles:** 1. **Build-time compilation** -- Tailwind classes compile to native `StyleSheet.create` objects, keeping runtime overhead minimal (~2ms per render vs 0ms for raw StyleSheet) 2. **className is first-class** -- The JSX transform makes `className` available inside your components, enabling compatibility with class merging libraries (clsx, tailwind-variants, cva) 3. **No CSS cascade on native** -- React Native does not cascade styles. You must always declare both sides of conditional styles (`text-black dark:text-white`, not just `dark:text-white`) 4. **Platform prefixes over Platform.select** -- For styling concerns, `ios:shadow-lg android:elevation-4` is more declarative than wrapping in `Platform.select` 5. **Custom components just merge classNames** -- Never use `cssInterop` or `remapProps` on your own components. Simply accept a `className` prop and merge it with defaults 6. **Third-party integration is explicit** -- Use `remapProps` (lightweight) or `cssInterop` (full runtime) only for third-party components that need className support **Architecture:** NativeWind's JSX transform intercepts component rendering. On native, it resolves className strings into `StyleSheet.create` IDs and applies conditional logic. On web, it passes className through as standard CSS. This means: - `react-native-reanimated` is a peer dependency (powers animations and transitions) - `react-native-safe-area-context` is a peer dependency (used for safe area utilities) - `tailwindcss ^3.4` is required (v4 uses Tailwind CSS v3 config format; NativeWind v5 targets Tailwind CSS v4) - Inline `style` props merge with className-based styles, with inline taking precedence **rem units:** NativeWind uses rem: 14 on native (matching React Native's default 14px font size) and rem: 16 on web. Specify `10px` in theme config and let NativeWind normalize per platform. </philosophy> --- <patterns> ## Core Patterns ### Pattern 1: className on React Native Components All React Native core components accept `className` after installing NativeWind. Styles compile at build time -- no runtime string parsing in production. ```tsx import { View, Text, Pressable } from "react-native"; export function Card({ title, onPress, }: { title: string; onPress: () => void; }) { return ( <View className="rounded-lg bg-white p-4 shadow-md dark:bg-gray-800"> <Text className="text-lg font-bold text-gray-900 dark:text-white"> {title} </Text> <Pressable className="mt-3 rounded-md bg-blue-500 px-4 py-2 active:bg-blue-600" onPress={onPress} > <Text className="text-center font-medium text-white">View Details</Text> </Pressable> </View> ); } ``` **Why good:** Both light and dark variants declared, `active:` pseudo-class for press feedback, no inline style objects, compile-time resolution See [examples/core.md](examples/core.md) for custom component patterns with className merging and variant props. --- ### Pattern 2: Custom Components with className Merging Accept a `className` prop and merge it with defaults. Never use `cssInterop` or `remapProps` on your own components. ```tsx interface BadgeProps { label: string; variant?: "info" | "success" | "warning" | "error"; className?: string; } const VARIANT_CLASSES = { info: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200", success: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200", warning: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200", error: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200", } as const; export function Badge({ label, variant = "info", className }: BadgeProps) { return ( <Text className={`rounded-full px-2 py-1 text-xs font-medium ${VARIANT_CLASSES[variant]} ${className ?? ""}`} > {label} </Text> ); } ``` **Why good:** className prop enables external overrides, variant map is a named constant, both light and dark styles declared per variant **When to use:** For complex variant logic, use a class merging library (clsx, tailwind-variants, cva) to handle conditional classes and conflict resolution. See [examples/core.md](examples/core.md) for patterns with clsx and tailwind-variants. --- ### Pattern 3: Dark Mode with useColorScheme NativeWind follows the system color scheme by default. Use `dark:` prefix for dark-mode styles. Use `useColorScheme()` from `nativewind` to read or manually set the scheme. ```tsx import { useColorScheme } from "nativewind"; import { View, Text, Pressable } from "react-native"; export function ThemeToggle() { const { colorScheme, toggleColorScheme } = useColorScheme(); return ( <View className="flex-1 items-center justify-center bg-white dark:bg-gray-900"> <Text className="text-lg text-gray-900 dark:text-white"> Current: {colorScheme} </Text> <Pressable className="mt-4 rounded-md bg-gray-200 px-4 py-2 dark:bg-gray-700" onPress={toggleColorScheme} > <Text className="text-gray-900 dark:text-white">Toggle Theme</Text> </Pressable> </View> ); } ``` **Why good:** `useColorScheme` from nativewind (not react-native) provides `toggleColorScheme` and `setColorScheme`, system preference followed by default **Gotcha:** For Expo apps, `userInterfaceStyle` must be set to `"automatic"` in `app.json` for system preference to work. See [examples/theming.md](examples/theming.md) for manual theme persistence and multi-theme patterns with `vars()`. --- ### Pattern 4: CSS Variables for Runtime Theming Use `vars()` to set CSS variable values that flow down the component tree via React Context. Use `useUnstableNativeVariable()` to read resolved values in JavaScript. ```tsx import { vars, useUnstableNativeVariable } from "nativewind"; import { View, Text, ActivityIndicator } from "react-native"; const brandTheme = vars({ "--color-primary": "#3b82f6", "--color-primary-text": "#ffffff", "--color-surface": "#f8fafc", }); export function ThemedScreen() { return ( <View style={brandTheme} className="flex-1 bg-[--color-surface]"> <Text className="text-lg font-bold text-[--color-primary]"> Branded Content </Text> <ThemedSpinner /> </View> ); } // useUnstableNativeVariable reads resolved CSS variable values function ThemedSpinner() { const primaryColor = useUnstableNativeVariable("--color-primary"); return <ActivityIndicator color={primaryColor} />; } ``` **Why good:** `vars()` returns a style object applied to ancestor, children resolve variables via context, `useUnstableNativeVariable` bridges CSS variables to props that don't accept className See [examples/theming.md](examples/theming.md) for multi-brand theming and combining `vars()` with `useColorScheme`. --- ### Pattern 5: Platform Prefixes Use `ios:`, `android:`, `web:`, and `native:` prefixes to apply styles per platform. The `native:` prefix targets all platforms except web. ```tsx <View className="p-4 ios:pt-12 android:pt-8"> <Text className="text-base ios:font-semibold android:font-bold"> Platform-aware text </Text> <View className="ios:shadow-lg android:elevation-4 rounded-lg bg-white p-4"> <Text className="text-gray-900">Card with platform shadows</Text> </View> </View> ``` **Why good:** Declarative platform branching in className, no Platform.select boilerplate for styling, shadows handled correctly per platform (iOS ignores elevation, Android ignores shadow props) See [examples/platform-and-interop.md](examples/platform-and-interop.md) for complex platform patterns. --- ### Pattern 6: Third-Party Component Integration Use `remapProps` (lightweight, no runtime cost) to map className props to style props. Use `cssInterop` (full runtime, performance cost) only when style attributes need extraction to individual props. ```tsx import { remapProps, cssInterop } from "nativewind"; import { FlatList, TextInput } from "react-native"; // remapProps: maps className strings to style props (lightweight) remapProps(FlatList, { className: "style", contentContainerClassName: "contentContainerStyle", columnWrapperClassName: "columnWrapperStyle", }); // cssInterop: extracts style attributes to props (full runtime) cssInterop(TextInput, { className: { target: "style", nativeStyleToProp: { textAlign: true }, }, placeholderClassName: { target: false, nativeStyleToProp: { color: "placeholderTextColor" }, }, }); ``` **Why good:** `remapProps` has zero style resolution overhead, `cssInterop` used only when style attributes must become individual props (like placeholderTextColor) **When to use:** `remapProps` for components with multiple style props (FlatList, ScrollView). `cssInterop` only when a third-party component needs style properties extracted as individual props (TextInput placeholderTextColor, StatusBar backgroundColor). See [examples/platform-and-interop.md](examples/platform-and-interop.md) for TypeScript declarations, SVG integration, and the decision framework. --- ### Pattern 7: Animations and Transitions (Experimental) NativeWind supports Tailwind animation and transition classes, powered by react-native-reanimated under the hood. No need for `Animated.View` -- NativeWind creates animated versions automatically. ```tsx // Built-in animation classes <View className="animate-spin h-8 w-8 rounded-full border-2 border-blue-500 border-t-transparent" /> <View className="animate-pulse rounded-lg bg-gray-200 p-4 dark:bg-gray-700" /> <View className="animate-bounce"> <Text className="text-2xl">Bounce</Text> </View> // Transitions: smooth interpolation when classes change <Pressable className="rounded-md bg-blue-500 p-4 transition-colors duration-200 active:bg-blue-700"> <Text className="text-white">Press me</Text> </Pressable> ``` **Why good:** Standard Tailwind animation classes work without Animated wrappers, transitions powered by reanimated for native performance **Gotcha:** Animation and transition support is experimental on native. Animations currently only work with the `style` prop (not all mapped props). Transitions for `shadow` are web-only. </patterns> --- <decision_framework> ## Decision Framework ### Styling Approach ``` Need Tailwind utility classes in React Native? ├─ YES → NativeWind └─ NO → StyleSheet.create (zero overhead) Need zero runtime overhead? ├─ YES → StyleSheet.create (0ms) ├─ Acceptable ~2ms → NativeWind (compiled) └─ Runtime parsing OK → twrnc (~8-15ms, pure runtime) ``` ### Third-Party Component Integration ``` Does the component accept className already? ├─ YES → Use it directly (no setup needed) └─ NO → Does it have multiple style props (style, contentContainerStyle)? ├─ YES → remapProps (lightweight, zero overhead) └─ NO → Does a style attribute need to become a prop? ├─ YES → cssInterop (extracts style attributes to props) └─ NO → remapProps with simple mapping ``` ### Theming Strategy ``` Static theme (compile-time)? ├─ YES → Customize tailwind.config.js theme.extend └─ NO → Need runtime theme switching? ├─ YES → vars() with CSS variables └─ Need multiple brand themes? └─ Combine vars() + useColorScheme for brand + light/dark matrix ``` See [reference.md](reference.md) for full API cheat sheet, dark mode strategy tree, and migration notes. </decision_framework> --- <red_flags> ## RED FLAGS **High Priority Issues:** - Declaring only dark styles without light counterpart (`dark:text-white` without `text-black`) -- React Native has no CSS cascade, so the light variant will have no text color - Using `cssInterop` or `remapProps` on your own custom components -- these are exclusively for third-party components. Your own components should accept and merge `className` directly - Missing `import "./global.css"` at app entry point -- no styles will render without it - Missing `nativewind/preset` in tailwind.config.js presets -- platform prefixes, CSS variable support, and other NativeWind-specific features will not work - Using `useColorScheme` from `react-native` instead of `nativewind` -- the nativewind version provides `setColorScheme` and `toggleColorScheme` **Medium Priority Issues:** - Using `cssInterop` when `remapProps` would suffice -- `cssInterop` has runtime overhead for style resolution, event handlers, and context injection - Naming the TypeScript declaration file `nativewind.d.ts` -- it conflicts with the package's own types. Use `nativewind-env.d.ts` - Not setting `userInterfaceStyle: "automatic"` in Expo app.json -- system dark mode preference will not be detected - Using web-designed breakpoints (`sm:`, `md:`, `lg:`) without customizing for mobile -- NativeWind's default breakpoints are web-centric (640px, 768px, 1024px) and may not match mobile screen sizes **Gotchas & Edge Cases:** - Inline `style` prop takes precedence over `className` styles due to CSS specificity -- `<Text className="text-white" style={{ color: "black" }} />` renders black - `rem` units differ between platforms: 14 on native (RN default font size), 16 on web -- use px values in theme config for consistency - Color opacity is disabled by default for performance on native -- enable via `corePlugins` in tailwind.config.js if you need `bg-blue-500/50` syntax - `vars()` values propagate via React Context, not actual CSS -- they only flow to React children, not portal-rendered content - `useUnstableNativeVariable` API may change in future versions (prefixed "unstable" intentionally) - Animations and transitions are experimental on native -- `transition-shadow` is web-only, and animation performance is actively being improved - `gap-` compiles to native `columnGap`/`rowGap` in v4 (v2 used a polyfill) -- verify your React Native version supports gap layout props - `divide-` and `space-` utilities are temporarily unavailable in v4 - NativeWind v5 (in preview) deprecates `cssInterop`/`remapProps` in favor of `styled()`, and `vars()` in favor of `VariableContextProvider` -- check migration guide when upgrading - Tailwind CSS v4 is NOT yet supported by NativeWind v4 -- NativeWind v4 uses Tailwind CSS v3.4 config format </red_flags> --- <critical_reminders> ## CRITICAL REMINDERS > **All code must follow project conventions in CLAUDE.md** **(You MUST always declare BOTH light and dark styles -- `className="text-black dark:text-white"` not just `className="dark:text-white"` -- React Native has no CSS cascade)** **(You MUST use `remapProps` for third-party components with multiple style props and `cssInterop` ONLY when style attributes need extraction to props -- NEVER use either for your own custom components)** **(You MUST import `"./global.css"` at your app entry point -- without it no styles render)** **(You MUST add `/// <reference types="nativewind/types" />` in a `nativewind-env.d.ts` file for TypeScript className support)** **(You MUST use `nativewind/preset` in `tailwind.config.js` presets -- without it platform-specific features break)** **Failure to follow these rules will cause invisible styles, broken dark mode, TypeScript errors on className props, and platform-specific rendering failures.** </critical_reminders>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.