Web design best practices

Wireframing

Build me a house

By https://www.imaginationshaper.com/design-category/house-map/

What?


Wireframing is the action of drawing an app design on the paper or with a software:

  • Shows layout.
  • Shows user interactions.
  • Optional: Colors/typography.

Different fidelity levels

https://careerfoundry.com/en/blog/ux-design/what-is-a-wireframe-guide/

Why?


  • Have a physical representation of the goal to reach (for you and the end users).
  • Quickly realise potential caveats: fail early rather than too late.
  • End users will help you to get the right map.

How: 😎 β€œA l’ancienne”?


πŸ–Š pen, πŸ“œ paper, 🧠 brain

How: πŸ’» With a software/app?


  • Excalidraw (free).
  • Figma (plans).
  • Balsamiq (pay).

πŸ₯Ό Your turn: open {shinyMons2}
05:00

  1. Open the Posit cloud space shared with you and click on the shinyMons2 assignment.


  1. Open ./app.R, run the app and discuss.

About {shinyMons2}


This is an uglyfied pokemon app1 suffering from few UX/UI issues:

  • Wrong color choice and typography.
  • Confusing layout.
  • Confusing error messages.
  • …


You got it, this app s*cks πŸ™Š

{shinyMons2} structure 1


This app is composed of 7 modules (you can check in ./R).


πŸ₯Ό Your turn: {shinyMons2} data
05:00


poke_data is a nested list containing all relevant data for 151 pokemons.

What if I don’t know anything about Pokemons? πŸ€”


What would we expect to know from a Pokemon?

  • What’s its name? Type? Any detail counts!
  • Is it powerful?
  • Where to find it?
  • How it looks like (sprites are images).
  • Whether it is an evolution from a child pokemon.
  • …

{shinyMons2} redesign steps


From there, we’ll cover the redesign step by step:

  • Do the wireframing from poke_data.
  • Create the corresponding layout with bslib.
  • You’ll fine tune the bslib theme (colors and typography).
  • …

πŸ₯Ό Your turn: wireframing
30:00


  1. Open the shinyMons2 project in the Posit Cloud space.
  2. Run pkgload::load_all().
  3. Considering the available poke_data data, with the πŸ–Š pen, πŸ“œ paper and 🧠 brain approach discussed earlier, propose a new design for {shinyMons2}.

Hint

Only focus on the UI, not on the server part. Ask yourself: what would people want to know about their favorite Pokemon?

Time to check what you all did

15:00

Layout

Grids

Why?

Symmetry

Symmetry

Symmetry

Symmetry

https://www.siteinspire.com/

Symmetry Broken!

https://www.wix.com/blog/2022/10/asymmetrical-balance/

Asymmetry

Asymmetry

Asymmetry

Asymmetry

https://www.siteinspire.com/

Rule of Thirds

Rule of Thirds

Triad Composition

Triad Composition

Triad Composition

Swiss Design

Shapes

Saccade

Optical Alignment

Optical Alignment

Optical Alignment

Optical Alignment

Diagonals

Diagonals

dribbble.com

Scale


Anchoring


The architecture of space

Introduction to bslib layouts


3 main layouts 1:

  • Columns (grid): layout_columns().
  • Sidebar: layout_sidebar().
  • Multi pages: page_navbar().

Column-based layout


layout_column_wrap() is a simplified interface to CSS grid.

layout_column_wrap(
  width = 1/2, height = 300,
  card1, card2, card3
)

3 cards of 200px require at least a viewport of 600px to display side by side.

layout_column_wrap(
  width = "200px", height = 300,
  card1, card2, card3
)
layout_column_wrap(
  width = "200px", height = 300,
  fixed_width = TRUE,
  card1, card2, card3
)

πŸ₯Ό Your turn: nested layouts
03:00


With your fresh bslib knowledge, try to reproduce the following layout.

layout_column_wrap(
  width = 1/2,
  height = 300,
  card1,
  layout_column_wrap(
    width = 1,
    heights_equal = "row",
    card2, card3
  )
)

