Chapter section list

  • Validation, informing the user when an input (or combination of inputs) is in an invalid state. See Section 8.1.
  • Notification, sending general messages to the user, and progress bars, which give details for time consuming operations made up of many small steps. See Section 8.2.
  • Confirmation dialogs or the ability to undo an action to give users peace of mind in dangerous situations. See Section 8.4.

Resource 8.1 : Packages used in this chapter

8.1 Validation

8.1.1 Validating input

Using {shinyFeedback} is a two step process.

  1. First, you add shinyFeedback::useShinyFeedback() to the UI. This sets up the needed HTML and JavaScript for attractive error message display.
  2. Then in your shiny::server() function, you call one of the feedback functions:
  • feedback(),
  • feedbackWarning(),
  • feedbackDanger(), and
  • feedbackSuccess().

They all have six arguments, where the first three are essential and the last one (session) is for advanced usage.

  • inputId, the id of the input where the feedback should be placed.
  • show, a logical determining whether or not to show the feedback.
  • text, the text to display.
  • color, the color of the feedback and
  • icon, an html icon tag.

8.1.2 Cancelling execution

You may have noticed that when you start an app, the complete reactive graph is computed even before the user does anything. This works well when you can choose meaningful default values for your inputs. But that’s not always possible, and sometimes you want to wait until the user actually does something. This tends to crop up with three controls:

  • In textInput(), you’ve used value = “” and you don’t want to do anything until the user types something.
  • In selectInput(), you’ve provide an empty choice, ““, and you don’t want to do anything until the user makes a selection.
  • In fileInput(), which has an empty result before the user has uploaded anything. We’ll come back to this in Section 9.1.

We need some way to “pause” reactives so that nothing happens until some condition is true. That’s the job of req(), which checks for required values before allowing a reactive producer to continue.

Code Collection 8.2 : Canceling execution

req() works by signalling a special condition. This special condition causes all downstream reactives and outputs to stop executing. Technically, it leaves any downstream reactive consumers in an invalidated state. We’ll come back to this terminology in (XXX_16?).

req() is designed so that req(input$x) will only proceed if the user has supplied a value, regardless of the type of the input control. You can also use req() with your own logical statement if needed. For example, req(input$a > 0) will permit computation to proceed when a is greater than 0; this is typically the form you’ll use when performing validation, as we’ll see next.

8.1.3 req() and validation

Let’s combine req() and {shinyFeedback} to solve a more challenging problem. I’m going to return to the simple app we made in Chapter 1 which allowed you to select a built-in dataset and see its contents. We are going to make R Code 1.7 more general and more complex by using textInput() instead of selectInput().

  1. The UI will change very little.
  2. But the server function needs to get a little more complex. We’re going to use req() in two ways:
    • We only want to proceed with computation if the user has entered a value so we do req(input$dataset).
    • Then we check to see if the supplied name actually exists. If it doesn’t, we display an error message, and then use req() to cancel computation.
Note 8.1

Note the use of cancelOutput = TRUE: normally cancelling a reactive will reset all downstream outputs but cancelOutput = TRUE leaves them displaying the last good value. This is important for textInput() which may trigger an update while you’re in the middle of typing a name.

8.1.4 Validate output

{shinyFeedback} is great when the problem is related to a single input. But sometimes the invalid state is a result of a combination of inputs. In this case it doesn’t really make sense to put the error next to an input (which one would you put it beside?) and instead it makes more sense to put it in the output.

You can do so with a tool built into shiny: validate(). When called inside a reactive or an output, validate(message) stops execution of the rest of the code and instead displays message in any downstream outputs.

If the inputs are valid, the output shows the transformation. If the combination of inputs is invalid, then the output is replaced with an informative message.

8.2 Notification

If there isn’t a problem and you just want to let the user know what’s happening, then you want a notification. In Shiny, notifications are created with showNotification(), and stack in the bottom right of the page. There are three basic ways to use showNotification():

  1. To show a transient notification that automatically disappears after a fixed amount of time.
  2. To show a notification when a process starts and remove it when the process ends.
  3. To update a single notification with progressive updates.

