Skip to Content

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.create intentionnelle.

⏱️ 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.create valide 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.create apporte 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 de padding: '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.assign visuel.
  • Les false/null/undefined sont ignorés → conditions inline naturelles.
  • Convention de composant réutilisable : accepter une prop style et 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 dans StyleSheet.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 :

CSSRNÉquivalent
Sélecteurs, cascadeComposition de composants
:hover, :active, :focusÉtat pressed de Pressable, état React
Media queriesuseWindowDimensions + conditions JS
box-shadowpartielelevation (Android) / shadow* (iOS) — ou boxShadow ⚡ RN 0.76+
Pseudo-éléments ::beforeDe vrais composants
z-index globalpartielzIndex entre frères seulement
Animations/transitions CSSReanimated (Partie 10)
display: gridFlexbox + calculs (ou flexWrap)
Héritage de policeComposants de texte à soi (AppText, ch. 4.5)

Depuis RN 0.76 — Les styles boxShadow (syntaxe CSS-like) et filter sont 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 :

  1. Validation : une clé invalide (paddin) ou une valeur impossible jette une erreur en dev — l’objet nu, lui, échoue silencieusement.
  2. Types : autocomplétion et vérification TS des propriétés.
  3. 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.