πŸ₯Ό Your turn: nested sidebars
03:00


Taking a basic layout_sidebar and looking at ?bslib::sidebar, find a way to create another sidebar on the right side.

Hint

Add fillable = TRUE and class = "p-0" to the main layout_sidebar function.

layout_sidebar(
  sidebar = sidebar("Left sidebar", width = "33%", class = "bg-light"),
  layout_sidebar(
    sidebar = sidebar("Right sidebar", position = "right"),
    "Main contents",
    fillable = TRUE, # fillable, taking all vertical space
    class = "p-0" # zero padding
  )
)

Dashboard layout


page_sidebar()

My dashboard

Main content area
page_sidebar(
  title = "My dashboard",
  sidebar = "Sidebar",
  "Main content area"
)

Hint

Best practice:

  • Sidebar: for global inputs.
  • Main content: for outputs (local inputs).

πŸ₯Ό Your turn: dashboard layouts
03:00


Take the previous dashboard example and add an input and ouput of your choice.

library(shiny)
library(bslib)

ui <- page_sidebar(
  title = "My dashboard",
  sidebar = tagList(
    sliderInput(
      "obs", 
      "Number of observations:",
      min = 0, 
      max = 1000, 
      value = 500
    )
  ),
  plotOutput("distPlot")
)

shinyApp(ui, function(input, output) {
  output$distPlot <- renderPlot({
    hist(rnorm(input$obs))
  })
})

Multi pages layout


For more complex dashboards:

  • Replace page_sidebar by page_navbar.
  • Use nav_panel for each body tab.


page_navbar(
  title = "Multi pages",
  nav_panel("Tab 1", "Tab 1 content"),
  nav_panel("Tab 2", "Tab 2 content")
)

πŸ₯Ό Your turn: tab-specific sidebar
03:00


Until now, we only had shared sidebars.

Take the previous page_navbar template and try to add a dedicated sidebar for each nav_panel.

Hint

Don’t forget that each nav_panel is a container which can host its own layout.

library(shiny)
library(bslib)

ui <- page_navbar(
  title = "Multiple sidebars",
  nav_panel(
    "Tab 1", 
    layout_sidebar(
      sidebar = sidebar("Sidebar tab 1"),
      "Tab 1 content"
    )
  ),
  nav_panel(
    "Tab 2", 
    layout_sidebar(
      sidebar = sidebar("Sidebar tab 2"),
      "Tab 2 content"
    )
  )
)

shinyApp(ui, function(input, output) {
  output$distPlot <- renderPlot({
    hist(rnorm(input$obs))
  })
})

bslib components overview


Let’s review the most outstanding bslib components:

  1. Cards.
  2. Tabs.
  3. Accordions.
  4. Value boxes.

Hint

Most (if not all) bslib components have a class parameter to style them 🎨. They are also browsable, meaning typing card("My card") in the R console will show a card in the viewer panel 🎁

Cards


A card is a rectangular container for organizing related content, through a more appealing design.


Card header: card_header()
Card body content
card(
  full_screen = TRUE,
  card_header("Card header"),
  # If not passed explicitly, elements are wrapped inside
  # card_body ...
  layout_sidebar(
    sidebar = sidebar("Card sidebar"),
    "Card body content",
    fillable = TRUE
  ),
  card_footer("Card footer")
)

Hint

Pass full_screen = TRUE to make the card maximizable.

πŸ₯Ό Your turn: multiple cards
03:00


Combine layout_columns_wrap() and card() to create a grid composed of 3 columns and 2 rows. Then play with the screen width and conclude.

Hint

You can create those 6 cards programmatically with lapply() and pass them with !!! inside layout_columns_wrap() (introduced in shiny 1.7.0).

cards <- lapply(1:6, function(i) {
  card(sprintf("Card %s", i))
})

layout_column_wrap(
  width = 1/3,
  height = 300,
  !!!cards
)

Tabs


Tabs allow to organize related content over multiple pages 1.

They can be part of navbars (navigation), cards or standalone.


