This commit is contained in:
8
expo-tailwind-setup/.skillshare-meta.json
Normal file
8
expo-tailwind-setup/.skillshare-meta.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"source": "github.com/expo/skills/tree/main/plugins/expo-app-design/skills/expo-tailwind-setup",
|
||||
"type": "github-subdir",
|
||||
"installed_at": "2026-01-30T02:27:29.274573058Z",
|
||||
"repo_url": "https://github.com/expo/skills.git",
|
||||
"subdir": "plugins/expo-app-design/skills/expo-tailwind-setup",
|
||||
"version": "b631a60"
|
||||
}
|
||||
480
expo-tailwind-setup/SKILL.md
Normal file
480
expo-tailwind-setup/SKILL.md
Normal file
@@ -0,0 +1,480 @@
|
||||
---
|
||||
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
|
||||
---
|
||||
|
||||
# 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
|
||||
|
||||
```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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 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`:
|
||||
|
||||
```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`:
|
||||
|
||||
```js
|
||||
// postcss.config.mjs
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Global CSS
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 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:
|
||||
|
||||
```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",
|
||||
// ],
|
||||
// };
|
||||
// };
|
||||
```
|
||||
|
||||
## CSS Component Wrappers
|
||||
|
||||
Since react-native-css requires explicit CSS element wrapping, create reusable components:
|
||||
|
||||
### Main Components (`src/tw/index.tsx`)
|
||||
|
||||
```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)";
|
||||
```
|
||||
|
||||
### Image Component (`src/tw/image.tsx`)
|
||||
|
||||
```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)";
|
||||
```
|
||||
|
||||
### Animated Components (`src/tw/animated.tsx`)
|
||||
|
||||
```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:
|
||||
|
||||
```tsx
|
||||
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`:
|
||||
|
||||
```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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Platform-Specific Styles
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Apple System Colors with CSS Variables
|
||||
|
||||
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>
|
||||
```
|
||||
|
||||
## Using CSS Variables in JavaScript
|
||||
|
||||
Use the `useCSSVariable` hook:
|
||||
|
||||
```tsx
|
||||
import { useCSSVariable } from "@/tw";
|
||||
|
||||
function MyComponent() {
|
||||
const blue = useCSSVariable("--sf-blue");
|
||||
|
||||
return <View style={{ borderColor: blue }} />;
|
||||
}
|
||||
```
|
||||
|
||||
## Key Differences from NativeWind v4 / Tailwind v3
|
||||
|
||||
1. **No babel.config.js** - Configuration is now CSS-first
|
||||
2. **PostCSS plugin** - Uses `@tailwindcss/postcss` instead of `tailwindcss`
|
||||
3. **CSS imports** - Use `@import "tailwindcss/..."` instead of `@tailwind` directives
|
||||
4. **Theme config** - Use `@theme` in CSS instead of `tailwind.config.js`
|
||||
5. **Component wrappers** - Must wrap components with `useCssElement` for className support
|
||||
6. **Metro config** - Use `withNativewind` with different options (`inlineVariables: false`)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Styles not applying
|
||||
|
||||
1. Ensure you have the CSS file imported in your app entry
|
||||
2. Check that components are wrapped with `useCssElement`
|
||||
3. Verify Metro config has `withNativewind` applied
|
||||
|
||||
### Platform colors not working
|
||||
|
||||
1. Use `platformColor()` in `@media ios` blocks
|
||||
2. Fall back to `light-dark()` for web/Android
|
||||
|
||||
### TypeScript errors
|
||||
|
||||
Add className to component props:
|
||||
|
||||
```tsx
|
||||
type Props = React.ComponentProps<typeof RNView> & { className?: string };
|
||||
```
|
||||
Reference in New Issue
Block a user