Chapitre 320 minIntermédiaire
Composants et styles React Native : View, Text, Flexbox
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- traduire une interface HTML en composants React Native ;
- mettre en forme avec StyleSheet et Flexbox, colonne par défaut ;
- gérer les zones sûres, les tailles d'écran et le mode sombre ;
- rendre les composants accessibles aux lecteurs d'écran ;
- écrire tes propres composants réutilisables avec leurs props.
4 exercices corrigés1 schéma
Une interface React Native s'assemble avec des composants de base fournis par React Native (View, Text, Image, TextInput, Pressable…) qui deviennent des vues natives, et se met en forme avec des styles écrits en JavaScript, proches du CSS, et une mise en page Flexbox. Ce chapitre fait le lien avec le HTML et le CSS que tu connais, explique les différences qui piègent au début, et construit les premiers composants d'AppliFrais : un bouton et un champ de saisie.
Du HTML aux composants natifs
En React web, tu écris des balises HTML. En React Native, ces balises n'existent pas : tu utilises des composants importés de react-native.
| En HTML | En React Native | Remarque |
|---|---|---|
<div> | <View> | Le conteneur universel, en Flexbox par défaut |
<p>, <span>, <h1> | <Text> | Tout texte doit être dans un <Text> |
<img> | <Image> (ou expo-image) | Il faut lui donner une taille |
<input>, <textarea> | <TextInput> | multiline pour une zone de texte |
<button>, <a> | <Pressable> (ou <Link> d'Expo Router) | Tu dessines le bouton toi-même |
<ul> d'éléments | <FlatList> | Ne crée que les éléments visibles (chapitre 5) |
| Page qui défile | <ScrollView> | Une View ne défile pas |
<input type="checkbox"> | <Switch> | Interrupteur natif |
| Indicateur de chargement | <ActivityIndicator> | La roue native du système |
alert() | Alert.alert(titre, message, boutons) | Boîte de dialogue native |
La règle qui provoque la première erreur de tout débutant : un texte doit toujours être dans un <Text>. Écrire <View>Bonjour</View> déclenche l'erreur « Text strings must be rendered within a <Text> component ». Même chose pour une expression qui produit du texte par accident :
// ERREUR si fiches.length vaut 0 : le 0 est du texte, posé hors d'un <Text>
<View>{fiches.length && <Text>{fiches.length} fiches</Text>}</View>
// CORRECT : une condition qui vaut vrai ou faux
<View>{fiches.length > 0 && <Text>{fiches.length} fiches</Text>}</View>
Le reste, tu le connais : des composants fonctions, des props, l'état avec useState, les effets avec useEffect, les listes avec une clé. Si un de ces mots ne te dit rien, relis le cours React avant de continuer.
Styler avec StyleSheet
Les styles s'écrivent en objets JavaScript, avec les noms des propriétés CSS en camelCase : background-color devient backgroundColor, font-size devient fontSize.
import { StyleSheet, Text, View } from 'react-native';
export function Carte() {
return (
<View style={styles.carte}>
<Text style={styles.titre}>Octobre 2026</Text>
<Text style={[styles.texte, styles.important]}>En cours de saisie</Text>
</View>
);
}
const styles = StyleSheet.create({
carte: { backgroundColor: '#ffffff', borderRadius: 12, padding: 16 },
titre: { fontSize: 17, fontWeight: '600' },
texte: { fontSize: 15, color: '#374151' },
important: { color: '#1d4ed8' },
});
Ce qui change par rapport au CSS :
| En CSS | En React Native |
|---|---|
padding: 16px | padding: 16, un nombre sans unité (des points indépendants de la densité de l'écran) |
margin: 8px 16px | marginVertical: 8, marginHorizontal: 16 : pas de raccourci à plusieurs valeurs |
| Feuille de style, sélecteurs, classes | Un objet par composant, passé par la prop style |
| Cascade et spécificité | Aucune : dans un tableau de styles, le dernier gagne |
Héritage de color, font-size | Seulement d'un <Text> à un <Text> imbriqué, jamais d'une View à un Text |
:hover, :focus, media queries | N'existent pas : on utilise l'état (pressed, focused) et les dimensions de l'écran |
% | Accepté pour les largeurs et hauteurs : width: '100%' |
StyleSheet.create n'est pas obligatoire (un objet littéral fonctionne), mais il regroupe les styles en bas du fichier, les valide en développement et les rend plus lisibles. Pour un style qui dépend de l'état, combine-les dans un tableau : style={[styles.saisie, erreur ? styles.saisieEnErreur : null]}. Les valeurs null, false et undefined y sont ignorées.
Flexbox, la mise en page par défaut
Toute View est un conteneur Flexbox. Tu n'as pas à écrire display: 'flex'. Ce qui diffère du Web :
- la direction par défaut est la colonne (
flexDirection: 'column'), pas la ligne : les enfants s'empilent de haut en bas, comme sur un écran de téléphone ; flex: 1sur un enfant lui fait prendre tout l'espace restant dans son parent ; c'est ce qu'on met sur laViewracine d'un écran pour qu'elle occupe tout l'écran ;flexShrinkvaut 0 par défaut (1 sur le Web) : un élément trop grand ne rétrécit pas, il dépasse.
| Propriété | Agit sur | Valeurs courantes |
|---|---|---|
flexDirection | Le sens de l'axe principal | 'column' (défaut), 'row' |
justifyContent | La répartition sur l'axe principal | 'flex-start', 'center', 'space-between', 'flex-end' |
alignItems | Le placement sur l'axe secondaire | 'stretch' (défaut), 'center', 'flex-start', 'flex-end' |
flex | La part d'espace restant prise par l'enfant | 1 pour tout prendre |
gap, rowGap, columnGap | L'espace entre les enfants | Un nombre |
flexWrap | Le retour à la ligne | 'wrap' |
Un en-tête avec un titre à gauche et un compteur à droite, sur une ligne :
const styles = StyleSheet.create({
entete: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 16 },
titre: { fontSize: 22, fontWeight: '700' },
compteur: { minWidth: 32, textAlign: 'center', borderRadius: 16, backgroundColor: '#dbeafe' },
});
Tailles, densité et zones sûres
Les nombres des styles sont des points indépendants de la densité (dp sur Android, points sur iOS). Un bouton de 48 points de haut a la même taille physique sur un vieux téléphone et sur un écran très dense, même si l'un l'affiche avec 96 pixels et l'autre avec 168. Tu n'as jamais à raisonner en pixels.
Pour adapter la mise en page à la taille de l'écran, le hook useWindowDimensions donne la largeur et la hauteur courantes, mises à jour si l'utilisateur tourne le téléphone :
const { width } = useWindowDimensions();
const colonnes = width > 600 ? 2 : 1; // deux colonnes sur une tablette
Les téléphones récents ont une encoche ou une caméra dans l'écran, des coins arrondis et une barre de geste en bas. Les zones sûres (safe areas) sont les parties de l'écran où le contenu ne risque pas d'être caché. Le composant SafeAreaView de la bibliothèque react-native-safe-area-context, installée avec le modèle, ajoute les marges nécessaires :
import { SafeAreaView } from 'react-native-safe-area-context';
export function Entete() {
return (
<SafeAreaView style={styles.entete} edges={['top']}>
<Text style={styles.titre}>Mes fiches</Text>
<Text style={styles.compteur}>3</Text>
</SafeAreaView>
);
}
Les écrans affichés par la navigation d'Expo Router ont déjà un en-tête qui respecte la zone sûre du haut ; SafeAreaView sert surtout pour les écrans sans en-tête, comme l'écran de connexion.
Pressable et les retours tactiles
Il n'y a pas de <button> stylable en React Native. Le composant Pressable rend n'importe quel contenu touchable, et donne à sa fonction de style l'état pressed pour afficher un retour visuel :
<Pressable onPress={() => console.log('touché')} style={({ pressed }) => [styles.bouton, pressed && { opacity: 0.7 }]}>
<Text style={styles.texteBouton}>Valider</Text>
</Pressable>
Les autres événements utiles : onLongPress (appui long), onPressIn et onPressOut. La prop hitSlop agrandit la zone touchable sans agrandir le dessin, pratique pour une petite icône.
Images
Pour afficher une image, le modèle d'Expo installe expo-image, plus performant que l'Image de React Native (cache disque, formats modernes, transitions) :
import { Image } from 'expo-image';
// Une image du projet, embarquée dans l'application
<Image source={require('@/assets/images/logo.png')} style={{ width: 120, height: 40 }} contentFit="contain" />
// Une image distante
<Image source={{ uri: 'https://api.gsb.example/v1/frais-hors-forfait/58/justificatif' }} style={{ width: '100%', height: 200 }} />
Une image n'a pas de taille par défaut : sans width et height (ou flex), elle ne s'affiche pas. contentFit joue le rôle de object-fit en CSS : 'cover' remplit en rognant, 'contain' montre tout.
Adapter à la plateforme et au thème
La plupart du code est commun à Android et à iOS. Pour les différences :
import { Platform } from 'react-native';
// Un test ponctuel
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
// Une valeur par plateforme
const ombre = Platform.select({
ios: { shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 4, shadowOffset: { width: 0, height: 2 } },
android: { elevation: 2 },
});
Pour une différence importante, crée deux fichiers : Calendrier.ios.tsx et Calendrier.android.tsx. Un import { Calendrier } from '@/components/Calendrier' charge automatiquement le bon selon le système.
Le mode sombre se lit avec le hook useColorScheme(), qui renvoie 'light' ou 'dark' selon le réglage du téléphone. Le modèle d'Expo l'utilise dans src/constants/theme.ts : pour une application soignée, définis tes couleurs dans un objet par thème plutôt qu'en dur dans chaque style.
Accessibilité
Les lecteurs d'écran du téléphone (TalkBack sur Android, VoiceOver sur iOS) lisent les vues à voix haute. Ils s'appuient sur des props que tu dois renseigner :
| Prop | Rôle | Exemple |
|---|---|---|
accessibilityLabel | Ce que le lecteur d'écran prononce | "Fiche octobre 2026, En cours de saisie" |
accessibilityRole | La nature de l'élément | 'button', 'header', 'link' |
accessibilityState | Son état | { disabled: true } |
accessibilityHint | Ce qui se passe si on l'active | "Ouvre le détail de la fiche" |
accessibilityLiveRegion (Android) | Annonce un texte qui change | Un message d'erreur qui apparaît |
Teste aussi avec la taille de police maximale du téléphone : les Text s'agrandissent (c'est voulu), et une mise en page à hauteur fixe se casse. Les règles de contraste et de lisibilité du Web s'appliquent (accessibilité RGAA et WCAG).
Tes premiers composants : Bouton et Champ
AppliFrais réutilise partout deux composants. Crée src/components/Bouton.tsx :
import { Pressable, StyleSheet, Text } from 'react-native';
type Props = {
titre: string;
onPress: () => void;
desactive?: boolean;
variante?: 'principal' | 'secondaire';
};
export function Bouton({ titre, onPress, desactive = false, variante = 'principal' }: Props) {
return (
<Pressable
onPress={onPress}
disabled={desactive}
accessibilityRole="button"
accessibilityState={{ disabled: desactive }}
style={({ pressed }) => [styles.base, styles[variante], (pressed || desactive) && styles.attenue]}>
<Text style={[styles.texte, variante === 'secondaire' && styles.texteSecondaire]}>{titre}</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
base: { minHeight: 48, borderRadius: 8, paddingHorizontal: 16, justifyContent: 'center', alignItems: 'center' },
principal: { backgroundColor: '#1d4ed8' },
secondaire: { borderWidth: 1, borderColor: '#1d4ed8' },
attenue: { opacity: 0.6 },
texte: { color: '#ffffff', fontSize: 16, fontWeight: '600' },
texteSecondaire: { color: '#1d4ed8' },
});
Le type Props décrit ce que le composant accepte ; le ? rend une prop facultative, et la déstructuration lui donne une valeur par défaut. styles[variante] choisit le style principal ou secondaire selon la prop : le nom de la variante est aussi le nom du style.
Puis src/components/Champ.tsx, un champ de saisie avec son libellé et son message d'erreur :
import { StyleSheet, Text, TextInput, View, type TextInputProps } from 'react-native';
type Props = TextInputProps & { libelle: string; erreur?: string };
export function Champ({ libelle, erreur, style, ...autres }: Props) {
return (
<View style={styles.champ}>
<Text style={styles.libelle}>{libelle}</Text>
<TextInput
accessibilityLabel={libelle}
style={[styles.saisie, erreur ? styles.saisieEnErreur : null, style]}
placeholderTextColor="#6b7280"
{...autres}
/>
{erreur ? <Text style={styles.erreur}>{erreur}</Text> : null}
</View>
);
}
const styles = StyleSheet.create({
champ: { gap: 4, marginBottom: 16 },
libelle: { fontSize: 14, fontWeight: '600', color: '#111827' },
saisie: { minHeight: 48, borderWidth: 1, borderColor: '#d1d5db', borderRadius: 8, paddingHorizontal: 12, fontSize: 16 },
saisieEnErreur: { borderColor: '#b91c1c' },
erreur: { color: '#b91c1c', fontSize: 13 },
});
TextInputProps & { … } veut dire : toutes les props d'un TextInput, plus libelle et erreur. Le ...autres récupère les props restantes (value, onChangeText, keyboardType…) et les transmet au TextInput. Le libellé visible sert aussi de libellé d'accessibilité, et le message d'erreur s'affiche sous le champ, en texte et pas seulement en couleur.
Exercice 1 sur 4 Du HTML à React Native Réussi À revoir
Traduis ce fragment web en React Native, avec un StyleSheet :
<div class="ligne">
<span class="libelle">Repas client à Lyon</span>
<span class="montant">23,50 €</span>
</div>
<style>
.ligne { display: flex; justify-content: space-between; padding: 12px 0; border-bottom: 1px solid #e5e7eb; }
.montant { font-weight: 600; }
</style>
Voir la réponseMasquer la réponse de l'exercice 1
import { StyleSheet, Text, View } from 'react-native';
export function LigneFrais() {
return (
<View style={styles.ligne}>
<Text style={styles.libelle}>Repas client à Lyon</Text>
<Text style={styles.montant}>23,50 €</Text>
</View>
);
}
const styles = StyleSheet.create({
ligne: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 12, borderBottomWidth: 1, borderColor: '#e5e7eb' },
libelle: { flex: 1 },
montant: { fontWeight: '600' },
});
Trois différences : flexDirection: 'row' est obligatoire (la colonne est la valeur par défaut) ; le raccourci padding: 12px 0 devient paddingVertical: 12 ; border-bottom se décompose en borderBottomWidth et borderColor. Le flex: 1 du libellé lui laisse la place restante et l'empêche de chevaucher le montant si le texte est long.
Tu avais trouvé ?
Exercice 2 sur 4 Trouver les erreurs Réussi À revoir
Ce composant contient quatre erreurs. Trouve-les et corrige-les.
export function Total({ total }: { total: number }) {
return (
<View style={{ display: 'flex', padding: '16px' }}>
Total du mois
<Text style={{ font-weight: 'bold' }}>{total} €</Text>
<img source={require('./logo.png')} />
</View>
);
}
Voir la réponseMasquer la réponse de l'exercice 2
padding: '16px': un nombre sans unité,padding: 16. (display: 'flex'est inutile mais pas faux.)Total du moisest du texte directement dans uneView: il faut<Text>Total du mois</Text>.font-weightn'est pas un nom de propriété JavaScript valide :fontWeight: 'bold'(ou'700').<img>n'existe pas :<Image source={require('./logo.png')} style={{ width: 120, height: 40 }} />, avec une taille.
Tu avais trouvé ?
Exercice 3 sur 4 Prévoir la mise en page Réussi À revoir
Un écran contient une View avec style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }} et trois Text dedans. Où s'affichent les trois textes ? Et si l'on ajoute flexDirection: 'row' ?
Voir la réponseMasquer la réponse de l'exercice 3
Sans flexDirection, l'axe principal est vertical : les trois textes sont empilés les uns sous les autres, le groupe est centré verticalement (justifyContent) et chaque texte est centré horizontalement (alignItems). Avec flexDirection: 'row', les trois textes sont côte à côte sur une ligne, groupe centré horizontalement (justifyContent suit maintenant l'axe horizontal) et textes centrés verticalement.
Tu avais trouvé ?
Exercice 4 sur 4 Un composant Badge Réussi À revoir
Écris un composant Badge qui affiche un texte court dans une pastille arrondie, avec une prop ton qui vaut 'info' (fond bleu clair, texte bleu foncé) ou 'alerte' (fond rouge clair, texte rouge foncé), 'info' par défaut. Il doit être lisible par un lecteur d'écran.
Voir la réponseMasquer la réponse de l'exercice 4
import { StyleSheet, Text } from 'react-native';
type Props = { texte: string; ton?: 'info' | 'alerte' };
export function Badge({ texte, ton = 'info' }: Props) {
return <Text style={[styles.badge, styles[ton]]}>{texte}</Text>;
}
const styles = StyleSheet.create({
badge: { alignSelf: 'flex-start', fontSize: 12, paddingHorizontal: 8, paddingVertical: 2, borderRadius: 999, overflow: 'hidden' },
info: { backgroundColor: '#dbeafe', color: '#1e3a8a' },
alerte: { backgroundColor: '#fee2e2', color: '#7f1d1d' },
});
Un Text est lu tel quel par le lecteur d'écran : pas besoin d'accessibilityLabel tant que le texte se suffit à lui-même. alignSelf: 'flex-start' empêche la pastille de s'étirer sur toute la largeur (alignItems vaut 'stretch' par défaut dans le parent), et overflow: 'hidden' est nécessaire sur iOS pour que le fond suive l'arrondi d'un Text.
Tu avais trouvé ?