UI
Misc
- Packages
{basecoat} - Bindings to the basecoat UI component library from R which is a pure tailwind css implementation of shadcn/ui.
- Ships with a skill that can be loaded using the {btw}
{brand_yml} - Create reports, apps, dashboards, plots and more that match your company’s brand guidelines with a single
_brand.ymlfile- For Quarto dashboards, websites, reports, etc.
- For Python shiny, seaborn, matplotlib, etc.
- For R shiny, ggplot2, plot, RMarkdown
{calcite} (Demo, Repo) - R bindings to Esri’s Calcite Design System, designed to work directly with Shiny or standalone HTML.
- Essentially a full screem map app with a slim sidebar
{dockviewR} - An interactive layout manager for Shiny apps and interactive R documents. It builds on top of dockview.
{imola} - CSS Grid and flexbox layouts for R/Shiny (e.g. allows you to put that side panel in other locations)
{linkeR} - Creates linked views in Shiny applications. When users interact with one component (like clicking a map marker), all related components (tables, charts, other maps) automatically update to show corresponding information.
Leaflet Maps ✅ Full Support Interactive maps with markers, circles, polygons DT DataTables ✅ Full Support Sortable, filterable tables Plotly Charts 🔄 Partial Requires manual event handling Custom Components 🔄 Partial Any Shiny component with click events, Requires manual event handling Base R Plots 📋 Planned Static plots with click detection Mapbox 📋 Planned Alternative mapping solution {litter} - Lit components for shiny, with a twist.
- Allows you to “observe” on any valid selector such as a .class (as opposed to shiny which can only observe on #id), this means one can have a single observe for multiple inputs.
- Create inputs that, instead of inputId, take a name argument which can be shared by multiple inputs. This makes it possible to sensibly dynamically generate inputs in R as each input does not need its corresponding observer.
- Limitations
- Only works with bslib’s Bootstrap 5.
- These inputs are not captured by Shiny’s bookmarking session
- These inputs are not captured by shinytest2
{muiMaterial} - Wraps the Material UI React components for use in shiny applications and quarto documents, including inputs, layouts, navigation, and surfaces.
{resizableSplitLayout} - Facilitate page layouts with resizable panes for page content based on ‘split.js’ ‘JavaScript’ library (https://split.js.org/)
- Has an interactive bar down the middle of the UI that allows you slide left or right which controls the width of each pane.
{tabler} - Tabler Dashboard for Shiny: A modern dashboard framework for R Shiny using the beautiful Tabler Bootstrap theme. (tabler js site, github)
{shiny_adaptive_filter} - Filters that are aware of what the other filters were doing, and updates their own values according to the other filters.
{shinyLP} - Functions that wrap HTML Bootstrap code to enable the design and layout of informative landing home pages for Shiny applications
{shiny.fluent}, {shiny.blueprint}, {shiny.semantic}, {semantic.dashboard} - Appsilon UI theming packages
- {semantic.dashboard} is a dashboard template that uses {shiny.semantic} theming
- Resources
Design Principles
- Misc
- Notes from
- Erik Kennedy thread
- Notes from
- De-Emphasize Dividing lines

- Use variatios of the primary color (source)
- Also see Visualization, General >> Colors >> Adjustments once you’ve chosen a color (hue) to create variations upon
- Navbar, Search, Icons

- One color as a theme (e.g. ocean)

- Tabs and Buttons: Active, Inactive, Hover

- Use fonts that subtly convey brand

- Content cards should be lighter than their bg (in dark mode too)

- Don’t resize icons
- Their level of detail and stroke weights are meant to work best at a certain size.
- Instead, try adding a border or container around them for some extra visual pop
- REMOVE-HIDE-LIGHTEN for cleaner designs

- Be consistent until it’s time not to be consistent
- Break consistency example: when you’re trying to catch the user’s eye
- Good imagery

- Remove Congestion
- See Designing Accessible Research with R/Shiny UI – Part 2 for an example of an iterable workflow
- Congestion is when an app shows everything, all at once and that’s stressful for the user. It doesn’t elicit the behavior we want for an engaging, learning experience.
- This stress triggers two subconscious actions – run or freeze. Both lead to cognitive load and negative perception, and ultimately, failed adoption.
- Running is expressed in bounce rate. A user will enter the app, become frustrated, and leave.
- Freezing means that a user pauses with a delayed time to understand. This can be expressed in a number of ways, but most likely a combination of longer session times with aimless user behavior.
- Minimize options, legends, etc. and move to the edges of the app (e.g. header, sides)
- Example
- Prototype
- Issues
- The tree selector consumes space on the dashboard
- Selecting via images doesn’t add a lot of value as not all tree species are easily identified by image
- The UI is too dark
- The visual accessibility feature was lost as some colors don’t work on a dark background
- Issues
- Final
-
- The legend moved to the card (see mobile for more details
- Tree card gives details about species and a summary
- Clicking upper right icon flips the card to display the bar chart.
- Switching languages and information about the application moved to the header (right side of blue nav bar)
- Clicking text in header opens pop-up with options. One for type of tree and one for type of scenario (beteen blue nav bar and map)
- All other interactive elements that were scattered on the screen are now organized into the right vertical panel on the map. Here is also the color blindness option for people with visual disabilities, as the main focus of the application is on the color ratio on the map
- The legend moved to the card (see mobile for more details
Mobile
- “languages” and “about” moved to the top
-
- Prototype
- Example
Style
- Misc
- Images and style.css files should go into the “www” folder
- Implementing a css file
- Example: Basic
main.css file
@import url('https://fonts.googleapis.com/css2?family=Poppins&display=swap'); * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Poppins', sans-serif; } body { padding: 1rem; } #map { height: 98vh !important; border-radius: 0.5rem !important; }app.R
ui <- fluidPage( tags$head(tags$link(rel = "stylesheet", type = "text/css", href = "main.css")), sidebarLayout( ... ) )
- Example: Basic
Responsiveness
Responsiveness isn’t the same as performance. Performance is about completing an operation in the minimal amount of time, while responsiveness is about meeting human needs for feedback when executing an action
Misc
Button Click Registered
(Left) Default arrow for your app
(Middle) Hand indicates to the user that the app has registered their hovering over a button
(Right) Arrow + Pie indicates to the user that the app has registered their clicking the button
(generic) entry into CSS file
html.shiny-busy .container-fluid { cursor: wait; }- Looks like this code is to produce something like the right-side image
For touch devices (i.e. without cursor), you might want to take a look at {shinycssloaders} and/or {waiter}
If the user might have to wait longer than a few seconds for the process they’ve just set in motion to complete, you should consider a progress indicator.
JS
Misc
- For when the design asks for something Shiny and bslib don’t have: a unique layout, richer interaction, or a component from a non-Bootstrap design system.
- Packages
- {shinyreact} (Intro, Examples) - The Shiny server (Python or R) contains only reactive computation, and the UI is a React client you own. shinyreact is the bridge between the two; it ships zero UI components itself.
- Cheatsheet (source)

www- Directiory for images, styles, or JS script. Same directory asapp.R- Use a javascript file + library
Examples
Example Stylish Pop-Up Notification
// JS script; basic toastify notification Shiny.addCustomMessageHandler('notify', function(msg){ Toastify({ text: msg }).showToast(); });- Keep in
www - toastify.js is a library for some snazzy notifications you can use that pop-up in your browser when a event (e.g. button pushed) has happened.
addCustomMessageHandleris a js function that receives the message from the server- “notify” is the identifer that it listens for
Toastify()takes a JSON formatted list of argsshowToastmethod sends snazzy notification to the user’s browser.
ui <- fluidPage( tags$head( # toastify css dependency tags$link( rel = "stylesheet", type = "text/css", href = "https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css" ), # our script tags$script( type = "text/javascript", src = "script.js" ) ), h1("Notifications"), textInput("text", "Text"), actionButton("show", "Show"), # toastify js dependency tags$script( type = "text/javascript", src = "https://cdn.jsdelivr.net/npm/toastify-js" ) ) server <- function(input, output, session) { observeEvent(input$show, { session$sendCustomMessage( "notify", input$text }) } shinyApp(ui, server)- Button is pressed in ui, sends “message” (text, data.frame, etc.) to server, server sends “message” to js script in ui which triggers a notification in the browser.
- UI
tags$headincludes:tags$linklinks to the toastify CSS stylesheettags$scriptpoints to the path of the js script and provides the identifier (“type”), “text/javascript”
tags$scriptlinks to the toastify js dependency and also has the identifier, “text/javascript”
- Server
ObserveEventlistens for ui input “show” button to be pressedsendCustomMessageis a js function to send a message from the server to the browser- identifier (“type”) = “notify” says which custom handler to use in the js script
- Keep in
Example: DT table and Leaflet (sort of) sync (source)
gomap.js// When locator icon in datatable is clicked, go to that spot on the map $(document).on("click", ".go-map", function(e) { e.preventDefault(); $el = $(this); var lat = $el.data("lat"); var long = $el.data("long"); var zip = $el.data("zip"); // this one $($("#nav a")[0]).tab("show"); // this one Shiny.onInputChange("goto", { lat: lat, lng: long, zip: zip, // and this one nonce: Math.random() }); });- This example had a few of the lines deleted (indicated in the comments)
words
# ui -------------- navbarPage("Superzip", id="nav", tabPanel("Interactive map", div(class="outer", tags$head( # Include our custom CSS includeCSS("styles.css"), includeScript("gomap.js") ), ..., ... ) ) ) # server ---------------- observe({ if (is.null(input$goto)) return() isolate({ map <- leafletProxy("map") map %>% clearPopups() dist <- 0.5 zip <- input$goto$zip lat <- input$goto$lat lng <- input$goto$lng showZipcodePopup(zip, lat, lng) map %>% fitBounds(lng - dist, lat - dist, lng + dist, lat + dist) }) }) output$ziptable <- DT::renderDataTable({ df <- cleantable %>% filter( Score >= input$minScore, Score <= input$maxScore, is.null(input$states) | State %in% input$states, is.null(input$cities) | City %in% input$cities, is.null(input$zipcodes) | Zipcode %in% input$zipcodes ) %>% mutate(Action = paste('<a class="go-map" href="" data-lat="', Lat, '" data-long="', Long, '" data-zip="', Zipcode, '"><i class="fa fa-crosshairs"></i></a>', sep="")) action <- DT::dataTableAjax(session, df, outputId = "ziptable") DT::datatable(df, options = list(ajax = list(url = action)), escape = FALSE) }) }- Also see this example
Example: Download code from a LLM chat window (source, article)
# Attach a JavaScript snippet to handle the download. # When the download button is clicked, the script extracts the text inside # the chat's <shiny-chat-messages> element, creates a Blob from it, and triggers a download. ui <- tagList( ui, tags$script(HTML(" document.addEventListener('DOMContentLoaded', function() { document.getElementById('downloadChat').addEventListener('click', function() { // Find the chat container by the id you provided to chat_ui() var chatContainer = document.getElementById('chat'); if (!chatContainer) return; // The chat messages are rendered inside the <shiny-chat-messages> element var chatMessages = chatContainer.querySelector('shiny-chat-messages'); if (!chatMessages) return; // Get the visible text (this will strip extra formatting) var text = chatMessages.innerText; // Create a blob and a temporary anchor to trigger download var blob = new Blob([text], { type: 'text/plain' }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = 'chat.txt'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }); }); ")) )

.png)
.png)
.png)

.png)
.png)
.png)
.png)