EDS 430: Part 4.2

Styling apps with CSS & Sass


Styling apps with CSS & Sass

{bslib} & {fresh} are great ways to get started on your app customization journeys, but knowing some CSS & Sass can help you really fine-tune the appearance of your apps

Using Sass & CSS to style Shiny apps & dashboards


You can write your own stylesheets using CSS and Sass to fully customize your apps, from background colors, to font styles, to size and shape of elements, and more. Unlike {bslib} and {fresh}, these are languages, meaning they can be a bit more challenging to get started with (but the payoff it big!).

Pros:

applies to any web page (not just shiny apps / dashboards)

allows you to customize pretty much any aspect of your app

can be combined with themes generated using {bslib} or {fresh} to fine-tune your app’s styling

Cons:

a steeper learning curve / generally more complex than packages like {bslib} and {fresh}


We’ll review a little bit about CSS & Sass, then practice writing and applying custom styling to apps and dashboards.

Resources for a deeper dive


We’ll be doing a rather high-level and quick overview of Sass & CSS today, though I encourage you to check out the Customizing Quarto Websites workshop materials, which takes a much deeper dive (the information in that workshop is largely applicable here).

W3Schools is my favorite online resource for all-things CSS – in addition to really digestible descriptions and examples, they also offer interactive tutorials to get your hands on updating (and breaking) code (in a safe space, of course).


The W3Schools logo, which is a green 'W' with a green '3' to the right.

What even is CSS? Sass?


The CSS 3 logo.

CSS (Cascading Style Sheets) is a programming language that allows you to control how HTML elements look (e.g. colors, font styles, etc.) on a webpage.


The Sass logo.

Sass (Syntactically Awesome Stylesheets) is a CSS extension language and CSS preprocessor – meaning Sass needs to be converted (aka compiled) to CSS before it can be interpreted by your web browser.

CSS is a rule-based language


CSS is a rule-based language, meaning that it allows you to define groups of styles that should be applied to particular elements or groups of elements on a web page. For example, “I want all level one headings (<h1> or tags$h1(), as written in Shiny) in my app to be green with a bit of extra space between each letter” could be coded as:


An example of a CSS rule that modifies all h1 elements so that the text is green and that there's a bit more space between each letter.

Selectors select the HTML element(s) you want to style (e.g. level one headings, <h1>)

Declarations sit inside curly brackets, {}, and are made up of property and value pairs. Each pair specifies the property of the HTML element(s) you’re selecting (e.g. the color property of the element <h1>), and a value you’d like to assign to that property (e.g. green)

A property and it’s corresponding value are separated by a colon, :. Declarations end with a semicolon, ;

There are a variety of CSS selectors – check out some of the basics that will take you far in styling your apps, starting on this slide of the Customizing Quarto Websites workshop.

3 ways to add CSS styling to your apps


Add styling directly to tags. It’s best not to use a lot of these! It’s easy to lose track of your “in-line” styling in large projects, you can’t reuse rules easily, it’s hard to keep styling consistent, and it’s difficult to implement large stylistic changes.

#..............................setup.............................
library(shiny)

#...............................ui...............................
ui <- fluidPage(
  
  # text color = purple
  tags$h1("My app title",
          style = "color: #711EBA;"),
  
  # text color = blue; increase space between letters
  tags$h3("Section 1",
          style = "color: #1E4DBA; letter-spacing: 4px;"),
  
  # no styling
  tags$h3("Section 2"),
  
  # increase border thickness and color green; round corners 
  tags$button("This is a button",
              style = "border: 2px solid #1EBA38; border-radius: 5px")
  
)

#.............................server.............................
server <- function(input, output) {}

#......................combine ui & server.......................
shinyApp(ui, server)

A basic app with a large purple level 1 header that reads, 'My App', a blue level 2 header that reads, 'Section 1' with a bit of extra space between letters, an un-styled level 2 header that reads, 'Section 2', and a button with a green border that reads, 'This is a button'.

Add CSS rules to your app’s header (tags$head). This is a little bit better than option 1 since it allows for the reuse of rules, however, styles can’t be cached (i.e. saved for future usage when you reopen your app). Note: explore Google fonts and check out this slide for instructions on selecting a font.

#..............................setup.............................
library(shiny)

#...............................ui...............................
ui <- fluidPage(
  
  tags$head(
    tags$style("
    
      @import url('https://fonts.googleapis.com/css2?family=Lobster&display=swap');
    
      h1 {font-family: 'Lobster', cursive;}
      
      h3 {color: blue;}
      
      .wide-letters {letter-spacing: 4px;}
      
    "
    ) # END styles
  ), # END head
  
  tags$h1("My app title"),
  
  tags$h3("Section 1"),
  
  tags$h3(class = "wide-letters", "Section 2"),
  
  tags$button("This is a button")
  
)