navset_tab(
  nav_panel(title = "One", p("First tab content.")),
  nav_panel(title = "Two", p("Second tab content."))
)

πŸ₯Ό Your turn: tabs within cards
03:00


Have a look at ?navset_card_tab to include tabs within a card. Try out with navset_card_pill.

Hint

All those functions have a server part to programmatically add/remove/select tabs. When doing so, pass an id to the parent container like navset_card_tab(id = ..., ...).

panels <- list(
  nav_panel(title = "One", p("First tab content.")),
  nav_panel(title = "Two", p("Second tab content."))
)

# Tabs
navset_card_tab(!!!panels)

# Pills
navset_card_pill(!!!panels)

Accordions


Accordions are made of collapsible elements to organize content in the same container.


accordion()
Content 1
Content 2
Content 3
panels <- lapply(1:3, function(i) {
  accordion_panel(
    sprintf("Item %s", i),
    sprintf("Content %s", i)
  )
})
accordion(
  # pass FALSE to have only 1 item open at a time
  multiple = FALSE,
  !!!panels
)

Hint

You can see an accordion as a group of collapsible cards, to save vertical space.

πŸ₯Ό Your turn: accordion and sidebars
03:00


Combine layout_sidebar() to accordion to better organize input parameters within the following example.


An imaginary plot
layout_sidebar(
  sidebar = sidebar(
    textInput("xlab", "X label", "x vals"),
    numericInput(
      "obs", 
      "Number of observations:",
      min = 0, 
      max = 1000, 
      value = 500
    ),
    textInput("ylab", "Y label", "y yals"),
  ),
  "An imaginary plot"
)
layout_sidebar(
  sidebar = sidebar(
    accordion(
      accordion_panel(
        icon = bs_icon("file-bar-graph"),
        "Value",
        numericInput(
          "obs", 
          "Number of observations:",
          min = 0, 
          max = 1000, 
          value = 500
        )
      ),
      accordion_panel(
        "Axis",
        icon = bs_icon("body-text"),
        textInput("xlab", "X label", "vals"),
        textInput("ylab", "Y label", "yals")   
      )
    )
  ),
  "An imaginary plot"
)

Value boxes


Value boxes are tiny card containers dedicated to highlight a specific metric.


value_box()

Title

value

description
# Value box 1
value_box(
  title = "Title",
  value = "value",
  showcase = bs_icon("graph-up"),
  showcase_layout = showcase_left_center(),
  "description",
  theme = "success"
)
value_box()

Title

value

description
# Value box 1
value_box(
  title = "Title",
  value = "value",
  showcase = bs_icon("pie-chart"),
  showcase_layout = showcase_top_right(),
  "description"
)
value_box()
A sparkline plot with a randomly-generated timeseries. The timeseries starts high and ends low, with lots of variation.

Title

value

# Value box 1
value_box(
  title = "Title",
  value = "value",
  showcase = sparkline_plot(),
  showcase_layout = showcase_bottom()
)

Hint

Value box can host plots but more treatment is required to have a good rendering when the card is minimized. Please see here.

πŸ₯Ό Your turn: a value box themer
03:00


Spend a few minutes to play with the below value box generator.

πŸ₯Ό Your turn: {shinyMons2} layout
45:00


Considering the wireframing obtained in the previous part, create a new bslib layout which illustrates the most your original idea.


  1. Switch to the fresh-start git branch, which has the 7 modules but without any existing UI nor server.
  2. mod_poke_select does not need to be changed.
  3. For each module in the R folder, you’ll see WORKSHOP TO DO. Looking at poke_data, complete each module with your layout.
  4. You can run the app by sourcing the ./app.R script …

Hint

Module files are like ./R/mod_poke_*.R. You may not need all modules depending on the wireframe. To go faster, you may assign 1 module per person in your team.

Time to check what you all did πŸ˜ƒ

15:00

Color and Typography

Monochrome

Monotone color scheme that uses variations of shades of a single color, such as red, dark red, and pink. Clean, elegant and balanced.

Monochrome

Analogous