8.2.1 Transient notification

8.2.2 Removing on completion

It’s often useful to tie the presence of a notification to a long-running task. In this case, you want to show the notification when the task starts, and remove the notification when the task completes. To do this, you’ll need to:

Set duration = NULL and closeButton = FALSE so that the notification stays visible until the task is complete.

Store the id returned by showNotification(), and then pass this value to removeNotification(). The most reliable way to do so is to use on.exit(), which ensures that the notification is removed regardless of how the task completes (either successfully or with an error). You can learn more about on.exit() in Changing and restoring state1

8.2.3 Progressive updates

As you saw in the first example, multiple calls to showNotification() usually create multiple notifications. You can instead update a single notification by capturing the id from the first call and using it in subsequent calls. This is useful if your long-running task has multiple subcomponents. You can see the results in https://hadley.shinyapps.io/ms-notification-updates.

Note 8.2

I have added the “Start” button so that I can repeat the calculation without running the program from scratch. For this small change I had to change reactive() to eventReactive().

I also tried to implement a “Reset” button, but I didn’t know how to do it at my current state of knowledge. Maybe I will come back, when I learned more and understand Shiny better. (A possible option would be to use {shinycssloaders} because it replaces the output during calculation with a spinner.)

8.3 Progress bars

For long-running tasks, the best type of feedback is a progress bar. In this section, I’ll show two techniques for displaying progress bars, one built into Shiny, and one from the {waiter} package developed by John Coene (2022).

Unfortunately both techniques suffer from the same major drawback: to use a progress bar you need to be able to divide the big task into a known number of small pieces that each take roughly the same amount of time. This is often hard, particularly since the underlying code is often written in C and it has no way to communicate progress updates to you. We are working on tools in the {progress} package so that packages like {dplyr}, {readr}, and {vroom} will one day generate progress bars that you can easily forward to Shiny.

Note 8.3

If your code doesn’t have a loop or apply/map function, it#s going to be very difficult to implement a progress bar.

8.3.1 Shiny

A few things to note:

  • I used the optional message argument to add some explanatory text to the progress bar.
  • I used Sys.sleep() to simulate a long running operation; in your code this would be a slow function.
  • I allowed the user to control when the event starts by combining a button with eventReactive(). This is good practice for any task that requires a progress bar.
  • Additionally, I (pb) added label before the output of the random number. Otherwise one could believe that the number is a time indicator ranging from 0 to 1.

8.3.2 Waiter

The built-in progress bar is great for the basics, but if you want something that provides more visual options, you might try the {waiter} package. Adapting the above code to work with Waiter is straightforward.

  • In the UI we just have to add waiter::use_waitress() to invoke the {waiter} package.
  • The interface for Waiter’s progress bars are a little different. The {waiter} package uses an R6 object to bundle all progress related functions into a single object.

Code Collection 8.4 : Progress bar with Waiter

The default display is a thin progress bar at the top of the page, but there are a number of ways to customise the output:

  • You can override the default theme to use one of:
    • overlay: an opaque progress bar that hides the whole page
    • overlay-opacity: a translucent progress bar that covers the whole page
    • overlay-percent: an opaque progress bar that also displays a numeric percentage.
  • Instead of showing a progress bar for the entire page, you can overlay it on an existing input or output by setting the selector parameter, e.g.: waitress <- Waitress$new(selector = "#steps", theme = "overlay")

8.3.3 Spinners

Sometimes you don’t know exactly how long an operation will take, and you just want to display an animated spinner that reassures the user that something is happening. You can also use the {**waiter*} package for this task; just switch from using a Waitress to using a Waiter:

The {**waiter*} package provides a large variety of spinners to choose from. There are more than 60 (!) options at ?waiter::spinners. Choose one (e.g.) Waiter$new(html = spin_ripple()).

Watch out! 8.1: The documentation is helpful but outdated.

