expo-tailwind-setup

Universal Tailwind CSS v4 styling for Expo apps across iOS, Android, and Web. Integrates Tailwind CSS v4 with react-native-css and NativeWind v5 via Metro transformer, eliminating the need for Babel configuration Provides reusable CSS-wrapped component library (View, Text, ScrollView, Pressable, TextInput, Image, Link) that apply className styles to native elements Supports platform-specific styling through CSS media queries, custom theme variables via @theme blocks, and Apple semantic colors with platformColor() and light-dark() functions Includes CSS variable hooks for accessing theme values in JavaScript and configuration examples for Metro, PostCSS, and global CSS setup

INSTALLATION
npx skills add https://github.com/expo/skills --skill expo-tailwind-setup
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

Tailwind CSS Setup for Expo with react-native-css

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.

Overview

This setup uses:

  • Tailwind CSS v4 - Modern CSS-first configuration
  • react-native-css - CSS runtime for React Native
  • NativeWind v5 - Metro transformer for Tailwind in React Native
  • @tailwindcss/postcss - PostCSS plugin for Tailwind v4

Installation

# 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"

  }

}
  • autoprefixer is not needed in Expo because of lightningcss
  • postcss is included in expo by default

Configuration Files

Metro Config

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,

});

PostCSS Config

Create postcss.config.mjs:

// postcss.config.mjs

export default {

  plugins: {

    "@tailwindcss/postcss": {},

  },

};

Global CSS

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;

  }

}

IMPORTANT: No Babel Config Needed

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",

//     ],

//   };

// };

CSS Component Wrappers

Since react-native-css requires explicit CSS element wrapping, create reusable components:

Main 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> &#x26; { 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> &#x26; {

  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> &#x26; { className?: string }

) => {

  return useCssElement(RNText, props, { className: "style" });

};

Text.displayName = "CSS(Text)";

// ScrollView

export const ScrollView = (

  props: React.ComponentProps<typeof RNScrollView> &#x26; {

    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> &#x26; { className?: string }

) => {

  return useCssElement(RNPressable, props, { className: "style" });

};

Pressable.displayName = "CSS(Pressable)";

// TextInput

export const TextInput = (

  props: React.ComponentProps<typeof RNTextInput> &#x26; { className?: string }

) => {

  return useCssElement(RNTextInput, props, { className: "style" });

};

TextInput.displayName = "CSS(TextInput)";

// AnimatedScrollView

export const AnimatedScrollView = (

  props: React.ComponentProps<typeof Animated.ScrollView> &#x26; {

    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)";

Image Component ( 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> &#x26; { className?: string }

) => {

  return useCssElement(CSSImage, props, { className: "style" });

};

Image.displayName = "CSS(Image)";

Animated Components ( src/tw/animated.tsx )

import * as TW from "./index";

import RNAnimated from "react-native-reanimated";

export const Animated = {

  ...RNAnimated,

  View: RNAnimated.createAnimatedComponent(TW.View),

};

Usage

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>

  );

}

Custom Theme Variables

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;

  }

}

Platform-Specific Styles

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;

  }

}

Apple System Colors with CSS Variables

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>

Using CSS Variables in JavaScript

Use the useCSSVariable hook:

import { useCSSVariable } from "@/tw";

function MyComponent() {

  const blue = useCSSVariable("--sf-blue");

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

}

Key Differences from NativeWind v4 / Tailwind v3

  • No babel.config.js - Configuration is now CSS-first
  • PostCSS plugin - Uses @tailwindcss/postcss instead of tailwindcss
  • CSS imports - Use @import "tailwindcss/..." instead of @tailwind directives
  • Theme config - Use @theme in CSS instead of tailwind.config.js
  • Component wrappers - Must wrap components with useCssElement for className support
  • Metro config - Use withNativewind with different options (inlineVariables: false)

Troubleshooting

Styles not applying

  • Ensure you have the CSS file imported in your app entry
  • Check that components are wrapped with useCssElement
  • Verify Metro config has withNativewind applied

Platform colors not working

  • Use platformColor() in @media ios blocks
  • Fall back to light-dark() for web/Android

TypeScript errors

Add className to component props:

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

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card