Related color scheme that uses shades next to each other on the color wheel such as red and violet. Richer, more variety than monochrome

Analogous

Complementary

Uses colors that are opposite in the color wheel, such as orange and blue. Contrast cool against warm colors.

Complementary

Triadic

Uses three colors equally spaced around the color wheel such as red, blue, and yellow. Vibrant, rich, harmonious

Triadic

A COLOR IS ONLY A COLOR IN RELATION TO ANOTHER COLOR

Edward H. Adelson

A COLOR IS ONLY A COLOR IN RELATION TO ANOTHER COLOR

Edward H. Adelson

Contrast1

Adobe Color Wheel

Color in Code

RGBA

rgb(x,x,x);

rgba(x,x,x,y)

Where x is 0 (black) to 255 (white) Where y is 0.0-1.0

If all numbers are the same you’ll have some sort of gray

HEXCIDECIMAL

Values range from 0-9 and A-F where #00000 is the lowest value (black) and #FFFFFF the highest (white)

Easy to copy and paste. Understand? Not so much.

HSL(A)

hsl(x,y,y)

hsla(x,y,y,z)

Where x is 0 (black) to 360 (white) Where y is a percentage from 0 to 100 Where z is a number from 0.0-1.0

Human readable, awesome color mode

NAMED COLORS

Good for demos, not much else

COLOR VARIABLES

Apply colors with CSS

<h1 class=β€œred_header”>Hello!<h1>
<h1 class=β€œred_header”>Howdy!<h1>


We can easily style multiple elements the same by giving them the same class

COLOR VARIABLES

(Finally!!)

COLOR VARIABLES

(Finally!!)

HOW TO PICK A PALETTE

HOW TO PICK A PALETTE

HOW TO PICK A PALETTE

HOW TO PICK A PALETTE

Exercise:
Pick a palette!

Introduction to bslib themes

bslib provides theming capabilities for Shiny 1.

Create a new theme 🎨


Hello bs_theme() 1

  • version: Bootstrap version (3, 4, 5, …).
  • bootswatch: predefined themes.
  • fg: foreground color.
  • bg: background color.
  • primary, secondary, …: palette 🎨
  • …: other theme variables.
bs_theme(
  version = version_default(),
  bootswatch = NULL,
  ...,
  bg = NULL,
  fg = NULL,
  primary = NULL,
  secondary = NULL,
  success = NULL,
  info = NULL,
  warning = NULL,
  danger = NULL
)

πŸ₯Ό Your turn: create a palette
10:00


With what you learnt about colors and bslib, create the palette of your dreams and preview it. You can browse to https://color.adobe.com/create/color-wheel to create the color palette.

Hint

To run the theme next to your favorite app, you may call run_with_themer(shinyApp(ui, server)).

bs_theme(
  version = 5L,
  primary = "#A5DCFA",
  secondary = NULL,
  success = "#FAF670",
  info = NULL,
  warning = NULL,
  danger = "#FA7D85"
) |> bs_theme_preview()

Typography

  • Serif Font

  • Sans Serif Font

  • Script Font

  • Handwritten Font

  • Display Font

  • Novelty Font

  • Monospace

Type Foundaries

Google Fonts

Font Squirrel

fonts.com

Hoefler&Co

Pairing Rules

  • One Display, one sans-serif
    One serif, one sans-serif
  • Try for 2
    never more than 3
  • Pick dissimilar types

+

βœ“

Typographic color

  • Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

  • Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

  • Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

  • Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

  • Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum./li>

  • Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

  • Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Typographic color

  • Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

  • Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

  • Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

  • Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

  • Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum./li>

  • Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

  • Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Terminology

Terminology

Kerning Gone Wrong

Terminology

Fonts with bslib


bs_theme accepts specific font parameters:

  • base_font Default font for text.
  • code_font used for code.
  • heading_font for heading elements.
  • font_scale Font size multiplier (>0).

Helpers such as font_google() allow to serve font files locally 1.

πŸ₯Ό Your turn: adding fonts
05:00


Taking the previous theme, add a font to it. Don’t forget to leverage bslib font helpers such as font_link() and font_google() …

