npx expo install nativewind react-native-reanimated tailwindcss@^3.4.0 prettier-plugin-tailwindcss rn-icon-mapper expo-symbols @shopify/flash-list class-variance-authority clsx expo-dev-client tailwind-merge
npx tailwindcss init
tailwind.config.js
content
tailwind.config.js
const { hairlineWidth, platformSelect } = require('nativewind/theme');
/** @type {import('tailwindcss').Config} */
module.exports = {
// NOTE: Update this to include the paths to all of your component files.
content: ['./app/**/*.{js,jsx,ts,tsx}', './components/**/*.{js,jsx,ts,tsx}'],
presets: [require('nativewind/preset')],
theme: {
extend: {
colors: {
border: withOpacity('border'),
input: withOpacity('input'),
ring: withOpacity('ring'),
background: withOpacity('background'),
foreground: withOpacity('foreground'),
primary: {
DEFAULT: withOpacity('primary'),
foreground: withOpacity('primary-foreground'),
},
secondary: {
DEFAULT: withOpacity('secondary'),
foreground: withOpacity('secondary-foreground'),
},
destructive: {
DEFAULT: withOpacity('destructive'),
foreground: withOpacity('destructive-foreground'),
},
muted: {
DEFAULT: withOpacity('muted'),
foreground: withOpacity('muted-foreground'),
},
accent: {
DEFAULT: withOpacity('accent'),
foreground: withOpacity('accent-foreground'),
},
popover: {
DEFAULT: withOpacity('popover'),
foreground: withOpacity('popover-foreground'),
},
card: {
DEFAULT: withOpacity('card'),
foreground: withOpacity('card-foreground'),
},
},
borderWidth: {
hairline: hairlineWidth(),
},
},
},
plugins: [],
};
function withOpacity(variableName) {
return ({ opacityValue }) => {
if (opacityValue !== undefined) {
return platformSelect({
ios: `rgb(var(--${variableName}) / ${opacityValue})`,
android: `rgb(var(--android-${variableName}) / ${opacityValue})`,
});
}
return platformSelect({
ios: `rgb(var(--${variableName}))`,
android: `rgb(var(--android-${variableName}))`,
});
};
}
global.css
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 242 242 247;
--foreground: 0 0 0;
--card: 255 255 255;
--card-foreground: 8 28 30;
--popover: 230 230 235;
--popover-foreground: 0 0 0;
--primary: 0 123 254;
--primary-foreground: 255 255 255;
--secondary: 45 175 231;
--secondary-foreground: 255 255 255;
--muted: 175 176 180;
--muted-foreground: 142 142 147;
--accent: 255 40 84;
--accent-foreground: 255 255 255;
--destructive: 255 56 43;
--destructive-foreground: 255 255 255;
--border: 230 230 235;
--input: 210 210 215;
--ring: 230 230 235;
--android-background: 249 249 255;
--android-foreground: 0 0 0;
--android-card: 255 255 255;
--android-card-foreground: 24 28 35;
--android-popover: 215 217 228;
--android-popover-foreground: 0 0 0;
--android-primary: 0 112 233;
--android-primary-foreground: 255 255 255;
--android-secondary: 176 201 255;
--android-secondary-foreground: 20 55 108;
--android-muted: 193 198 215;
--android-muted-foreground: 65 71 84;
--android-accent: 169 73 204;
--android-accent-foreground: 255 255 255;
--android-destructive: 186 26 26;
--android-destructive-foreground: 255 255 255;
--android-border: 215 217 228;
--android-input: 210 210 215;
--android-ring: 215 217 228;
}
@media (prefers-color-scheme: dark) {
:root {
--background: 0 0 0;
--foreground: 255 255 255;
--card: 21 21 24;
--card-foreground: 255 255 255;
--popover: 40 40 42;
--popover-foreground: 255 255 255;
--primary: 3 133 255;
--primary-foreground: 255 255 255;
--secondary: 100 211 254;
--secondary-foreground: 255 255 255;
--muted: 70 70 73;
--muted-foreground: 142 142 147;
--accent: 255 52 95;
--accent-foreground: 255 255 255;
--destructive: 254 67 54;
--destructive-foreground: 255 255 255;
--border: 40 40 42;
--input: 55 55 57;
--ring: 40 40 42;
--android-background: 0 0 0;
--android-foreground: 255 255 255;
--android-card: 16 19 27;
--android-card-foreground: 224 226 237;
--android-popover: 39 42 50;
--android-popover-foreground: 224 226 237;
--android-primary: 3 133 255;
--android-primary-foreground: 255 255 255;
--android-secondary: 28 60 114;
--android-secondary-foreground: 189 209 255;
--android-muted: 216 226 255;
--android-muted-foreground: 139 144 160;
--android-accent: 83 0 111;
--android-accent-foreground: 238 177 255;
--android-destructive: 147 0 10;
--android-destructive-foreground: 255 255 255;
--android-border: 39 42 50;
--android-input: 55 55 57;
--android-ring: 39 42 50;
}
}
}
module.exports = function (api) {
api.cache(true);
const plugins = [];
plugins.push('react-native-worklets/plugin');
return {
presets: [['babel-preset-expo', { jsxImportSource: 'nativewind' }], 'nativewind/babel'],
plugins,
};
};
metro.config.js
npx expo customize metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
const { withNativeWind } = require('nativewind/metro');
// eslint-disable-next-line no-undef
const config = getDefaultConfig(__dirname);
module.exports = withNativeWind(config, {
input: './global.css',
inlineRem: 16,
});
nativewind-env.d.ts
/// <reference types="nativewind/types" />
expo-env.d.ts
.gitignore
/// <reference types="expo/types" />
// NOTE: This file should not be edited and should be in your git ignore
lib
import { useColorScheme as useNativewindColorScheme } from 'nativewind';
import { COLORS } from '@/theme/colors';
function useColorScheme() {
const { colorScheme, setColorScheme } = useNativewindColorScheme();
function toggleColorScheme() {
return setColorScheme(colorScheme === 'light' ? 'dark' : 'light');
}
return {
colorScheme: colorScheme ?? 'light',
isDarkColorScheme: colorScheme === 'dark',
setColorScheme,
toggleColorScheme,
colors: COLORS[colorScheme ?? 'light'],
};
}
export { useColorScheme };
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
theme
import { Platform } from 'react-native';
const IOS_SYSTEM_COLORS = {
white: 'rgb(255, 255, 255)',
black: 'rgb(0, 0, 0)',
light: {
grey6: 'rgb(242, 242, 247)',
grey5: 'rgb(230, 230, 235)',
grey4: 'rgb(210, 210, 215)',
grey3: 'rgb(199, 199, 204)',
grey2: 'rgb(175, 176, 180)',
grey: 'rgb(142, 142, 147)',
background: 'rgb(242, 242, 247)',
foreground: 'rgb(0, 0, 0)',
root: 'rgb(255, 255, 255)',
card: 'rgb(255, 255, 255)',
cardForeground: 'rgb(8, 28, 30)',
popover: 'rgb(230, 230, 235)',
popoverForeground: 'rgb(0, 0, 0)',
destructive: 'rgb(255, 56, 43)',
primary: 'rgb(0, 123, 254)',
primaryForeground: 'rgb(255, 255, 255)',
secondary: 'rgb(45, 175, 231)',
secondaryForeground: 'rgb(255, 255, 255)',
muted: 'rgb(175, 176, 180)',
mutedForeground: 'rgb(142, 142, 147)',
accent: 'rgb(255, 40, 84)',
accentForeground: 'rgb(255, 255, 255)',
border: 'rgb(230, 230, 235)',
input: 'rgb(210, 210, 215)',
ring: 'rgb(230, 230, 235)',
},
dark: {
grey6: 'rgb(21, 21, 24)',
grey5: 'rgb(40, 40, 42)',
grey4: 'rgb(55, 55, 57)',
grey3: 'rgb(70, 70, 73)',
grey2: 'rgb(99, 99, 102)',
grey: 'rgb(142, 142, 147)',
background: 'rgb(0, 0, 0)',
foreground: 'rgb(255, 255, 255)',
root: 'rgb(0, 0, 0)',
card: 'rgb(21, 21, 24)',
cardForeground: 'rgb(255, 255, 255)',
popover: 'rgb(40, 40, 42)',
popoverForeground: 'rgb(255, 255, 255)',
destructive: 'rgb(254, 67, 54)',
primary: 'rgb(3, 133, 255)',
primaryForeground: 'rgb(255, 255, 255)',
secondary: 'rgb(100, 211, 254)',
secondaryForeground: 'rgb(255, 255, 255)',
muted: 'rgb(70, 70, 73)',
mutedForeground: 'rgb(142, 142, 147)',
accent: 'rgb(255, 52, 95)',
accentForeground: 'rgb(255, 255, 255)',
border: 'rgb(40, 40, 42)',
input: 'rgb(55, 55, 57)',
ring: 'rgb(40, 40, 42)',
},
} as const;
const ANDROID_COLORS = {
white: 'rgb(255, 255, 255)',
black: 'rgb(0, 0, 0)',
light: {
grey6: 'rgb(249, 249, 255)',
grey5: 'rgb(215, 217, 228)',
grey4: 'rgb(193, 198, 215)',
grey3: 'rgb(113, 119, 134)',
grey2: 'rgb(65, 71, 84)',
grey: 'rgb(24, 28, 35)',
background: 'rgb(249, 249, 255)',
foreground: 'rgb(0, 0, 0)',
root: 'rgb(255, 255, 255)',
card: 'rgb(255, 255, 255)',
cardForeground: 'rgb(24, 28, 35)',
popover: 'rgb(215, 217, 228)',
popoverForeground: 'rgb(0, 0, 0)',
destructive: 'rgb(186, 26, 26)',
primary: 'rgb(0, 112, 233)',
primaryForeground: 'rgb(255, 255, 255)',
secondary: 'rgb(176, 201, 255)',
secondaryForeground: 'rgb(20, 55, 108)',
muted: 'rgb(193, 198, 215)',
mutedForeground: 'rgb(65, 71, 84)',
accent: 'rgb(169, 73, 204)',
accentForeground: 'rgb(255, 255, 255)',
border: 'rgb(215, 217, 228)',
input: 'rgb(210, 210, 215)',
ring: 'rgb(215, 217, 228)',
},
dark: {
grey6: 'rgb(16, 19, 27)',
grey5: 'rgb(39, 42, 50)',
grey4: 'rgb(49, 53, 61)',
grey3: 'rgb(54, 57, 66)',
grey2: 'rgb(139, 144, 160)',
grey: 'rgb(193, 198, 215)',
background: 'rgb(0, 0, 0)',
foreground: 'rgb(255, 255, 255)',
root: 'rgb(0, 0, 0)',
card: 'rgb(16, 19, 27)',
cardForeground: 'rgb(224, 226, 237)',
popover: 'rgb(39, 42, 50)',
popoverForeground: 'rgb(224, 226, 237)',
destructive: 'rgb(147, 0, 10)',
primary: 'rgb(3, 133, 255)',
primaryForeground: 'rgb(255, 255, 255)',
secondary: 'rgb(28, 60, 114)',
secondaryForeground: 'rgb(189, 209, 255)',
muted: 'rgb(216, 226, 255)',
mutedForeground: 'rgb(139, 144, 160)',
accent: 'rgb(83, 0, 111)',
accentForeground: 'rgb(238, 177, 255)',
border: 'rgb(39, 42, 50)',
input: 'rgb(55, 55, 57)',
ring: 'rgb(39, 42, 50)',
},
} as const;
const COLORS = Platform.OS === 'ios' ? IOS_SYSTEM_COLORS : ANDROID_COLORS;
export { COLORS };
import { Theme, DefaultTheme, DarkTheme } from '@react-navigation/native';
import { COLORS } from './colors';
const NAV_THEME: { light: Theme; dark: Theme } = {
light: {
dark: false,
colors: {
background: COLORS.light.background,
border: COLORS.light.grey5,
card: COLORS.light.card,
notification: COLORS.light.destructive,
primary: COLORS.light.primary,
text: COLORS.black,
},
fonts: DefaultTheme.fonts,
},
dark: {
dark: true,
colors: {
background: COLORS.dark.background,
border: COLORS.dark.grey5,
card: COLORS.dark.grey6,
notification: COLORS.dark.destructive,
primary: COLORS.dark.primary,
text: COLORS.white,
},
fonts: DarkTheme.fonts,
},
};
export { NAV_THEME };
components/nativewindui
import { VariantProps, cva } from 'class-variance-authority';
import * as React from 'react';
import { Text as RNText } from 'react-native';
import { cn } from '@/lib/cn';
const textVariants = cva('text-foreground', {
variants: {
variant: {
largeTitle: 'text-4xl',
title1: 'text-2xl',
title2: 'text-[22px] leading-7',
title3: 'text-xl',
heading: 'text-[17px] leading-6 font-semibold',
body: 'text-[17px] leading-6',
callout: 'text-base',
subhead: 'text-[15px] leading-6',
footnote: 'text-[13px] leading-5',
caption1: 'text-xs',
caption2: 'text-[11px] leading-4',
},
color: {
primary: '',
secondary: 'text-secondary-foreground/90',
tertiary: 'text-muted-foreground/90',
quarternary: 'text-muted-foreground/50',
},
},
defaultVariants: {
variant: 'body',
color: 'primary',
},
});
const TextClassContext = React.createContext<string | undefined>(undefined);
function Text({
className,
variant,
color,
...props
}: React.ComponentPropsWithoutRef<typeof RNText> & VariantProps<typeof textVariants>) {
const textClassName = React.useContext(TextClassContext);
return (
<RNText className={cn(textVariants({ variant, color }), textClassName, className)} {...props} />
);
}
export { Text, TextClassContext, textVariants };
import { cssInterop } from 'nativewind';
import { Icon } from './Icon';
cssInterop(Icon, {
className: {
target: 'style',
nativeStyleToProp: {
color: 'color',
height: 'size',
width: 'size',
},
},
});
export { Icon };
import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import {
SF_SYMBOLS_TO_MATERIAL_COMMUNITY_ICONS,
SF_SYMBOLS_TO_MATERIAL_ICONS,
} from 'rn-icon-mapper';
import type { IconProps } from './types';
import { useColorScheme } from '../../../lib/useColorScheme';
function Icon({
name,
materialCommunityIcon,
materialIcon,
sfSymbol: _sfSymbol,
size = 24,
...props
}: IconProps) {
const { colors } = useColorScheme();
const defaultColor = colors.foreground;
if (materialCommunityIcon) {
return (
<MaterialCommunityIcons
size={size}
color={defaultColor}
{...props}
{...materialCommunityIcon}
/>
);
}
if (materialIcon) {
return <MaterialIcons size={size} color={defaultColor} {...props} {...materialIcon} />;
}
const materialCommunityIconName =
SF_SYMBOLS_TO_MATERIAL_COMMUNITY_ICONS[
name as keyof typeof SF_SYMBOLS_TO_MATERIAL_COMMUNITY_ICONS
];
if (materialCommunityIconName) {
return (
<MaterialCommunityIcons
name={materialCommunityIconName}
size={size}
color={defaultColor}
{...props}
/>
);
}
const materialIconName =
SF_SYMBOLS_TO_MATERIAL_ICONS[name as keyof typeof SF_SYMBOLS_TO_MATERIAL_ICONS];
if (materialIconName) {
return <MaterialIcons name={materialIconName} size={size} color={defaultColor} {...props} />;
}
return <MaterialCommunityIcons name="help" size={size} color={defaultColor} {...props} />;
}
export { Icon };
import { SymbolView } from 'expo-symbols';
import type { IconProps } from './types';
import { useColorScheme } from '../../../lib/useColorScheme';
function Icon({
materialCommunityIcon: _materialCommunityIcon,
materialIcon: _materialIcon,
sfSymbol,
name,
color,
size = 24,
...props
}: IconProps) {
const { colors } = useColorScheme();
return (
<SymbolView
name={name ?? 'questionmark'}
tintColor={rgbaToHex(color ?? colors.foreground)}
size={size}
resizeMode="scaleAspectFit"
{...props}
{...sfSymbol}
/>
);
}
export { Icon };
// NOTE: seems like the need to convert rgba to hex color is a bug in expo-symbols, accordion to the docs, it should accept a hex color, but it doesn't.
function rgbaToHex(color: string): string {
if (!color) return 'black';
const rgbaRegex =
/^rgba?(s*(d{1,3})s*,s*(d{1,3})s*,s*(d{1,3})(?:s*,s*(d*.?d+))?s*)$/i;
const match = color.match(rgbaRegex);
if (!match) {
return color;
}
const [, rStr, gStr, bStr, aStr] = match;
const r = Math.min(255, parseInt(rStr));
const g = Math.min(255, parseInt(gStr));
const b = Math.min(255, parseInt(bStr));
const a = aStr !== undefined ? Math.round(parseFloat(aStr) * 255) : 255;
const toHex = (n: number) => n.toString(16).padStart(2, '0');
return `#${toHex(r)}${toHex(g)}${toHex(b)}${a < 255 ? toHex(a) : ''}`;
}
import type MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons';
import type MaterialIcons from '@expo/vector-icons/MaterialIcons';
import type { SymbolViewProps } from 'expo-symbols';
import type { IconMapper } from 'rn-icon-mapper';
type MaterialCommunityIconsProps = React.ComponentProps<typeof MaterialCommunityIcons>;
type MaterialIconsProps = React.ComponentProps<typeof MaterialIcons>;
type Style = SymbolViewProps['style'] &
MaterialIconsProps['style'] &
MaterialCommunityIconsProps['style'];
type IconProps = IconMapper<SymbolViewProps, MaterialIconsProps, MaterialCommunityIconsProps> & {
style?: Style;
className?: string;
};
export type { IconProps };
import { Pressable, View } from 'react-native';
import Animated, { LayoutAnimationConfig, ZoomInRotate } from 'react-native-reanimated';
import { Icon } from '@/components/nativewindui/Icon';
import { cn } from '@/lib/cn';
import { useColorScheme } from '@/lib/useColorScheme';
import { COLORS } from '@/theme/colors';
export function ThemeToggle() {
const { colorScheme, toggleColorScheme } = useColorScheme();
return (
<LayoutAnimationConfig skipEntering>
<Animated.View
className="items-center justify-center"
key={`toggle-${colorScheme}`}
entering={ZoomInRotate}>
<Pressable onPress={toggleColorScheme} className="opacity-80">
{colorScheme === 'dark'
? ({ pressed }) => (
<View className={cn('px-0.5', pressed && 'opacity-50')}>
<Icon name="moon.stars" color={COLORS.white} />
</View>
)
: ({ pressed }) => (
<View className={cn('px-0.5', pressed && 'opacity-50')}>
<Icon name="sun.min" color={COLORS.black} />
</View>
)}
</Pressable>
</Animated.View>
</LayoutAnimationConfig>
);
}
<NavThemeProvider>
import '../global.css';
import 'expo-dev-client';
{YOUR_OTHER_IMPORTS}
import { StatusBar } from 'expo-status-bar';
import { ThemeProvider as NavThemeProvider } from '@react-navigation/native';
import { NAV_THEME } from '@/theme';
export {
// Catch any errors thrown by the Layout component.
ErrorBoundary,
} from 'expo-router';
export default function RootLayout() {
const { colorScheme, isDarkColorScheme } = useColorScheme();
return (
<>
<StatusBar
key={`root-status-bar-${isDarkColorScheme ? 'light' : 'dark'}`}
style={isDarkColorScheme ? 'light' : 'dark'}
/>
<NavThemeProvider value={NAV_THEME[colorScheme]}>
{YOUR_ROOT_NAVIGATOR}
</NavThemeProvider>
</>
);
}
import { Text } from 'react-native';
import { Text } from '@/components/nativewindui/Text';
index.tsx
import { useHeaderHeight } from '@react-navigation/elements';
import { FlashList } from '@shopify/flash-list';
import { cssInterop } from 'nativewind';
import * as React from 'react';
import {
Linking,
useWindowDimensions,
View,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Icon } from '@/components/nativewindui/Icon';
import { Text } from '@/components/nativewindui/Text';
import { useColorScheme } from '@/lib/useColorScheme';
cssInterop(FlashList, {
className: 'style',
contentContainerClassName: 'contentContainerStyle',
});
export default function Screen() {
return (
<FlashList
contentInsetAdjustmentBehavior="automatic"
keyboardShouldPersistTaps="handled"
data={COMPONENTS}
contentContainerClassName="py-4"
keyExtractor={keyExtractor}
ItemSeparatorComponent={renderItemSeparator}
renderItem={renderItem}
ListEmptyComponent={COMPONENTS.length === 0 ? ListEmptyComponent : undefined}
/>
);
}
function ListEmptyComponent() {
const insets = useSafeAreaInsets();
const dimensions = useWindowDimensions();
const headerHeight = useHeaderHeight();
const { colors } = useColorScheme();
const height = dimensions.height - headerHeight - insets.bottom - insets.top;
return (
<View style={{ height }} className="flex-1 items-center justify-center gap-1 px-12">
<Icon name="doc.badge.plus" size={42} color={colors.grey} />
<Text variant="title3" className="pb-1 text-center font-semibold">
No Components Installed
</Text>
<Text color="tertiary" variant="subhead" className="pb-4 text-center">
You can install any of the free components from the{' '}
<Text
onPress={() => Linking.openURL('https://nativewindui.com')}
variant="subhead"
className="text-primary">
NativewindUI
</Text>
{' website.'}
</Text>
</View>
);
}
type ComponentItem = { name: string; component: React.FC };
function keyExtractor(item: ComponentItem) {
return item.name;
}
function renderItemSeparator() {
return <View className="p-2" />;
}
function renderItem({ item }: { item: ComponentItem }) {
return (
<Card title={item.name}>
<item.component />
</Card>
);
}
function Card({ children, title }: { children: React.ReactNode; title: string }) {
return (
<View className="px-4">
<View className="gap-4 rounded-xl border border-border bg-card p-4 pb-6 shadow-sm shadow-black/10 dark:shadow-none">
<Text className="text-center text-sm font-medium tracking-wider opacity-60">{title}</Text>
{children}
</View>
</View>
);
}
const COMPONENTS: ComponentItem[] = [];
npx expo prebuild --clean
npm run start