For instance the function call_waitress() does not exist anymore.

8.3.4 shinycssloaders

An even simpler alternative is to use the {shinycssloaders} package by Dean Attali (2024). It uses JavaScript to listen to Shiny events, so it doesn’t even need any code on the server side. Instead, you just use shinycssloaders::withSpinner() to wrap outputs that you want to automatically get a spinner when they have been invalidated.

Note 8.4

There is a {shinycssloaders} demo, where you can test all different options of the package.

Resource 8.2 : Other shiny tools by Dean Attali

This package is part of a larger ecosystem of packages with a shared vision: solving common Shiny issues and improving Shiny apps with minimal effort, minimal code changes, and clear documentation. Other packages for your Shiny apps:

Package Description Demo
shinyjs 💡 Easily improve the user experience of your Shiny apps in seconds 🔗
shinyalert 🗯️ Easily create pretty popup messages (modals) in Shiny 🔗
shinyscreenshot 📷 Capture screenshots of entire pages or parts of pages in Shiny apps 🔗
timevis 📅 Create interactive timeline visualizations in R 🔗
colourpicker 🎨 A colour picker tool for Shiny and for selecting colours in plots 🔗
shinybrowser 🌐 Find out information about a user’s web browser in Shiny apps 🔗
shinydisconnect 🔌 Show a nice message when a Shiny app disconnects or errors 🔗
shinytip 💬 Simple flexible tooltips for Shiny apps WIP
shinymixpanel 🔍 Track user interactions with Mixpanel in Shiny apps or R scripts WIP
shinyforms 📝 Easily create questionnaire-type forms with Shiny WIP

8.4 Confirming and undoing

Sometimes an action is potentially dangerous, and you either want to make sure that the user really wants to do it, or you want to give them the ability to back out before it’s too late. Of the three options (explicit confirmation, undoing an action and trash) the last one is a complex strategy difficult to realize in Shiny and therefore out of scope of this book.

8.4.1 Explicit confirmation

The simplest approach to protecting the user from accidentally performing a dangerous action is to require an explicit confirmation. The easiest way is to use a dialog box which forces the user to pick from one of a small set of actions. In Shiny, you create a dialog box with modalDialog(). This is called a “modal” dialog because it creates a new “mode” of interaction; you can’t interact with the main application until you have dealt with the dialog.

Imagine you have a Shiny app that deletes some files from a directory (or rows in a database etc). This is hard to undo so you want to make sure that the user is really sure. You could create a dialog box requiring an explicit confirmation:

There are a few small, but important, details to consider when creating a dialog box:

  • What should you call the buttons? It’s best to be descriptive, so avoid yes/no or continue/cancel in favor of recapitulating the key verb.
  • How should you order the buttons? Do you put cancel first (like the Mac), or continue first (like Windows)? Your best option is to mirror the platform that you think most people will be using
  • Can you make the dangerous option more obvious? Here I’ve used class = btn btn-danger to style the button prominently.

There is more advice on choosing th4e right button names and ordering by Jakob Nielson.

There are two new ideas in the server():

  • We use showModal() and removeModal() to show and hide the dialog.
  • We observe events generated by the UI from modal_confirm. These objects aren’t created statically in the ui, but are instead dynamically added in the server() by showModal(). You’ll see that idea in much more detail in (XXX_10?).

8.4.2 Undoing an action

Explicit confirmation is most useful for destructive actions that are only performed infrequently. You should avoid it if you want to reduce the errors made by frequent actions. For example, this technique would not work for twitter — if there was a dialog box that said “Are you sure you want to tweet this?” you would soon learn to automatically click yes, and still feel the same feeling of regret when you notice a typo 10s after tweeting.

In this situation a better approach is to wait few seconds before actually performing the action, giving the user a chance to notice any problems and undo them. This isn’t really an undo (since you’re not actually doing anything), but it’s an evocative word that users will understand.