bs_theme(
  version = 5L,
  primary = "#A5DCFA",
  secondary = NULL,
  success = "#FAF670",
  info = NULL,
  warning = NULL,
  danger = "#FA7D85",
  base_font = font_link(
    "Press Start 2P",
    href = "https://fonts.googleapis.com/css?family=Press+Start+2P"
  ),#font_google("Azeret Mono"),
  code_font = font_google("Fira Code"),
  heading_font = font_google("Inconsolata"),
  font_scale = 1
) |> bs_theme_preview()

πŸ₯Ό Your turn: {shinyMons2} colors
20:00


Considering the layout from the previous part, create a new bslib palette to make {shinyMons2} shining.


To help validate your theme:

  • Open the developer tools (Chrome).
  • Enable CSS Overview: Options -> More Tools -> CSS Overview.
  • Click on Capture overview.
  • Look at Contrast issues and target AA (even AAA).

Hint

bslib::bs_theme() will be your best friend. You may have a look here to discover more bslib helper functions.

Time to check what you all did πŸ˜ƒ

15:00

User feedback

Chatty apps


Users should not guess what the app is doing:

  • If something goes wrong, users should know.
  • If something was successful, users should also know.

Respect people’s perseption


  • Not everyone can properly see colors.
  • Prioritise components that are accessibility proven (can be digested by screen readers).

How to handle feedback?


We’ll explore a few topics:

  1. Validate user input/outputs?
  2. Handle notifications?
  3. Handle loading/waiting time?
  4. Convey extra information?

(1) Inputs validation

Input validation best practices


πŸ‘ Do’s πŸ‘Ž Don’ts
Proper color Only rely on color
Add icon Print raw error message
Meaningful text Display error before the element
Extra tooltip (on press) Use toasts (disable auto dismiss)
Provide example of valid statement Don’t highlight correct fields
An error summary (accordion)

Input validation with a single field


E-mail
+417562356
Error 23
E-mail
+417562356
Error: a valid email should be like name.surname@domain.com
  • Added icon next to field label (avoid error).
  • Added icon to error message.
  • Provide valid example.

Example from my health insurance πŸ˜ƒ

Inputs validation with {shinyvalidate}


library(shiny)
library(shinyvalidate)

ui <- fluidPage(
  textInput(
    "email", 
    tagList(icon("envelope"), "Email"),
    placeholder = "example: name.surname@domain.com"
  )
)

server <- function(input, output, session) {
  iv <- InputValidator$new()
  iv$add_rule("email", sv_required())
  iv$add_rule(
    "email",
    sv_email(
      message = tagList(
        icon("exclamation-circle"),
        "Error: a valid email should be like:",
        span(class = "underline", "name.surname@domain.com")
      )
    )
  )
  iv$enable()
}
1
Add placeholder as example.
2
Initialise input validator R6 instance.
3
Prerequisites + add custom error message with icon.
4
Activate the validation.

Output validation 1


ui <- fluidPage(
  textInput("name", "Name"),
  textInput("surname", "Surname"),
  textOutput("res")
)

server <- function(input, output) {
  output$res <- renderText({
    if (nchar(input$name) == 0 || nchar(input$surname) == 0) {
      validate("Fill all the inputs to see the result")
    } else {
      paste0(input$name, input$surname)
    }
  })
}
  • if statement: condition for which the message will be shown.
  • Wrap end-user message within shiny::validate.

Going further


  • {shiny.emptystate}: https://appsilon.github.io/shiny.emptystate/

(2) Notifications

Different levels of urgency


  • High attention: errors, user confirmation.
  • Medium attention: warnings.
  • Lower attention: success.

Notifications best practices


High attention Medium attention Lower attention
Blocking, no auto close No auto close Auto close
Modal with overlay Toast Toast
Meaningful icon Meaningful icon Meaningful icon

Example for success


ui <- fluidPage(
  actionButton(
    "send", 
    "Send mail", 
    icon = icon("paper-plane")
  )
)

