Chapitre 4.4 — NativeWind : Tailwind dans React Native
Où on en est : StyleSheet maîtrisé, tokens posés. Vous avez gagné le droit au confort : on installe NativeWind et Drill retrouve vos réflexes Tailwind.
⏱️ TL;DR — NativeWind compile des classes Tailwind vers du StyleSheet au build (via Metro + le compilateur Tailwind). Installation : 4 fichiers de config. Ensuite :
className="flex-1 items-center bg-ink-950", variantesdark:(le dark mode devient déclaratif),active:pour le pressed. Nos tokens du chapitre 4.3 déménagent danstailwind.config.js. À partir d’ici, tout le nouveau code du cours est en NativeWind — StyleSheet reste pour les valeurs calculées et le debug.
🎯 Objectifs
- Installer NativeWind proprement dans Drill (et savoir ce que fait chaque fichier de config).
- Migrer les tokens de thème vers tailwind.config.js.
- Utiliser className, dark:, active: — et connaître les limites.
- Savoir quand revenir à StyleSheet (les cas légitimes).
Comment ça marche (pour ne pas subir la magie)
NativeWind n’embarque PAS de moteur CSS dans l’app : les classes sont compilées au build.
Metro passe vos fichiers au compilateur Tailwind, qui produit des objets de style RN ; à
l’exécution, className="p-4" est déjà devenu l’équivalent d’un StyleSheet (+ une couche
runtime pour dark:, active:, etc.). Coût runtime marginal, et surtout : tout ce que vous
avez appris aux chapitres 4.1-4.3 reste la réalité sous-jacente — quand une classe surprend,
l’element inspector montre le style RN généré.
Installation
npx expo install nativewind react-native-reanimated react-native-safe-area-context
npx expo install -- --dev tailwindcss@^3.4.0 prettier-plugin-tailwindcssQuatre fichiers à créer/modifier :
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}'],
presets: [require('nativewind/preset')],
darkMode: 'media', // suit le système (cf. useColorScheme)
theme: {
extend: {
colors: { // ← nos tokens du chapitre 4.3 !
ink: { 100: '#e2e8f0', 400: '#7a8b94', 700: '#1e3a45', 900: '#101b21', 950: '#0b1216' },
brand: { DEFAULT: '#087ea4', light: '#58c4dc' },
streak: '#fbbf24',
},
},
},
}/* global.css */
@tailwind base;
@tailwind components;
@tailwind utilities;// metro.config.js
const { getDefaultConfig } = require('expo/metro-config')
const { withNativeWind } = require('nativewind/metro')
const config = getDefaultConfig(__dirname)
module.exports = withNativeWind(config, { input: './global.css' })// babel.config.js
module.exports = function (api) {
api.cache(true)
return {
presets: [
['babel-preset-expo', { jsxImportSource: 'nativewind' }],
'nativewind/babel',
],
}
}Dernier branchement : import '../global.css' en tête d’app/_layout.tsx, et pour TS, un
nativewind-env.d.ts avec /// <reference types="nativewind/types" /> (donne className sur
les composants RN). Puis npx expo start -c (toujours vider le cache après avoir touché
Metro/Babel).
Le before/after
La ThemeCard du chapitre 4.3, réécrite :
// components/ThemeCard.tsx — version NativeWind
import { Text, View } from 'react-native'
export function ThemeCard({ title, questionCount, emoji }: Props) {
return (
<View className="flex-row items-center gap-3 rounded-2xl border border-ink-700 bg-ink-900 p-4">
<Text className="text-3xl">{emoji}</Text>
<View className="flex-1">
<Text className="text-lg font-bold text-ink-100" numberOfLines={1}>{title}</Text>
<Text className="text-[13px] text-ink-400">{questionCount} questions</Text>
</View>
</View>
)
}Plus de StyleSheet.create, plus de hook thème pour les couleurs, plus de tableau — et le
useTheme devient inutile pour les cas courants grâce à dark: :
// Light + dark en une ligne — le chapitre 4.3 en déclaratif
<View className="flex-1 bg-slate-50 dark:bg-ink-950">
<Text className="text-slate-900 dark:text-ink-100">Score</Text>
</View>Et l’état pressé avec la variante active: sur Pressable :
<Pressable className="rounded-xl bg-ink-900 p-4 active:scale-[0.99] active:opacity-85">💡 Pour un dev Next.js — Oui :
flex-rows’appelle ainsi parce que le défaut RN est column (chapitre 4.2) — Tailwind web n’a pas besoin deflexici, NativeWind si peu : les Views sont déjà flex. À part ça, vos réflexes sont intacts : espacement (p-4,gap-3), typo (text-lg font-bold), arbitraires (text-[13px]),prettier-plugin-tailwindcsspour l’ordre des classes. Même votre muscle mémoiredark:fonctionne.
Les limites honnêtes
- Pas de cascade non plus :
classNamesur une View ne style pas les Text enfants. Les classes s’appliquent au composant qui les porte — comme StyleSheet. - Classes dynamiques interdites :
className={"p-" + size}ne PEUT pas marcher (compilation au build → la classe doit exister littéralement dans le source). Variantes conditionnelles OK :className={cn('p-4', isActive && 'border-brand')}. - Valeurs calculées (largeur animée, couleur interpolée, insets) → prop
style, qui cohabite avec className :className="rounded-xl" style={{ width: animatedWidth }}. - Certaines props de style restent hors CSS (ex.
resizeModed’Image) — normales, en props.
La règle du cours à partir d’ici : NativeWind pour tout le style déclaratif ; style pour
les valeurs calculées/animées ; StyleSheet.create ne réapparaît que dans les exemples de
debug ou les libs. C’est le même partage que Tailwind/style inline sur vos projets Next.js.
⚠️ Piège — Trois causes couvrent 95 % des « ma classe ne s’applique pas » : (1) le fichier n’est pas couvert par
contentde tailwind.config.js (nouveau dossier non listé) ; (2) cache Metro pas vidé après changement de config (npx expo start -c) ; (3) classe construite dynamiquement (voir ci-dessus). Vérifiez dans cet ordre.
✏️ Exercices
1. Migrez AnswerButton (version tokens du chapitre 4.3) vers NativeWind, états compris.
✅ Solution
const stateClasses: Record<AnswerState, string> = {
idle: 'border-ink-700 bg-ink-900',
correct: 'border-green-500 bg-green-500/10',
incorrect: 'border-red-500 bg-red-500/10',
disabled: 'border-ink-700 bg-ink-900 opacity-45',
}
<Pressable
disabled={!interactive}
className={cn(
'min-h-[56px] justify-center rounded-xl border px-4',
stateClasses[state],
interactive && 'active:scale-[0.99] active:opacity-85',
)}
>
<Text className="text-base font-semibold text-ink-100">{label}</Text>
</Pressable>Les fonds teintés bg-green-500/10 remplacent l’astuce hex+alpha ; le mapping objet garde des
classes littérales (compilables). cn = petit helper concat (ou clsx).
2. Reconstruisez le layout d’écran de quiz (header/body/footer du chapitre 4.2) en className uniquement.
✅ Solution
<View className="flex-1 gap-4 p-4">
<View className="flex-row items-center justify-between">{/* timer + score */}</View>
<View className="flex-1 justify-center">{/* question */}</View>
<View className="gap-2.5">{/* réponses */}</View>
</View>Traduction directe : flex: 1 → flex-1, justifyContent: 'space-between' →
justify-between, gap: 10 → gap-2.5.
3. Pourquoi className={"text-" + color} échoue-t-il silencieusement, et quelles sont les
deux écritures correctes ?
✅ Solution
Le compilateur scanne le source au build : text-red-500 n’apparaissant nulle part
littéralement, aucun style n’est généré. Correct : (1) mapping de classes complètes
({ danger: 'text-red-500', muted: 'text-ink-400' }[variant]) ; (2) prop style pour une
couleur réellement dynamique (style={{ color }}). Identique au Tailwind web, en plus strict.
🧠 Quiz
1. Quand les classes NativeWind sont-elles transformées en styles, et par quoi ?
Réponse
Au build, par le compilateur Tailwind branché dans Metro (withNativeWind) — elles
deviennent des styles RN natifs, pas du CSS interprété à l’exécution.
2. Où vivent désormais les tokens de couleur de Drill ?
Réponse
Dans theme.extend.colors de tailwind.config.js (ink, brand, streak…) — les mêmes valeurs
que constants/theme.ts, consommées en classes (bg-ink-900, text-brand-light).
3. Comment gère-t-on le dark mode avec NativeWind ?
Réponse
Variante dark: (bg-white dark:bg-ink-950), avec darkMode: 'media' pour suivre le
système. (Un contrôle manuel utilisateur passera par useColorScheme de nativewind /
darkMode: 'class'-équivalent — Partie 6.)
4. Citez les trois causes classiques d’une classe sans effet.
Réponse
Fichier hors du glob content, cache Metro non vidé après changement de config, classe
construite dynamiquement (non littérale dans le source).
5. Quand garde-t-on la prop style malgré NativeWind ?
Réponse
Pour les valeurs calculées à l’exécution : dimensions/couleurs animées (Reanimated), insets de safe area injectés, valeurs venant des données. className et style cohabitent sur le même composant.
👉 Chapitre suivant : 4.5 — Le design system de Drill — on capitalise : les composants UI qui serviront jusqu’à la Partie 19.