Interface dynamique Shiny : renderUI, insertUI, conditionalPanel et fenêtres modales en R
Construisez une interface que le server crée et modifie à l’exécution — des contrôles qui dépendent des données, des blocs à afficher/masquer, des éléments à ajouter/retirer, des fenêtres modales, des notifications et un verrouillage soigné.
La plupart de l’interface Shiny est figée dans le ui, mais les applications les plus puissantes la construisent et la modifient à l’exécution. Ce module final couvre la boîte à outils de l’interface dynamique : renderUI/uiOutput pour des contrôles générés par le serveur, insertUI/removeUI pour ajouter et retirer des éléments à la volée, conditionalPanel pour afficher ou masquer un bloc côté client, update*Input pour modifier un widget existant, showModal et showNotification pour les boîtes de dialogue et les notifications, et req/validate/need pour verrouiller les sorties proprement — pour chacun le code, ce qu’il fait, et quand y recourir.
- L’interface dynamique est une interface que le serveur construit ou modifie à l’exécution — des contrôles dont l’existence ou les options ne sont pas connues tant que l’application n’est pas lancée, des blocs qui apparaissent et disparaissent, des boîtes de dialogue et des alertes.
renderUI()+uiOutput()construisent un morceau d’interface sous forme de valeur réactive — utilisez-les quand l’interface elle-même doit être calculée (une liste déroulante dont les choix proviennent d’un fichier téléversé, des curseurs générés un par colonne).insertUI()/removeUI()ajoutent ou retirent des éléments à la volée — le motif « ajouter une ligne », quand vous ne savez pas d’avance combien de champs l’utilisateur voudra.conditionalPanel()affiche ou masque un bloc dans le navigateur — une bascule purement côté client sur une entrée, sans aller-retour au serveur ; elle utilise une condition JavaScript (input.id, un point, pas$).showModal()/showNotification()sont des boîtes de dialogue et des notifications, etreq()/validate()/need()verrouillent les sorties pour que rien n’affiche une vilaine erreur avant que les entrées ne soient prêtes.
Introduction
Tout ce que cette série a construit jusqu’ici était une interface figée : vous écrivez un selectInput() ou un plotOutput() dans le ui, et il y reste. Mais les applications les plus capables vont plus loin — elles construisent et modifient l’interface en cours d’exécution. Une liste déroulante dont les choix proviennent du fichier que l’utilisateur vient de téléverser. Un bouton « supprimer » qui apparaît dès qu’il y a quelque chose à supprimer. Une boîte de dialogue de réglages qui surgit par-dessus la page. Un panneau qui ne s’affiche que lorsqu’une case est cochée.
Voilà l’interface dynamique : une interface que le serveur construit ou modifie à l’exécution, plutôt qu’une interface écrite à l’avance dans la fonction ui. Vous en avez déjà rencontré la pierre angulaire — renderUI() dans les affichages de sortie construit une interface sous forme de valeur réactive. Ce module final est la boîte à outils complète qui l’entoure. Chaque outil ci-dessous s’accompagne de trois choses : le code, ce qu’il fait, et quand y recourir.
Le code de cette page n’est pas exécuté ici — une application Shiny a besoin d’une session R active, elle ne peut donc pas s’exécuter dans une page web statique. Copiez n’importe quel bloc dans un fichier app.R et exécutez-le localement avec shiny::runApp() (ou cliquez sur Run App dans RStudio). Chaque extrait montre une technique isolément ; placez la partie ui dans votre fluidPage() et la partie server dans votre fonction server.
Contrôles générés par le serveur : renderUI() et uiOutput()
Quand l’interface elle-même doit être calculée, vous la construisez dans le server et le ui en affiche le résultat. renderUI() renvoie des éléments d’interface Shiny sous forme de valeur réactive ; uiOutput() réserve l’emplacement où ils se posent. C’est le cheval de bataille de l’interface dynamique — recourez-y dès que l’existence ou les options d’un contrôle ne sont pas connues tant que l’application n’est pas lancée.
# ui — reserve a spot for server-built controls
uiOutput("column_picker")
# server — build the dropdown after launch, from the data
output$column_picker <- renderUI({
selectInput("col", "Column:", choices = names(dataset()))
})Comme le corps est réactif, l’interface se reconstruit chaque fois que quelque chose qu’il lit change — téléversez un nouveau fichier et les choix de la liste déroulante se rafraîchissent d’eux-mêmes. Un motif courant consiste à générer plusieurs contrôles à la fois, un par colonne, avec lapply() enveloppé dans tagList() :
output$sliders <- renderUI({
df <- dataset()
num_cols <- names(df)[sapply(df, is.numeric)]
tagList(
lapply(num_cols, function(col) {
sliderInput(col, col,
min = min(df[[col]]), max = max(df[[col]]),
value = range(df[[col]]))
})
)
})Utilisez renderUI quand le contrôle doit être créé depuis le serveur — les choix, le nombre ou le type dépendent de données ou d’un état dont vous ne disposez qu’à l’exécution. Le compromis : à chaque réaffichage, il reconstruit les contrôles de zéro, ce qui réinitialise leurs valeurs (voir Problèmes courants). Pour un contrôle que vous pouvez écrire au démarrage, déclarez-le directement dans le ui ; pour un contrôle qui existe déjà et n’a besoin que de nouveaux choix ou d’une nouvelle valeur, utilisez update*Input() (ci-dessous) plutôt que de le reconstruire.
Ajouter et retirer des éléments à la volée : insertUI() et removeUI()
renderUI() remplace tout un bloc à chaque fois. Quand vous voulez plutôt ajouter un nouvel élément sans perturber ce qui existe déjà — le classique formulaire « ajouter une ligne » — utilisez insertUI(), et removeUI() pour reprendre un élément.
insertUI() injecte de l’interface par rapport à un sélecteur CSS : choisissez un élément d’ancrage, indiquez où va la nouvelle interface ("beforeEnd", "afterEnd", "beforeBegin", "afterBegin"), et passez l’interface à insérer.
# ui — a button and a container to grow
actionButton("add", "Add another input")
tags$div(id = "inputs_here")
# server — each click appends one more text box inside the container
observeEvent(input$add, {
insertUI(
selector = "#inputs_here",
where = "beforeEnd",
ui = textInput(paste0("name_", input$add), "Name:")
)
})removeUI() supprime l’élément correspondant à un sélecteur. Enveloppez chaque bloc inséré dans un div avec un id connu pour pouvoir le cibler plus tard :
observeEvent(input$remove_last, {
removeUI(selector = "#inputs_here .shiny-input-container:last-child")
})Utilisez insertUI/removeUI pour un ensemble d’éléments qui grandit ou rétrécit — un formulaire où l’utilisateur ajoute des lignes une à une, une liste de filtres qu’il construit, des onglets ajoutés dynamiquement. La différence essentielle avec renderUI : insérer/retirer laisse les éléments existants intacts (leurs valeurs survivent donc), alors que renderUI reconstruit le bloc entier. Insérez quand vous ajoutez à un ensemble ; affichez avec render quand vous remplacez un bloc.
Afficher ou masquer un bloc côté client : conditionalPanel()
Parfois vous n’avez pas besoin de construire l’interface — elle existe déjà, vous voulez seulement l’afficher ou la masquer selon une entrée. conditionalPanel() fait cela entièrement dans le navigateur, sans aller-retour au serveur, c’est donc instantané et économique.
Le piège qui surprend tout le monde : sa condition est une expression JavaScript, écrite sous forme de chaîne — vous référencez donc une entrée avec input.id (un point), jamais le input$id de R.
# ui — the panel shows only when the "advanced" checkbox is ticked
checkboxInput("advanced", "Show advanced options", value = FALSE)
conditionalPanel(
condition = "input.advanced == true", # JavaScript: input.advanced, a dot, not input$advanced
sliderInput("alpha", "Transparency:", 0, 1, 0.7),
selectInput("palette", "Palette:", c("viridis", "magma"))
)La condition peut lire la valeur de n’importe quelle entrée : "input.plot_type == 'scatter'" affiche un panneau uniquement pour l’option scatter ; "input.n > 100" affiche un avertissement au-delà d’un seuil. Pour tester une valeur côté serveur (une valeur qui n’est pas une entrée), exposez-la d’abord avec output et référencez-la comme output.id dans la condition.
Utilisez conditionalPanel pour de simples bascules afficher/masquer sur une entrée — des réglages optionnels derrière une case « Avancé », un panneau pertinent uniquement pour un type de graphique, un texte d’aide qui apparaît à la demande. C’est plus léger et plus vif que renderUI parce que rien n’est envoyé au serveur puis renvoyé. Quand le choix d’afficher implique un vrai calcul ou des données côté serveur, revenez à renderUI ; pour un simple « affiche ceci si cette entrée vaut X », conditionalPanel est le bon outil.
Modifier un widget existant : update*Input()
Vous les avez rencontrés dans les contrôles d’entrée, mais ils ont aussi leur place dans la boîte à outils de l’interface dynamique, car ils sont l’alternative légère à la reconstruction. Quand un widget existe déjà dans le ui et que vous n’avez qu’à le modifier — réapprovisionner ses choices, réinitialiser sa value, le réétiqueter — appelez le update*Input(session, "id", …) correspondant depuis le server.
# ui — the widget exists from the start
selectInput("col", "Column:", choices = NULL)
# server — fill its choices once the data arrives, without rebuilding the widget
observeEvent(dataset(), {
updateSelectInput(session, "col", choices = names(dataset()))
})Chaque famille d’entrées a sa fonction de mise à jour : updateSelectInput(), updateSliderInput(), updateTextInput(), updateNumericInput(), updateCheckboxInput(), et ainsi de suite. Elles prennent l’objet session plus l’id de l’entrée et les arguments à modifier.
Utilisez update*Input dès que le widget existe déjà et que seul son état doit changer — c’est presque toujours préférable à renderUI, parce que la mise à jour préserve l’identité du widget (et la sélection actuelle de l’utilisateur là où elle s’applique encore) au lieu de le détruire et de le recréer. Réservez renderUI aux cas où le contrôle doit être créé de zéro, et non simplement mis à jour.
Boîtes de dialogue et notifications : showModal() et showNotification()
Deux fonctions du server posent une interface transitoire par-dessus la page sans modifier la mise en page.
modalDialog() construit une boîte de dialogue ; showModal() l’affiche ; removeModal() la ferme. Utilisez une fenêtre modale pour quelque chose qui doit interrompre et exiger l’attention — une confirmation, un formulaire, un message détaillé.
# server — pop a confirmation dialog when the delete button is clicked
observeEvent(input$delete, {
showModal(modalDialog(
title = "Delete this record?",
"This action cannot be undone.",
footer = tagList(
modalButton("Cancel"),
actionButton("confirm_delete", "Delete", class = "btn-danger")
)
))
})
# closing it once the user confirms
observeEvent(input$confirm_delete, {
# ... do the delete ...
removeModal()
})showNotification() affiche une petite notification qui disparaît d’elle-même dans le coin — l’outil idéal pour un retour non bloquant sur lequel l’utilisateur n’a pas à agir : « Enregistré », « Export terminé », un avertissement. Réglez type pour la colorer et duration pour contrôler combien de temps elle reste (NULL la garde jusqu’à fermeture).
observeEvent(input$save, {
# ... save ...
showNotification("Changes saved.", type = "message", duration = 3)
})Utilisez une fenêtre modale quand l’utilisateur doit répondre avant de continuer (confirmer, remplir, accuser réception) ; utilisez une notification pour un retour passif et non bloquant (une mise à jour de statut, un message de succès ou d’avertissement). Une fenêtre modale bloque la page jusqu’à sa fermeture ; une notification se glisse, s’attarde, et s’estompe d’elle-même.
Verrouiller les sorties proprement : req(), validate() et need()
Les applications dynamiques ont un problème au démarrage : au premier instant, les entrées peuvent être vides, un fichier n’est pas encore téléversé, un contrôle généré par renderUI n’existe pas. Exécutez quand même votre code de sortie et l’utilisateur voit une erreur rouge clignoter avant que rien ne soit prêt. req(), validate() et need() mettent fin à cela.
req() exige que ses arguments soient « truthy » (présents, non-NULL, non vides) ; si l’un d’eux ne l’est pas, il stoppe silencieusement le rendu en cours — pas d’erreur, juste un vide qui se remplit dès que l’exigence est satisfaite.
output$plot <- renderPlot({
req(input$file, input$column) # do nothing until both exist
df <- read.csv(input$file$datapath)
hist(df[[input$column]])
})validate() avec need() fait la même chose mais vous permet d’afficher un message convivial au lieu d’un vide, et d’imposer une condition (pas seulement une présence) :
output$plot <- renderPlot({
validate(
need(input$file, "Please upload a CSV file to begin."),
need(input$column != "", "Select a column to plot.")
)
df <- read.csv(input$file$datapath)
hist(df[[input$column]])
})Utilisez req() pour attendre silencieusement les entrées (le cas courant — la plupart des sorties ne doivent simplement pas s’exécuter tant que leurs entrées n’existent pas) ; utilisez validate() + need() quand vous voulez dire à l’utilisateur pourquoi rien ne s’affiche (« téléversez d’abord un fichier »). Dans les deux cas, vous évitez les vilaines erreurs transitoires qui font qu’une application dynamique paraît cassée au démarrage.
Aller plus loin : htmlwidgets et JavaScript personnalisé
Quand les fonctions intégrées ne suffisent pas, deux échappatoires étendent Shiny sans limite. Les packages htmlwidgets (plotly, leaflet, DT, visNetwork) enveloppent des bibliothèques JavaScript complètes en tant que sorties Shiny ordinaires — la même association render*()/*Output() que vous connaissez déjà, mais le résultat est une carte, un graphique ou un réseau interactif et riche. Et pour un comportement vraiment personnalisé, JavaScript peut renvoyer une valeur vers Shiny avec Shiny.setInputValue("id", value), qui se lit ensuite dans le server comme input$id — le pont pour des widgets d’entrée personnalisés, le glisser-déposer et l’interface tierce. Ce sont de plus vastes sujets ; recourez d’abord à un htmlwidget (c’est presque toujours suffisant) et ne descendez au JS personnalisé que lorsque rien de tout fait ne convient.
Quel outil pour quelle tâche
Un repère rapide. La technique que vous choisissez découle de ce que vous changez et quand.
| Vous voulez… | Utilisez | Ce que ça fait |
|---|---|---|
| Construire un contrôle à partir des données / de l’état | renderUI() + uiOutput() |
le serveur construit l’interface sous forme de valeur réactive |
| Ajouter des éléments un à la fois | insertUI() |
ajoute de l’interface sans perturber les éléments existants |
| Retirer un élément | removeUI() |
supprime l’interface correspondant à un sélecteur |
| Afficher / masquer un bloc selon une entrée | conditionalPanel() |
bascule côté client, sans aller-retour au serveur |
| Modifier un widget existant | update*Input(session, …) |
réapprovisionne choix / valeur sans reconstruire |
| Une boîte de dialogue bloquante | showModal() + modalDialog() |
dialogue que l’utilisateur doit fermer ; removeModal() la ferme |
| Un retour passif | showNotification() |
notification dans le coin qui disparaît d’elle-même |
| Attendre les entrées silencieusement | req() |
stoppe le rendu jusqu’à ce que les exigences soient truthy |
| Attendre avec un message | validate() + need() |
affiche un texte convivial jusqu’à ce que les conditions soient remplies |
Les deux distinctions à retenir : renderUI remplace un bloc (et le réinitialise) ; insertUI ajoute à un ensemble (et le préserve) ; et conditionalPanel affiche/masque dans le navigateur (économique, instantané) tandis que renderUI reconstruit via le serveur (ne l’utilisez que lorsqu’un vrai calcul est en jeu). Pour modales vs notifications : modale = l’utilisateur doit agir ; notification = on informe simplement l’utilisateur.
Problèmes fréquents
Une entrée construite par renderUI perd sa valeur à chaque réaffichage. Comme renderUI() reconstruit son interface de zéro chaque fois que ses dépendances réactives changent, tout contrôle qu’il a créé est détruit puis recréé avec les valeurs par défaut — la sélection de l’utilisateur disparaît. Si le widget doit persister et ne changer qu’occasionnellement, déclarez-le une fois dans le ui et utilisez update*Input() pour le modifier, ou utilisez insertUI() pour ajouter des éléments sans reconstruire tout le bloc. Réservez renderUI à l’interface qui doit véritablement être reconstruite.
conditionalPanel ignore votre condition parce qu’elle utilise input$id. La condition est une expression JavaScript, pas du R — une entrée est donc input.id avec un point, jamais input$id avec un signe dollar, et les valeurs de chaîne ont besoin de guillemets : "input.type == 'scatter'", pas "input$type == scatter". Une mauvaise syntaxe échoue silencieusement (le panneau ne s’affiche jamais ou s’affiche toujours). Écrivez la condition en termes JavaScript, et rappelez-vous que true/false sont en minuscules.
Les sorties affichent une erreur rouge au démarrage avant que les entrées n’existent. Quand une sortie lit une entrée vide ou un fichier qui n’est pas encore téléversé, le code s’exécute quand même et échoue au premier passage. Protégez-le avec req() en tête du rendu — req(input$file) — pour que la sortie attende simplement que ses entrées soient prêtes au lieu d’échouer. Utilisez validate(need(...)) si vous préférez montrer à l’utilisateur un message expliquant ce qui manque.
Questions fréquentes
Utilisez renderUI() dans le server et uiOutput() dans le ui. Construisez l’interface à l’intérieur de renderUI({ ... }) — en lisant des entrées ou des données — et renvoyez des éléments d’interface Shiny (un contrôle unique, ou plusieurs enveloppés dans tagList()) ; ils se reconstruisent automatiquement chaque fois que les entrées qu’ils lisent changent. Pour un contrôle qui existe déjà et n’a besoin que de nouveaux choix ou d’une valeur, utilisez plutôt update*Input(), qui est plus léger et conserve l’état du widget.
Construisez la boîte de dialogue avec modalDialog() et affichez-la avec showModal() depuis le server, généralement à l’intérieur d’un observeEvent() : observeEvent(input$go, { showModal(modalDialog(title = "...", "body text")) }). Ajoutez des boutons dans le footer (un modalButton("Cancel") pour fermer, un actionButton() pour agir), et appelez removeModal() pour la fermer par programme après la confirmation de l’utilisateur.
renderUI() construit l’interface sur le serveur et l’envoie à la page — utilisez-le quand l’interface doit être calculée à partir des données ou de l’état. conditionalPanel() affiche ou masque une interface qui existe déjà, entièrement dans le navigateur sans aller-retour au serveur — utilisez-le pour une simple bascule sur une entrée. conditionalPanel est plus économique et instantané ; ne recourez à renderUI que lorsqu’un vrai calcul côté serveur décide de l’interface.
Utilisez insertUI() pour ajouter un élément (donnez-lui un selector cible, une position where comme "beforeEnd", et le ui à insérer) et removeUI() pour en supprimer un par sélecteur. Contrairement à renderUI(), ces fonctions laissent les éléments existants intacts, ce sont donc le bon outil pour un formulaire « ajouter une ligne » où l’utilisateur fait grandir un ensemble d’entrées une à une et où vous ne voulez pas réinitialiser les autres.
input.id au lieu de input$id ?
Parce que sa condition est une expression JavaScript qui s’exécute dans le navigateur, pas du code R. JavaScript accède à la valeur d’une entrée comme input.id (avec un point), donc condition = "input.show == true" est correct et "input$show" échoue silencieusement. Les comparaisons de chaînes ont besoin de guillemets dans la condition ("input.type == 'bar'"), et les booléens sont en minuscules true/false.
Testez vos connaissances
Construisez une application où le ui a une case showOptions et deux régions uiOutput/conditionalPanel. (1) Utilisez renderUI pour créer un selectInput("col", …) dont les choix sont les noms de colonnes de mtcars. (2) Enveloppez un sliderInput("bins", …) dans un conditionalPanel qui n’apparaît que lorsque showOptions est cochée. Rappelez-vous que la condition est du JavaScript.
Pour (1) : uiOutput("col_ui") dans le ui, et output$col_ui <- renderUI({ selectInput("col", "Column:", choices = names(mtcars)) }) dans le server. Pour (2) : conditionalPanel(condition = "input.showOptions == true", sliderInput("bins", "Bins:", 1, 50, 30)) — notez input.showOptions avec un point, et true en minuscules.
library(shiny)
ui <- fluidPage(
checkboxInput("showOptions", "Show options", value = FALSE),
uiOutput("col_ui"),
conditionalPanel(
condition = "input.showOptions == true", # JavaScript: dot, lowercase true
sliderInput("bins", "Bins:", min = 1, max = 50, value = 30)
)
)
server <- function(input, output) {
output$col_ui <- renderUI({
selectInput("col", "Column:", choices = names(mtcars))
})
}
shinyApp(ui, server)renderUI/uiOutput construit la liste déroulante à partir de names(mtcars) à l’exécution, et conditionalPanel affiche le curseur uniquement lorsque la case est cochée — input.showOptions (un point) parce que la condition est du JavaScript, pas du R.
Vous voulez qu’un bloc de réglages n’apparaisse que lorsque input$mode == "advanced", sans aller-retour au serveur et sans reconstruction. Quel outil convient le mieux ?
A. renderUI() — construire le bloc sur le serveur quand le mode est avancé. B. conditionalPanel(condition = "input.mode == 'advanced'", ...) — le basculer dans le navigateur. C. insertUI() — insérer le bloc chaque fois que le mode change. D. updateSelectInput() — modifier les choix de l’entrée de mode.
B. Un simple afficher/masquer sur la valeur d’une entrée est exactement ce à quoi sert conditionalPanel() — il bascule côté client sans aller-retour au serveur et le bloc existe déjà, donc rien n’est reconstruit. A (renderUI) impliquerait inutilement le serveur et reconstruirait le bloc ; C (insertUI) sert à ajouter de nouveaux éléments, pas à basculer un élément existant ; D (updateSelectInput) modifie les choix d’une entrée, pas la visibilité d’un panneau. Notez que la condition est du JavaScript : input.mode avec un point, la chaîne entre guillemets.
Conclusion
Ceci achève la série UI de Shiny. Vous disposez maintenant de toute la boîte à outils de l’interface dynamique : renderUI/uiOutput pour des contrôles générés par le serveur, insertUI/removeUI pour ajouter et retirer des éléments sans perturber le reste, conditionalPanel pour afficher ou masquer un bloc dans le navigateur, update*Input() pour modifier un widget existant, showModal/showNotification pour les boîtes de dialogue et les notifications, et req/validate/need pour verrouiller les sorties proprement. Les distinctions directrices : construire vs remplacer vs basculer (renderUI remplace, insertUI ajoute, conditionalPanel affiche/masque), et bloquer vs informer (modale vs notification).
Avec les entrées, les sorties, les mises en page, le style, la responsivité, et maintenant l’interface dynamique, vous pouvez construire n’importe quelle interface dont une application Shiny a besoin. La pièce suivante est l’autre moitié de toute application — la logique du server qui fait tout réagir : expressions réactives, observateurs, et la gestion d’événements qui transforme ces contrôles en une application vivante.
Leçons connexes
- À partir d’ici : le design responsive — les conteneurs
bslibdans lesquels votre interface dynamique se réorganise (la leçon précédente) ; les affichages de sortie — oùrenderUI/uiOutputa été introduit ; la programmation réactive — le modèle de réactivité qui fait recalculerrenderUI, les observateurs etreq()d’eux-mêmes. Logique du server — les expressions réactives, les observateurs et la gestion d’événements qui pilotent ces contrôles. - Pour aller plus loin : Shiny UI — la série complète. · Science computationnelle & IA — le pilier.
Réutilisation
Citation
@online{2026,
author = {},
title = {Interface dynamique Shiny : renderUI, insertUI,
conditionalPanel et fenêtres modales en R},
date = {2026-06-28},
url = {https://www.datanovia.com/learn/programming/shiny/ui/advanced-ui},
langid = {fr}
}