server <- function(input, output) {
  observeEvent(input$send, {
    showNotification(
      ui = tagList(
        icon("check"),
        "Message successfully sent!"
      )
    )
  })
}
  • We used shiny::showNotification.
  • Auto close is ok.
  • Displayed in the corner (not high priority).

Example for errors


ui <- fluidPage(
  actionButton(
    "run", 
    "Run simulation", 
    icon = icon("calculator")
  )
)

server <- function(input, output, session) {
  observeEvent(input$run, {
    Sys.sleep(2)
    showModal(
      modalDialog(
        title = tagList(
          icon("xmark"),
          "Error: computation failed."
        ),
        p("Error description (depends on user level...)"),
        br(),
        tagList(
          icon("exclamation-circle"),
          "Parameter a is ..."
        )
      )
    )
  })
}
  • Modal with overlay (focus on error).
  • Correct icons (no need for colors).
  • Brief description.
  • More details (depends on the user level).

What if we don’t know the result of a given task?


  • The same task may succeed or fail based on different parameters.
  • Need to capture exceptions.
  • tryCatch is your friend.

Capture exceptions with {shiny}


observeEvent(input$run, {
  res <- sample(c(FALSE, TRUE), 1)
  tryCatch({
    if (res) {
      showNotification(
        ui = tagList(
          icon("check"),
          "Computation successful."
        )
      )
    } else {
        stop("<Error message>")
    }
  }, error = function(e) {
    showModal(
      modalDialog(
        title = tagList(
          icon("xmark"),
          "Error: computation failed."
        ),
        sprintf("%s", e$message)
      )
    )
  })
})
  • Capture error within tryCatch.
  • Return either notification or modal.
  • Note: we could also capture warnings.

πŸ₯Ό Your turn: errors in {shinyMons2}
10:00


Taking our {shinyMons2} app, the select_pokemon() function has unpredictible behavior and may crash. Leverage tryCatch so that the app can handle success (notification) or errors (modal).

select_pokemon <- function(selected) {
  # We make the function slow on purpose.

  # WORKSHOP TODO
  # Find a way to warn the user about this waiting time ...
  Sys.sleep(5)

  # We simulate an imaginary failing API connection
  # This randomly fails so the function result
  # isn't predictable...and the app crashes without
  # notifying the user of what happened...

  # WORKSHOP TODO
  # Find a way to make this function elegantly failing
  # and warn the end user ...
  res <- sample(c(FALSE, TRUE), 1)
  if (!res) {
    stop("Could not connect to the Pokemon API ...")
  } else {
    poke_data[[selected]]
  }
}
# Server code
selected_pokemon <- eventReactive(input$selected, {
  select_pokemon(input$selected)
})
select_pokemon <- function(selected, session) {
  # We make the function slow on purpose.

  # WORKSHOP TODO
  # Find a way to warn the user about this waiting time ...
  Sys.sleep(5)
  
  res <- sample(c(FALSE, TRUE), 1)
  tryCatch({
    if (res) {
      showNotification(
        ui = tagList(
          icon("check", class = "fa-2xl"),
          "Successfully connected to the Pokemon API ..."
        )
      )
      poke_data[[selected]]
    } else {
        stop("Could not connect to the Pokemon API ...")
    }
  }, error = function(e) {
    showModal(
      modalDialog(
        title = tagList(
          icon("xmark", class = "fa-2xl"),
          "Error..."
        ),
        sprintf("%s", e$message),
        br(),
        a(
          onclick = "location.reload()", 
          "Reload app?", 
          href = "#", 
          style = "color: white"
        )
      )
    )
  })
}

Going further


  • Safer render_* and observe_* with {elvis}: https://github.com/ThinkR-open/elvis.
  • Error summary: https://service-manual.nhs.uk/design-system/components/error-summary

(3) Loading/waiting time

Progress best practices


  • Initial loading: full screen waiter.
  • Quantifiable subtasks: shiny::withProgess.
  • For blocking tasks: full screen {waiter}.
  • Button spinner: related to local task, async (can run in the background).