#.............................server.............................
server <- function(input, output) {}

#......................combine ui & server.......................
shinyApp(ui, server)

A basic app with a large black level 1 header written in a cursive font that reads, 'My App', a blue level 2 header that reads, 'Section 1', a blue level 2 header that reads, 'Section 2' with a bit of extra space between letters, and an un-styled button that reads, 'This is a button'.

Build a stylesheet (a .css file) inside your app’s www/ directory and apply your styles to your app’s header (for shinydashboards, include your header inside dashboardBody(). This is the most ideal approach – it allows for style reuse, caching, and keeps styling contained in one spot. Tip: use touch styles.css in the terminal to create a new .css file.

~/myApp/app.R
#..............................setup.............................
library(shiny)

#...............................ui...............................
ui <- fluidPage(
  
  tags$head(
    tags$link(rel = "stylesheet", type = "text/css", href = "styles.css")
  ),
  
  tags$h1("My app title"),
  
  tags$h3("Section 1"),
  
  tags$h3(class = "wide-letters", "Section 2"),
  
  tags$button("This is a button")
  
)

#.............................server.............................
server <- function(input, output) {}

#......................combine ui & server.......................
shinyApp(ui, server)
~/myApp/www/styles.css
/*import google fonts (Josephine Slab (serif) & Heebo (sans serfi))*/
@import url('https://fonts.googleapis.com/css2?family=Heebo:wght@300&family=Josefin+Slab:wght@300&display=swap');

/* element selectors */
h1 {
  font-family: 'Heebo', sans-serif;
  color: #179A1F; /* green */
}

h3 {
  font-family: 'Josefin Slab', serif;
  color: #CA781C; /* orange */
}

button {
  background-color: #FCF982; /* yellow */
}


/* class selectors */
.wide-letters {
  letter-spacing: 4px;
}

A basic app with a large green level 1 header that reads, 'My App' written in a sans-serif font, an orange level 2 header that reads, 'Section 1' with a bit of extra space between letters and written in a serif font, an orange level 2 header that reads, 'Section 2' written in a serf font, and a button with a yellow background that reads, 'This is a button'.

Let’s practice on a small dashboard first:


Create a new subdirectory called css-dashboard/, and add the following ui.R, server.R, and global.R files.

~/css-dashboard/ui.R
#........................dashboardHeader.........................
header <- dashboardHeader(
  
  # add title ----
  title = "Penguin Dashboard"
  
) # END dashboardHeader

#........................dashboardSidebar........................
sidebar <- dashboardSidebar(
  
  # sidebarMenu ----
  sidebarMenu(
    
    menuItem(text = "Dashboard", tabName = "dashboard", icon = icon("gauge"))
    
  ) # END sidebarMenu
  
) # END dashboardSidebar

#..........................dashboardBody.........................
body <- dashboardBody(
  
  # tabItems ----
  tabItems(
    
    # dashboard tabItem ----
    tabItem(tabName = "dashboard",
            
            # fluidRow ----
            fluidRow(
              
              # input box ----
              box(width = 4,
                  
                  checkboxGroupInput(
                    inputId = "penguin_species",
                    label = "Filter by species: ",
                    choices = c("Adelie", "Chinstrap", "Gentoo"),
                    selected = c("Adelie", "Chinstrap", "Gentoo")
                    
                  ) # END checkboxGroupInput
                  
              ), # END input box
              
              # output box ----
              box(width = 8,
                  
                  plotOutput(outputId = "penguin_plot")
                  
              ) # END output box
              
            ), # END fluidRow
            
    ) # END dashboard tabItem
    
  ) # END tabItems
  
) # END dashboardBody

#..................combine all in dashboardPage..................
dashboardPage(header, sidebar, body)
~/css-dashboard/server.R
server <- function(input, output) {
  
  # penguin spp reactive df----
  penguin_spp <- reactive({
    
    na.omit(penguins) |> 
      filter(species %in% input$penguin_species) |> 
      select(species, island, bill_length_mm, bill_depth_mm)
    
  }) # END penguin spp reactive df
  
  
  # plot ----
  output$penguin_plot <- renderPlot({
    
    ggplot(penguin_spp(), aes(x = bill_length_mm, y = bill_depth_mm, color = species)) +
      geom_point() + 
      scale_color_manual(values = c("Adelie" = "#FEA346", "Chinstrap" = "#B251F1", "Gentoo" = "#4BA4A4"))
    
  }) # END plot 
  
}
~/css-dashboard/global.R
# LOAD LIBRARIES ----
library(shiny)
library(shinydashboard)
library(tidyverse)
library(palmerpenguins)

What if I want to style an element, but don’t know how to target it?


Oftentimes, you’ll have to do a bit of exploration to determine how to target specific elements for styling. In either your app viewer or web browser, right click on an element of interest > choose Inspect (or Inspect Element) to open up the underlying HTML and CSS. You can make temporary edits to your app (e.g. adding a background color, changing font sizes, etc.) to see how they look first, then copy the appropriate CSS rule into your stylesheet to apply to your app.

A shiny dashboard, opened in the RStudio viewer, with the Inspect pane open to the right. The top half of the inspect pane shows the underlying HTML and the bottom half shows the underlying CSS styles.

Inspect & identify how to update box styling


For example, let’s say I want to change the color of this shinydashboard’s boxes and the color of the box text (find a gif of these steps on the next slide).

  1. Determine which type of HTML element creates our box
  • right click on the box > choose Inspect Element to pull up the HTML and CSS files underlying the app
  • hover over different parts of the HTML to highlight different elements in the UI

The box is highlighted when I hover over <div class="box-body"> – this tells me that boxes are formed using the <div> HTML element and they’re assigned a class called box-body.

  1. (Temporarily) adjust the CSS rules that style these boxes to see how they work
  • in the CSS file (for me, that’s located in the bottom half of the developer pane), find the .body-body class selector
  • add property / value pairs and / or update existing property values to adjust the appearance of the box
    • note: changing the .box-body class selector updates both boxes (if you inspect the box containing the plot, you’ll notice that it also has the class .box-body, so any changes will apply to both)

This process is purely for testing purposes – refreshing your app will remove any of these changes.

Inspect & identify how to update box styling


Finally, apply box styling to our dashboard


Now that we know that we can use the .box-body class to customize the appearance of our boxes, let’s create a stylesheet inside our www/ folder and add our new CSS rule. The shinydashboard framework already provides the “standard” styling for boxes, contained in the .box-body class. Anything we specify in our own stylesheet will build upon or modify existing styling.

Remember to create a header and link your stylesheet within dashboardBody() to apply our styles.



Check out the complete code for this small example dashboard.

An example shinydashboard with default colors (light blue dashboardHeader, dark blue dashboardSidebar, gray/blue dashboardBody) and two boxes, one containing a checkboxGroupInput and the other containing a scatterplot -- the box colors are colored orange, rather than the default white.

~/css-dashboard/www/styles.css
.box-body {
  background-color: #FCAE82; /* light orange */
  color: #6368C6; /* purple-blue */
}
~/css-dashboard/ui.R
#........................dashboardHeader.........................
header <- dashboardHeader(
  
  # add title ----
  title = "Penguin Dashboard"
  
) # END dashboardHeader

#........................dashboardSidebar........................
sidebar <- dashboardSidebar(
  
  # sidebarMenu ----
  sidebarMenu(
    
    menuItem(text = "Dashboard", tabName = "dashboard", icon = icon("gauge"))
    
  ) # END sidebarMenu
  
) # END dashboardSidebar

#..........................dashboardBody.........................
body <- dashboardBody(
  
  # link stylesheet
  tags$head(
    tags$link(rel = "stylesheet", type = "text/css", href = "styles.css"),
  ),
  
  # tabItems ----
  tabItems(
    
    # dashboard tabItem ----
    tabItem(tabName = "dashboard",
            
            # fluidRow ----
            fluidRow(
              
              # input box ----
              box(width = 4,
                  
                  checkboxGroupInput(
                    inputId = "penguin_species",
                    label = "Filter by species: ",
                    choices = c("Adelie", "Chinstrap", "Gentoo"),
                    selected = c("Adelie", "Chinstrap", "Gentoo")
                    
                  ) # END checkboxGroupInput
                  
              ), # END input box
              
              # output box ----
              box(width = 8,
                  
                  plotOutput(outputId = "penguin_plot")
                  
              ) # END output box
              
            ), # END fluidRow
            
    ) # END dashboard tabItem
    
  ) # END tabItems
  
) # END dashboardBody

#..................combine all in dashboardPage..................
dashboardPage(header, sidebar, body)

What about Sass?


Okay, we wrote and applied some CSS styling to our apps, but what about Sass? You don’t need to write any Sass at all, however, it provides a number of benefits, including helping to reduce repetition.

For example, let’s say you’re working on an app that uses three primary colors throughout:

A color palette consisting of three colors: dark blue, teal, and dark gray.

You might imagine how often you’ll need to type those HEX codes out as you developing your stylesheet…it can get annoying rather quickly.

We can define and reference Sass variables throughout our stylesheet


Sass allows us to define variables (in the form $var-name: value;) for our colors to reference instead of writing out their HEX codes each time:

example-stylesheet.scss
// define Sass vars 

$darkblue: #053660;
$teal: #147C91;
$darkgray: #333333;

// use vars in CSS rules

h1 {
  color: $darkblue;
}

.button-styling {
  background: $teal;
  color: $darkblue; 
  border-color: $darkgray;
}

If you decide that you actually like a different shade of teal better, you’ll only need to update the hex code where you first define the $teal Sass variable, saving lots of time.

Sass for Shiny workflow


To style apps using both Sass and CSS, you’ll follow this general workflow:

(1) Create a .scss file inside ~/myApp/www/ (either through the terminal using the touch command, or using RStudio’s New File > Text File button). Write both your Sass variables and CSS rules in your .scss file (Note: you can write both Sass & CSS in a .scss file, but only CSS in a .css file)

(2) Compile (i.e. convert) Sass to CSS in global.R (or, if using a one-file app, at the top of your script before you define your UI) using the the sass() function from the {sass} package – this will generate a .css file that our shiny app can actually use. Be sure to save your .css file to your app’s /www directory.

(3) Apply your styles to your app by linking to to your .css file in your app’s header.

A chart showing the Sass to CSS workflow -- begin with writing Sass variables and CSS rule in a .scss file, then use the sass R package to compile sass to css, and finally use your compiled .css file to apply your styles to your app.

Let’s build our Sass file


We’ll practice on our two-file-app – first, remove (or comment out) any reference to your {bslib} or {fresh} themes that we practiced applying earlier so that we’re starting off with just the default shiny styling.

Next, create a .scss file inside ~/two-file-app/www using either the touch command or New Blank File > Text File (I’m calling mine sass-styles.scss). Finally, add Sass variables and CSS rules to sass-styles.scss:

~/two-file-app/www/sass-styles.scss
// import & define fonts vars
@import url('https://fonts.googleapis.com/css2?family=Karma&family=Prompt:wght@200&display=swap');
$font-family-serif: 'Karma', serif;
$font-family-sans-serif: 'Prompt', sans-serif;

// colors vars
$green: #8ca376;
$blue: #525cd1; 
$orange: #E59C5E;
$yellow: #f0eaa5;
$white: #f1f7eb;

// css

body {
  background-color: $green; 
  color: $white; 
}

h2 {
  letter-spacing: 5px;
  font-family: $font-family-serif;
}

h4 {
  color: $blue;
  font-family: $font-family-serif;
}

p {
  font-family: $font-family-sans-serif;
}

a {
  color: $orange;
}

.navbar-default {
  background-color: $yellow;
}

.btn.default.active {
  background-color: $green;
}

Then, compile Sass to CSS


Because web browsers can only interpret CSS (not Sass), we need to compile our Sass to CSS. To do this, we can use the sass() function from the {sass} package. We can do this in global.R. The sass() function requires two arguments: a sass file input and a file path + named .css file output.

We also need to apply our styles to our app by linking this newly-generated .css file in our app’s header.

Note: After running your app, you should see a sass-styles.css file appear in www/ – it should look quite familiar, except all of our Sass variables have been converted to CSS.



Check out the complete code for App #2 (NOTE: applied themes are commented out).

Our app, with our sass and css styling applied. The navbar is yellow, body is green, level 4 headings are blue, text is white, hyperlinks are orange, and header and body text font styles have been changed from the default.

~/two-file-app/global.R
# LOAD LIBRARIES ----
library(shiny)
library(sass)
# ~ additional libraries omitted for brevity ~

# COMPILE CSS ----
sass(
  input = sass_file("www/sass-styles.scss"),
  output = "www/sass-styles.css",
  options = sass_options(output_style = "compressed") # OPTIONAL, but speeds up page load time by removing white-space & line-breaks that make css files more human-readable
)

# ~ additional global.R objects omitted for brevity ~
~/two-file-app/ui.R
# navbar page ----
ui <- navbarPage(
  
  # add css file ----
  header = tags$head(
    tags$link(rel = "stylesheet", type = "text/css", href = "sass-styles.css")
  )
  
  # ~ additional UI elements omitted for brevity ~
)
~/two-file-app/www/sass-styles.css
@import url("https://fonts.googleapis.com/css2?family=Karma&family=Prompt:wght@200&display=swap");
body {
  background-color: #8ca376;
  color: #f1f7eb;
}

h2 {
  letter-spacing: 5px;
  font-family: "Karma", serif;
}

h4 {
  color: #525cd1;
  font-family: "Karma", serif;
}

p {
  font-family: "Prompt", sans-serif;
}

a {
  color: #E59C5E;
}

.navbar-default {
  background-color: #f0eaa5;
}

.btn.default.active {
  background-color: #8ca376;
}

End part 4.2

Up next: Improving UX / UI

05:00