Chapitre 4.1 — StyleSheet : styler sans CSS
Où on en est : vous composez des écrans avec les primitives, mais en copiant les styles. Ce chapitre rend chaque ligne de
StyleSheet.createintentionnelle.
⏱️ TL;DR — Pas de CSS en RN : les styles sont des objets JS aux clés camelCase, un sous-ensemble strict de CSS. Les nombres sont des dp (indépendants de la densité). La composition passe par des tableaux (
[base, cond && override]). Pas de sélecteurs, pas de pseudo-classes, pas de media queries — de la logique JS à la place.StyleSheet.createvalide et centralise.
🎯 Objectifs
- Écrire des styles RN sans traduire mentalement depuis CSS (clés, valeurs, unités).
- Composer des styles conditionnels avec les tableaux.
- Connaître la liste de ce qui n’existe PAS et les équivalents.
- Comprendre ce que
StyleSheet.createapporte vraiment.
Des objets, pas des feuilles
import { StyleSheet, Text, View } from 'react-native'
export function ScoreBoard({ score }: { score: number }) {
return (
<View style={styles.board}>
<Text style={styles.value}>{score}</Text>
<Text style={styles.unit}>points</Text>
</View>
)
}
const styles = StyleSheet.create({
board: {
alignItems: 'center',
paddingVertical: 24,
borderRadius: 16,
backgroundColor: '#101b21',
},
value: { fontSize: 56, fontWeight: '800', color: '#58c4dc' },
unit: { fontSize: 14, color: '#7a8b94', textTransform: 'uppercase' },
})Les règles du jeu :
- camelCase :
backgroundColor,paddingVertical,borderRadius. - Valeurs typées : nombres nus (dimensions), strings (couleurs,
fontWeight). - Raccourcis dédiés :
paddingVertical/paddingHorizontal(pas depadding: '24px 16px'— les shorthands CSS multi-valeurs n’existent pas),marginHorizontal, etc. - Le scope est local par nature : un style s’applique là où on le passe. Pas de fuite, pas
de spécificité, pas de
!important— la guerre du CSS n’a jamais eu lieu ici.
L’unité invisible : le dp
fontSize: 16 — seize quoi ? Des dp (density-independent pixels). Un écran moderne
entasse 2,5 à 3,5 pixels physiques par dp : en dessinant en dp, votre bouton fait la même
taille physique (en millimètres) sur tous les écrans. Vous ne convertissez jamais rien —
retenez juste : 1 dp ≈ même taille perçue partout, et les valeurs usuelles (16 de padding,
48 de cible tactile) ressemblent aux px du web.
Pas de rem/em (pas de racine dont hériter !). Les pourcentages existent (width: '50%'),
relatifs au parent.
Composer : le tableau de styles
L’outil de composition universel — déjà croisé avec Pressable :
<View style={[styles.card, isActive && styles.cardActive, style]} />- Fusion de gauche à droite (le dernier gagne) — votre
Object.assignvisuel. - Les
false/null/undefinedsont ignorés → conditions inline naturelles. - Convention de composant réutilisable : accepter une prop
styleet la placer en dernier (l’appelant peut tout surcharger).
⚠️ Piège — Style inline objet (
style={{ padding: 16 }}) : valide, mais l’objet est recréé à chaque render. Pour un composant feuille c’est sans conséquence ; passé à un composant mémoïsé (item de FlatList !), il casse le memo à chaque fois. Règle simple : les styles statiques dansStyleSheet.create, l’inline réservé aux valeurs réellement dynamiques (une largeur calculée, une couleur animée).
Ce que CSS avait et que RN n’a pas
La liste à connaître pour arrêter de chercher :
| CSS | RN | Équivalent |
|---|---|---|
| Sélecteurs, cascade | ❌ | Composition de composants |
:hover, :active, :focus | ❌ | État pressed de Pressable, état React |
| Media queries | ❌ | useWindowDimensions + conditions JS |
box-shadow | partiel | elevation (Android) / shadow* (iOS) — ou boxShadow ⚡ RN 0.76+ |
Pseudo-éléments ::before | ❌ | De vrais composants |
z-index global | partiel | zIndex entre frères seulement |
| Animations/transitions CSS | ❌ | Reanimated (Partie 10) |
display: grid | ❌ | Flexbox + calculs (ou flexWrap) |
| Héritage de police | ❌ | Composants de texte à soi (AppText, ch. 4.5) |
⚡ Depuis RN 0.76 — Les styles
boxShadow(syntaxe CSS-like) etfiltersont apparus avec la New Architecture — le fossé se réduit.elevation/shadow*restent la voie la plus répandue dans les libs et exemples ; sachez lire les deux.
StyleSheet.create : à quoi ça sert vraiment
Historiquement présenté comme une optimisation, son intérêt réel aujourd’hui :
- Validation : une clé invalide (
paddin) ou une valeur impossible jette une erreur en dev — l’objet nu, lui, échoue silencieusement. - Types : autocomplétion et vérification TS des propriétés.
- Convention : les styles en bas du fichier, nommés, réutilisables — la lisibilité d’un module CSS sans le fichier séparé.
💡 Pour un dev Next.js — Si vous utilisez Tailwind, vous ne connaissez plus vraiment la douleur du CSS global — et StyleSheet vous semblera verbeux mais familier : des utilitaires nommés, localisés, composables. Tenez deux chapitres : NativeWind (4.4) vous rend
className— mais vous saurez alors EXACTEMENT ce qu’il génère.
✏️ Exercices
1. Traduisez ce CSS en StyleSheet (certaines propriétés n’ont pas d’équivalent direct — trouvez la parade) :
.card {
padding: 16px 20px;
margin-bottom: 12px;
background: #101b21;
border: 1px solid #1e3a45;
border-radius: 14px;
box-shadow: 0 4px 12px rgba(0,0,0,.3);
cursor: pointer;
}
.card:hover { border-color: #58c4dc; }✅ Solution
const styles = StyleSheet.create({
card: {
paddingVertical: 16,
paddingHorizontal: 20,
marginBottom: 12,
backgroundColor: '#101b21',
borderWidth: 1,
borderColor: '#1e3a45',
borderRadius: 14,
...Platform.select({
android: { elevation: 4 },
ios: { shadowColor: '#000', shadowOpacity: 0.3, shadowRadius: 12, shadowOffset: { width: 0, height: 4 } },
}),
},
cardPressed: { borderColor: '#58c4dc' },
})padding: 16px 20px → deux props ; border shorthand → borderWidth+borderColor ;
box-shadow → Platform.select (ou boxShadow RN 0.76+) ; cursor → n’existe pas (tout est
tactile) ; :hover → état pressed de Pressable avec cardPressed en tableau.
2. Créez un composant Stack maison : une View qui espace verticalement ses enfants de
gap dp et accepte une prop style surchargeante.
✅ Solution
import { View, ViewProps } from 'react-native'
type Props = ViewProps & { gap?: number }
export function Stack({ gap = 12, style, ...rest }: Props) {
return <View style={[{ gap }, style]} {...rest} />
}Le gap dynamique est un inline légitime (valeur variable) ; style en dernier = l’appelant
gagne. ViewProps transmet le reste (testID, accessibilité…).
3. Pourquoi zIndex: 999 ne fait-il pas passer votre tooltip au-dessus d’un composant
d’un AUTRE parent, et quelle est la solution mobile ?
✅ Solution
En RN, zIndex n’arbitre qu’entre frères d’un même parent — il n’y a pas de contexte
d’empilement global. Pour passer « au-dessus de tout » : le composant Modal de RN, ou monter
l’élément plus haut dans l’arbre (pattern portal — ex. le composant au niveau du layout racine).
🧠 Quiz
1. Que vaut fontSize: 16 — px, pt, dp ? Et pourquoi c’est une bonne nouvelle ?
Réponse
Des dp (density-independent pixels) : mise à l’échelle automatique selon la densité de l’écran → même taille physique perçue partout, zéro conversion à faire.
2. Comment applique-t-on un style conditionnel ?
Réponse
Tableau de styles : style={[styles.base, cond && styles.variant]} — fusion gauche→droite,
falsy ignorés.
3. Citez trois choses de CSS absentes de RN et leurs remplaçants.
Réponse
Media queries → useWindowDimensions + JS ; :hover/:active → état pressed de Pressable ;
animations CSS → Reanimated ; sélecteurs/cascade → composition de composants ; grid → flexbox.
4. Quel est le danger d’un style inline sur un item de FlatList mémoïsé ?
Réponse
L’objet est recréé à chaque render du parent → nouvelle référence → React.memo invalidé →
tous les items visibles re-rendent. Styles statiques dans StyleSheet.create.
5. Que valide StyleSheet.create que l’objet nu ne valide pas ?
Réponse
Les clés et valeurs de style (erreur en dev sur paddin: 16 ou valeur invalide) + le typage TS.
L’objet nu passe silencieusement et le style est juste ignoré.
👉 Chapitre suivant : 4.2 — Flexbox à la sauce RN — le même flexbox que vous adorez, avec 5 différences qui piègent tout le monde.