Button spinner 1


When we don’t necessarily know the waiting time …


ui <- fluidPage(
  actionButton(
    "run", 
    "Run simulation", 
    icon = icon("calculator")
  )
)

server <- function(input, output, session) {
  observeEvent(input$run, {
    session$sendCustomMessage("add-spinner", "run")
    Sys.sleep(2) # Fake calculation time
    session$sendCustomMessage("remove-spinner", "run")
  })
}
  • Indicate user when task is running.
  • Notify when task is over (not shown for space reasons).
$(function() {
  Shiny.addCustomMessageHandler('add-spinner', function(m) {
    $('#run i')
      .removeClass('fa-calculator')
      .addClass('fa-spinner fa-spin');
  });
  Shiny.addCustomMessageHandler('remove-spinner', function(m) {
    $('#run i')
      .removeClass('fa-spinner fa-spin')
      .addClass('fa-calculator')
  });
}); 

Preloader


When the app is loading, you can display a spinner with Waiter.

ui <- fluidPage(
  useWaiter(), # include dependencies
  h1(icon("world"), "My super app")
)
server <- function(input, output, session) {
  # create a waiter
  state <- reactiveValues(loaded = FALSE)
  w <- Waiter$new()
  
  # on button click
  observeEvent(req(!state$loaded), {
    w$show()
    tryCatch({
      connect_db(w)
      w$update(
        html = tagList(
          icon("circle-check", class = "fa-2xl"),
          h3("Success: connected")
        )
      )
      Sys.sleep(1)
      state$loaded <- TRUE
    }, error = function(e) {
      w$update(
        html = tagList(
          icon("circle-xmark", class = "fa-2xl"),
          h3(sprintf("Error: %s", e$message)),
          a(
            onclick = "location.reload()", 
            "Reload app?", 
            href = "#", 
            style = "color: white"
          )
        )
      )
      Sys.sleep(3)
    })
  })
  
  observeEvent(req(state$loaded), {
    w$hide()
  })
}
connect_db <- function(w) {
  w$update(
    html = tagList(
      spin_flower(), 
      h3("Connecting to database ...")
    )
  )
  Sys.sleep(3)
  res <- sample(c(FALSE, TRUE), 1)
  if (!res) stop("could not connect to DB") 
}

πŸ₯Ό Your turn: a preloader for {shinyMons2}
20:00


Taking our {shinyMons2} app, the select_pokemon() function is still slow. Modify the previous function so that it leverage waiter to display a loading screen at start and each time the selected pokemon changes.


  1. Load the waiter dependencies in the app_ui.R with waiter::useWaiter().
  2. Initialize the waiter in mod_poke_select_server with Waiter$new().
  3. Replace eventReactive by observEvent and store the selected pokemon is a reactiveVal.
  4. Add a w parameter to select_pokemon to pass the waiter.
  5. Within select_pokemon, replace showNotification and showModal by waiter$show(), waiter$update().
# utils.R
select_pokemon <- function(selected, w) {
  w$show()
  w$update(
    html = tagList(
      spin_flower(),
      h3(sprintf("Getting %s data ...", selected))
    )
  )
  Sys.sleep(5)
  res <- sample(c(FALSE, TRUE), 1)
  tryCatch({
    if (res) {
      w$show()
      w$update(
        html = tagList(
          icon("circle-check", class = "fa-2xl"),
          h3("Success ...")
        )
      )
      Sys.sleep(1)
      w$hide()
      poke_data[[selected]]
    } else {
      stop("Could not connect to the Pokemon API ...")
    }
  }, error = function(e) {
    w$update(
      html = tagList(
        icon("circle-xmark", class = "fa-2xl"),
        h3(sprintf("Error: %s", e$message)),
        a(
          onclick = "location.reload()",
          "Reload app?",
          href = "#",
          style = "color: white"
        )
      )
    )
    Sys.sleep(5)
    w$hide()
    NULL
  })
}
# mod_poke_select_server

# Init waiter
w <- Waiter$new()
# Init pokemon data
selected_pokemon <- reactiveVal(NULL)

