Le seul vrai dropdown natif pour React Native
Hors Expo, react-native-native-select est la seule lib React Native qui rend le dropdown natif de l'OS : UIMenu sur iOS, la liste native sur Android.

L'écran d'inscription d'Animalert demande un numéro de téléphone. À côté du champ, un sélecteur de pays : 250 entrées, chacune avec un drapeau, un nom et un indicatif. C'est le composant le plus banal de l'app, et celui qui nous a donné le plus de mal.
Toutes les libs qu'on a testées avaient la même mécanique en dessous : une modale, une liste scrollable, 250 lignes rendues par JavaScript. Ça ressemble au système sans jamais en être. Sur un Android d'entrée de gamme, on voyait la liste rattraper le pouce.
Alors on a arrêté de dessiner un select, et on a wrappé celui que l'OS dessine déjà. Ça a donné react-native-native-select, un package open source sous licence MIT. Hors Expo, c'est la seule lib React Native qui rend le dropdown de la plateforme sur iOS comme sur Android, au lieu de le redessiner : npm install react-native-native-select.
React Native n'a plus de Picker dans son core
React Native a livré un composant Picker dans son core pendant des années. Il en est sorti pendant le Lean Core, l'effort de déplacement des composants non essentiels du framework vers des packages communautaires, et il est devenu @react-native-picker/picker.
Ce package fait autour du million de téléchargements par semaine, et sur iOS il ne donne qu'une seule chose : la roue. Son code iOS ne contient que UIPickerView, et pas une ligne de UIMenu. Le remplaçant officiel du Picker du core n'a donc toujours pas de dropdown natif sur iOS, plus de six ans après que la plateforme en a livré un.
Quant aux autres libs qui ont pris la place, la plupart redessinent un select en JavaScript au lieu d'aller chercher celui de la plateforme.
Trois composants natifs existent déjà pour ce job précis. iOS a UIPickerView, la roue, et depuis iOS 14 un bouton peut ouvrir un UIMenu, le menu qui se déplie sous le bouton qu'on vient de toucher. Android a AppCompatSpinner et la liste déroulante qu'il ouvre, une ListPopupWindow.
En utiliser un ne joue pas seulement sur l'apparence. Vous héritez du dark mode, de la taille de texte réglée par l'utilisateur, du scroll et de l'haptique du système, et de la sémantique d'accessibilité que VoiceOver et TalkBack savent déjà annoncer.
Une liste en JavaScript n'hérite de rien. Chacun de ces comportements est à reconstruire à la main, et dans les faits la plupart ne le sont jamais.
Avant d'installer une lib de composants, ça vaut le coup de vérifier si l'OS ne fournit pas déjà le contrôle voulu. Quand c'est le cas, le wrapper demande souvent moins de travail que d'habiller un remplaçant crédible, et ça vieillit beaucoup mieux.
Quelle lib rend vraiment un dropdown natif
On a lu le code natif de chacune plutôt que les READMEs. Voilà ce que rend réellement chaque option de l'écosystème.
Les quatre libs de dropdown les plus installées.
react-native-picker-select,react-native-element-dropdown,react-native-dropdown-pickeretreact-native-select-dropdowntotalisent environ 570 000 installations par semaine, et aucune des quatre n'embarque son propre dossierios/ouandroid/. Trois redessinent le dropdown en JavaScript, etreact-native-picker-selects'appuie sur@react-native-picker/picker: sur iOS, vous obtenez sa roue dans une modale JavaScript.@react-native-picker/picker. Vraiment natif, mais la roue seulement sur iOS, comme dit plus haut.@react-native-menu/menu. Un vraiUIMenu, et c'est un menu d'actions, pas un select : vous recevez un identifiant dansonPressAction, il n'y a pas deselectedIndexcontrôlé, et Android reçoit unPopupMenu.@expo/ui. Le seul autre à rendre un dropdown natif, en SwiftUI avecpickerStyle(.menu)sur iOS et unExposedDropdownMenuBoxMaterial sur Android. Il demande Expo.
D'où la position de la lib, et elle est étroite : hors Expo, react-native-native-select est la seule lib React Native qui rend le dropdown de la plateforme comme un select contrôlé. UIMenu ou UIPickerView sur iOS, la liste déroulante d'Android, un selectedIndex en entrée, une valeur en sortie, npm install react-native-native-select.
Pourquoi ça demande la New Architecture
Wrapper une vue native a toujours été possible. Ce qui a changé, c'est le coût de le faire proprement.
Avec l'ancien bridge, une vue native impliquait des messages asynchrones entre JavaScript et la plateforme, exactement là où il ne faut pas être quand quelqu'un fait tourner une roue avec son pouce. La New Architecture a remplacé ce renderer par Fabric, et un composant natif se déclare maintenant une seule fois sous forme de spec typée, dont le codegen génère la glue pour les deux plateformes.
Concrètement, 250 pays cessent d'être une liste rendue par React. Ils deviennent un menu construit par le système, et toute l'interaction se joue sur le thread UI. react-native-native-select exige la New Architecture et ne supporte pas l'ancien bridge du tout : c'est une vraie contrainte, et c'est aussi pour ça qu'il tient en moins de 700 lignes de natif, sans aucune dépendance.
Wrapper son propre champ dans le Select
Le composant de l'OS a un prix : il se dessine lui-même. Impossible de donner votre border radius à UIMenu, ou votre police à la liste d'Android.
Alors vous dessinez le champ, et l'OS dessine la liste. Depuis la version 3, votre champ se place dans le Select, comme enfant : l'utilisateur voit votre design, et un tap n'importe où dessus ouvre la vraie liste native, le menu sur iOS, la liste déroulante sur Android.
Sur Android, le composant n'est d'ailleurs plus un AppCompatSpinner. Dans React Native, un spinner ne reflétait pas fiablement un selectedIndex contrôlé, et continuait d'afficher l'ancienne ligne après un changement de valeur côté JavaScript. La version 3 ouvre directement la ListPopupWindow, la même liste que le spinner ouvrait en mode dropdown, ancrée sous votre champ.
L'overlay doit être à opacity 0.02, jamais 0
Avant la version 3, le pattern était inversé : votre champ en dessous, et le vrai select étiré par-dessus, invisible. Cet overlay fonctionne toujours, avec un piège. Mettez-le à opacity: 0 et les taps n'arrivent plus. Rien ne throw, rien ne log, le contrôle est juste mort.
La raison, c'est que le hit testing d'iOS ignore les vues dont l'alpha est inférieur ou égal à 0.01. Une vue totalement transparente n'est pas une vue touchable. Une opacité de 0.02 passe ce seuil tout en restant invisible à l'oeil. Tout le pattern d'overlay tient sur cette constante.
Si une zone de tap invisible arrête de répondre sur iOS, vérifiez son opacité avant tout le reste. Tout ce qui est à 0.01 ou en dessous est intouchable, et aucun warning n'est levé.
C'est devenu une règle dans tout notre code : les overlays de tap invisibles sont à 0.02, jamais à 0.
L'overlay avait un second point faible, sur Android. Un touch va d'abord au sibling qui a la plus forte élévation, quel que soit l'ordre côté JavaScript, donc un champ surélevé à côté de l'overlay pouvait prendre le tap et laisser la liste fermée. Un parent voit le touch avant tous ses enfants : c'est pour ça que la version 3 wrappe le champ.
Ce qu'il ne fait pas
Quatre limites à connaître avant d'installer.
Sélection simple uniquement. Un index en entrée, une valeur en sortie. Le multi-select se construit par-dessus en JavaScript, et c'est ce qu'on fait : notre champ de formulaire transforme les choix successifs en chips supprimables.
Des strings, rien d'autre. Les options sont un tableau de strings, pas des objets avec
labeletvalueséparés, et il n'y a pas d'icônes.Le choix roue ou menu est propre à iOS. Il ne vaut que pour un
Selectsans champ à l'intérieur : avec un champ, iOS ouvre toujours le menu. Android ouvre toujours sa liste déroulante, et la couleur de texte personnalisée est également réservée à iOS.Pas d'Expo Go. Le package embarque du natif et aucun config plugin : il demande un projet bare ou un prebuild.
Le lecteur d'écran reconnaît déjà le composant
La lib elle-même ne pose ni rôle ni label d'accessibilité, et pour l'essentiel elle n'en a pas besoin : ce qui est à l'écran est réellement un UIMenu, un UIPickerView ou la liste déroulante d'Android, donc VoiceOver et TalkBack savent déjà ce que c'est et comment l'annoncer.
Le reste est à votre charge, dans la couche du dessus. Notre champ de formulaire partagé ajoute un rôle de bouton, un label qui combine le nom du champ et la valeur courante, et marque les enfants visuels comme non accessibles pour que l'ensemble se lise comme un seul contrôle au lieu de trois fragments.
Et une leçon qui n'a rien à voir avec la lib : une roue, ça n'a de sens que pour quelqu'un qui voit l'écran. Sur l'écran où on saisit l'âge d'un animal, on détecte un lecteur d'écran actif et on propose un champ numérique avec des options à cocher plutôt que deux roues.
Où ce composant tourne en production
Animalert est une plateforme associative pour les animaux perdus et trouvés : quelqu'un signale un animal disparu, ou un animal qu'il vient de recueillir, et les personnes autour sont prévenues. L'app est publiée sur l'App Store et Google Play.
Le select est dans l'inscription, les formulaires de signalement, les profils d'animaux et les options d'affiche : sept appels directs, plus un champ de formulaire partagé qui l'enveloppe et qu'on réutilise dans une dizaine d'écrans.
Quand l'utiliser, et quand préférer une lib JS
Si vous avez besoin d'un dropdown cherchable, thémable, richement stylé, avec multi-select et chips, partez sur une des libs JavaScript matures. Elles sont populaires pour de bonnes raisons, elles font bien plus que la nôtre, et les reconstruire n'est pas l'affaire d'un week-end.
Si ce que vous voulez, c'est le contrôle que l'OS dessine déjà, avec sa sémantique et son scroll intacts, alors react-native-native-select est le seul choix hors Expo : npm install react-native-native-select, un composant, zéro dépendance, et rien à restyler le jour où l'OS change. 250 pays nous en ont convaincus.
Le code est sur GitHub, les issues et les PR sont ouvertes, et le README documente en entier le pattern du champ wrappé, avec un composant de champ complet et les props d'accessibilité qui vont avec.
Quelques questions qui reviennent souvent
Hors Expo, react-native-native-select est la seule option : npm install react-native-native-select. Il rend le menu déroulant UIMenu d'iOS, ou la roue UIPickerView si vous la préférez, et la liste déroulante d'Android. @react-native-picker/picker ne rend que la roue sur iOS, et trois des quatre libs de dropdown les plus installées redessinent la liste en JavaScript.
@react-native-menu/menu rend un vrai UIMenu, mais c'est un menu d'actions : un identifiant dans onPressAction, aucun selectedIndex contrôlé, et un PopupMenu sur Android. @expo/ui rend bien un dropdown natif, en SwiftUI, mais il demande Expo. Dans un projet React Native sans Expo, react-native-native-select est le seul select contrôlé qui rend le dropdown de la plateforme.
Non. Il a été sorti du framework pendant le Lean Core et survit sous la forme du package communautaire @react-native-picker/picker. Attention à ce qu'il couvre : sur iOS, il ne rend que la roue UIPickerView, jamais le menu déroulant natif que la plateforme a depuis iOS 14.
Pas react-native-native-select : il repose sur Fabric et le codegen, et exige que la New Architecture soit activée. Un projet encore sur l'ancien bridge doit se tourner vers une lib écrite en JavaScript.
Le hit testing d'iOS ignore les vues dont l'alpha est inférieur ou égal à 0.01, si bien qu'un overlay totalement transparent devient intouchable même si sa mise en page est correcte. Une opacité de 0.02 reste invisible à l'oeil et détectable par le système.
Pas un select qui embarque son propre code natif. Expo Go n'exécute que les modules compilés à l'intérieur, donc un package comme react-native-native-select demande un dev build ou un projet prebuild.
La différence tient à l'endroit où le travail se fait plutôt qu'à la vitesse brute. Un composant de plateforme se construit et s'anime sur le thread UI, donc une longue liste d'options n'entre pas en concurrence avec ce que fait JavaScript au même moment. Ça se voit surtout sur les Android d'entrée de gamme.