I illustrate the technique with a website that I personally wish had an undo button: Twitter.

  • The essence of the Twitter UI is very simple: there’s a text area to compose your tweet and a button to send it.
  • The server function is quite complex and requires some techniques that we haven’t talked about. Don’t worry too much about understanding the code, focus on the basic idea: we use some special arguments to observeEvent() to run some code after a few seconds. The big new idea is that we capture the result of observeEvent() and save it to a variable; this allows us to destroy the observer so the code that would really send the tweet is never run.

R Code 8.17 is a complex code chunk. The operator <<- is normally only used in functions, and cause a search to be made through parent environments for an existing definition of the variable being assigned. If such a variable is found (and its binding is not locked) then its value is redefined, otherwise assignment takes place in the global environment.

In this case the <<- are referring to the variables defined outside the observeEvent() functions: waiting and last_message.

8.4.3 Trash

For actions that you might regret days later, a more sophisticated pattern is to implement something like the trash or recycling bin on your computer. When you delete a file, it isn’t permanently deleted but instead is moved to a holding cell, which requires a separate action to empty. This is like the “undo” option on steroids; you have a lot of time to regret your action. It’s also a bit like the confirmation; you have to do two separate actions to make deletion permanent.

The primary downside of this technique is that it is substantially more complicated to implement (you have to have a separate “holding cell” that stores the information needed to undo the action), and requires regular intervention from the user to avoid accumulating. For that reason, I think it’s beyond the scope of all but the most complicated Shiny apps, so I’m not going to show an implementation here.

Glossary Entries

term definition
Conditionx Condition here means a technical term that includes errors, warnings, and messages. See <https://adv-r.hadley.nz/conditions.html>,
R6 An R6 object is an implementation of encapsulated object-oriented programming (OOP) in the R programming language. It provides a way to create classes and objects with both public and private members, allowing for better organization of code and data. More info at <https://adv-r.hadley.nz/r6.html>

Session Info

Session Info