# Replace by observeEvent
observeEvent(input$selected, {
  # Get and store pokemon data
  selected_pokemon(select_pokemon(input$selected, w))
})
# UI code: add this to app_ui.R to load the waiter assets
waiter::useWaiter()

Local progress


For async tasks, we indicate local computation with waiter::Waitress …


ui <- fluidPage(
  useWaitress(),
  fluidRow(
    column(
      width = 6,
      h3("Long task"),
      actionButton("go", "Run long task"),
      plotOutput("long_task") 
    ),
    column(
      width = 6,
      wellPanel(
        h3("Other task"),
        checkboxGroupInput(
          "variable", 
          "Variables to show:",
          c(
            "Cylinders" = "cyl",
            "Transmission" = "am",
            "Gears" = "gear"
          )
        ),
        tableOutput("quick_task")
      )
    )
  )
)
server <- function(input, output, session) {
  vals <- reactiveValues(job = list(el = NULL, res = NULL))
  
  # create a waitress
  waitress <- Waitress$new(
    "#go", 
    theme = "overlay-opacity", 
    infinite = TRUE
  )
  
  observeEvent(input$go, {
    vals$job$el <- NULL
    vals$job$res <- NULL
    
    waitress$start()
    vals$job$el <- callr::r_bg(
      func = bg_task,
      supervise = TRUE
    )
  })
  
  observe({
    invalidateLater(1000)
    req(vals$job$el)
    if (!vals$job$el$is_alive()) {
      vals$job$res <- vals$job$el$get_result()
      waitress$close()
    }
  })
  
  # Render the background process message to the UI
  output$long_task <- renderPlot({
    if (!is.null(vals$job$res)) {
      plot(vals$job$res)
    } 
  })
  
  output$quick_task <- renderTable({
    mtcars[, c("mpg", input$variable), drop = FALSE]
  }, rownames = TRUE)
}
bg_task <- function() {
  Sys.sleep(5)
  return(hist(rnorm(1000)))
}

(4) Convey extra information

Tooltips vs Popovers


tooltips popovers
What? Bubble appearing on hover over an element Card with title and body on press/hover
How to use? Display help, supplement input validation, avoid long text Print extra info, links, … without cluttering/overloading the main UI

Revisit input validation with tooltips


Instead of using placeholder field, we can show a tooltip.


tooltip()
textInput(
  "email",
  tagList(
    bs_icon("envelope"), 
    "Email", 
    tooltip(
      placement = "top",
      bs_icon("info-circle"),
      "Example: name.surname@domain.com"
    )
  ),
  height = 300
)

Provide extra information with popovers


popover()

A value box

130

More info here

value_box(
  title = "A value box",
  value = 130,
  theme_color = "light",
  showcase = bs_icon("arrow-up"),
  p(
    "More info here",
    popover(
      bs_icon("info-circle"),
      title = "Breaking news!",
      placement = "bottom",
      options = list(html = TRUE),
      tagList(
        tags$b("These few lines of text will change your life forever."),
        tags$img(src = "https://placehold.jp/150x150.png")
      )
    )
  )
)

πŸ₯Ό Your turn: tooltips and popovers for {shinyMons2}
10:00


Taking our {shinyMons2} app, leverage tooltip() and/or popovers() to better convey additional information and help messages.

Going further

πŸŽ‰ πŸŽ‰ πŸŽ‰ Congrats


First of all congrats for reaching this part!

πŸ‘€ Where to go now?


As you may imagine, we only scratched the surface of web design. We’d like to advise you to:

  • Watch UX videos such as from the NNgroup (Nielsen Norman Group).
  • Investigate human psychology (emotional design): Laws of UX.
  • Simply talk to people.

πŸ™ Thanks


Thanks for attenting this workshop.


We hope it will give you some inspiration on your upcoming or ongoing projects.


Please go to pos.it/conf-workshop-survey. Your feedback is crucial! Data from the survey informs curriculum and format decisions for future conf workshops, and we really appreciate you taking the time to provide it.