Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

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 HTMLEn React NativeRemarque
<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 :

tsx
// 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.

tsx
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 CSSEn React Native
padding: 16pxpadding: 16, un nombre sans unité (des points indépendants de la densité de l'écran)
margin: 8px 16pxmarginVertical: 8, marginHorizontal: 16 : pas de raccourci à plusieurs valeurs
Feuille de style, sélecteurs, classesUn 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-sizeSeulement d'un <Text> à un <Text> imbriqué, jamais d'une View à un Text
:hover, :focus, media queriesN'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: 1 sur un enfant lui fait prendre tout l'espace restant dans son parent ; c'est ce qu'on met sur la View racine d'un écran pour qu'elle occupe tout l'écran ;
  • flexShrink vaut 0 par défaut (1 sur le Web) : un élément trop grand ne rétrécit pas, il dépasse.
flexDirection: 'column' (défaut)titretextebouton↓ axe principal : justifyContent→ axe secondaire : alignItemsjustifyContent: 'center'alignItems: 'center'flexDirection: 'row'Mes fiches3→ axe principal : justifyContent↓ axe secondaire : alignItemsjustifyContent: 'space-between'alignItems: 'center'flex: 1en-tête, hauteur fixeflex: 1 : tout l'espace restant
justifyContent suit l'axe principal, alignItems l'axe secondaire ; en React Native, l'axe principal est vertical par défaut
PropriétéAgit surValeurs courantes
flexDirectionLe sens de l'axe principal'column' (défaut), 'row'
justifyContentLa répartition sur l'axe principal'flex-start', 'center', 'space-between', 'flex-end'
alignItemsLe placement sur l'axe secondaire'stretch' (défaut), 'center', 'flex-start', 'flex-end'
flexLa part d'espace restant prise par l'enfant1 pour tout prendre
gap, rowGap, columnGapL'espace entre les enfantsUn nombre
flexWrapLe retour à la ligne'wrap'

Un en-tête avec un titre à gauche et un compteur à droite, sur une ligne :

tsx
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 :

tsx
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 :

tsx
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 :

tsx
<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) :

tsx
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 :

tsx
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 :

PropRôleExemple
accessibilityLabelCe que le lecteur d'écran prononce"Fiche octobre 2026, En cours de saisie"
accessibilityRoleLa nature de l'élément'button', 'header', 'link'
accessibilityStateSon état{ disabled: true }
accessibilityHintCe qui se passe si on l'active"Ouvre le détail de la fiche"
accessibilityLiveRegion (Android)Annonce un texte qui changeUn 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 :

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 :

tsx
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 :

html
<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
tsx
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.

tsx
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
  1. padding: '16px' : un nombre sans unité, padding: 16. (display: 'flex' est inutile mais pas faux.)
  2. Total du mois est du texte directement dans une View : il faut <Text>Total du mois</Text>.
  3. font-weight n'est pas un nom de propriété JavaScript valide : fontWeight: 'bold' (ou '700').
  4. <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
tsx
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é ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Navigation avec Expo Router : pile, onglets et paramètresRevoir le sommaire du cours