Code
sessioninfo::session_info()
#> ─ Session info ───────────────────────────────────────────────────────────────
#>  setting  value
#>  version  R version 4.5.0 (2025-04-11)
#>  os       macOS Sequoia 15.5
#>  system   aarch64, darwin20
#>  ui       X11
#>  language (EN)
#>  collate  en_US.UTF-8
#>  ctype    en_US.UTF-8
#>  tz       Europe/Vienna
#>  date     2025-06-21
#>  pandoc   3.7.0.2 @ /opt/homebrew/bin/ (via rmarkdown)
#>  quarto   1.8.4 @ /usr/local/bin/quarto
#> 
#> ─ Packages ───────────────────────────────────────────────────────────────────
#>  package      * version    date (UTC) lib source
#>  cli            3.6.5      2025-04-23 [1] CRAN (R 4.5.0)
#>  commonmark     1.9.5      2025-03-17 [1] CRAN (R 4.5.0)
#>  curl           6.3.0      2025-06-06 [1] CRAN (R 4.5.0)
#>  dichromat      2.0-0.1    2022-05-02 [1] CRAN (R 4.5.0)
#>  digest         0.6.37     2024-08-19 [1] CRAN (R 4.5.0)
#>  dplyr          1.1.4      2023-11-17 [1] CRAN (R 4.5.0)
#>  evaluate       1.0.3      2025-01-10 [1] CRAN (R 4.5.0)
#>  farver         2.1.2      2024-05-13 [1] CRAN (R 4.5.0)
#>  fastmap        1.2.0      2024-05-15 [1] CRAN (R 4.5.0)
#>  generics       0.1.4      2025-05-09 [1] CRAN (R 4.5.0)
#>  ggplot2        3.5.2      2025-04-09 [1] CRAN (R 4.5.0)
#>  glossary     * 1.0.0.9003 2025-06-08 [1] local
#>  glue           1.8.0      2024-09-30 [1] CRAN (R 4.5.0)
#>  gtable         0.3.6      2024-10-25 [1] CRAN (R 4.5.0)
#>  htmltools      0.5.8.1    2024-04-04 [1] CRAN (R 4.5.0)
#>  htmlwidgets    1.6.4      2023-12-06 [1] CRAN (R 4.5.0)
#>  jsonlite       2.0.0      2025-03-27 [1] CRAN (R 4.5.0)
#>  kableExtra     1.4.0      2024-01-24 [1] CRAN (R 4.5.0)
#>  knitr          1.50       2025-03-16 [1] CRAN (R 4.5.0)
#>  lifecycle      1.0.4      2023-11-07 [1] CRAN (R 4.5.0)
#>  litedown       0.7        2025-04-08 [1] CRAN (R 4.5.0)
#>  magrittr       2.0.3      2022-03-30 [1] CRAN (R 4.5.0)
#>  markdown       2.0        2025-03-23 [1] CRAN (R 4.5.0)
#>  pillar         1.10.2     2025-04-05 [1] CRAN (R 4.5.0)
#>  pkgconfig      2.0.3      2019-09-22 [1] CRAN (R 4.5.0)
#>  R6             2.6.1      2025-02-15 [1] CRAN (R 4.5.0)
#>  RColorBrewer   1.1-3      2022-04-03 [1] CRAN (R 4.5.0)
#>  rlang          1.1.6      2025-04-11 [1] CRAN (R 4.5.0)
#>  rmarkdown      2.29       2024-11-04 [1] CRAN (R 4.5.0)
#>  rstudioapi     0.17.1     2024-10-22 [1] CRAN (R 4.5.0)
#>  rversions      2.1.2      2022-08-31 [1] CRAN (R 4.5.0)
#>  scales         1.4.0      2025-04-24 [1] CRAN (R 4.5.0)
#>  sessioninfo    1.2.3      2025-02-05 [1] CRAN (R 4.5.0)
#>  stringi        1.8.7      2025-03-27 [1] CRAN (R 4.5.0)
#>  stringr        1.5.1      2023-11-14 [1] CRAN (R 4.5.0)
#>  svglite        2.2.1      2025-05-12 [1] CRAN (R 4.5.0)
#>  systemfonts    1.2.3      2025-04-30 [1] CRAN (R 4.5.0)
#>  textshaping    1.0.1      2025-05-01 [1] CRAN (R 4.5.0)
#>  tibble         3.3.0      2025-06-08 [1] CRAN (R 4.5.0)
#>  tidyselect     1.2.1      2024-03-11 [1] CRAN (R 4.5.0)
#>  vctrs          0.6.5      2023-12-01 [1] CRAN (R 4.5.0)
#>  viridisLite    0.4.2      2023-05-02 [1] CRAN (R 4.5.0)
#>  xfun           0.52       2025-04-02 [1] CRAN (R 4.5.0)
#>  xml2           1.3.8      2025-03-14 [1] CRAN (R 4.5.0)
#>  yaml           2.3.10     2024-07-26 [1] CRAN (R 4.5.0)
#> 
#>  [1] /Library/Frameworks/R.framework/Versions/4.5-arm64/library
#>  [2] /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/library
#>  * ── Packages attached to the search path.
#> 
#> ──────────────────────────────────────────────────────────────────────────────

References

Attali, Dean, and Andras Sali. 2024. “Shinycssloaders: Add Loading Animations to a ’Shiny’ Output While It’s Recalculating.” https://doi.org/10.32614/CRAN.package.shinycssloaders.
Coene, John. 2022. “Waiter: Loading Screen for ’Shiny’.” https://doi.org/10.32614/CRAN.package.waiter.
Merlino, Andy, and Patrick Howard. 2021. “shinyFeedback: Display User Feedback in Shiny Apps.” https://doi.org/10.32614/CRAN.package.shinyFeedback.
Sievert, Carson, Richard Iannone, and Joe Cheng. 2023. “Shinyvalidate: Input Validation for Shiny Apps.” https://doi.org/10.32614/CRAN.package.shinyvalidate.

  1. This link refers to {withr} – a package for running code ‘with’ temporarily modified global state.↩︎