7 Introduction to SASS

One of the main problem with CSS is to handle the growing number of files and the code repetition. Nowadays, web developers mainly use CSS pre processors like Sass, that stands for “Syntactically Awesome Style Sheets”, providing access to variables, mathematical operators, functions and loops, thereby reducing the code complexity and extending the possibilities. Rstudio developed the {sass} package (Cheng, Mastny, et al. 2021), which makes it possible to use Sass in Shiny apps or Rmarkdown documents.

7.1 Getting started with Sass

While we could use Sass from the terminal, we leverage the sass package features to work from R. To install sass, we run:

install.packages("sass")
# OR
remotes::install_github("rstudio/sass")

The main function is sass(), whose input parameter accepts:

library(sass)
sass(input = ".element-class { color: pink;}")

7.1.1 Variables

Let us consider the following example, where two different classes have the same color:

.class-1{
  color: #33BEFF;
}

.class-2{
  background-color: #33BEFF;
}

Shouldn’t this be easier? Imagine if we had hundreds of elements with the same color. What happens in case the color changes? Do we have to update all properties by hand?

If we let the Sass variable $my-color:

$my-color: purple;
.class-1{
  color: $my-color;
}
.class-2{
  background-color: $my-color;
}

we can quickly solve that problem. With sass, we define one variable holding the color as well as our two rules, so that we obtain:

var <- "$my-color: purple;"
rule1 <- ".class-1{ color: $my-color; }"
rule2 <- ".class-2{ background-color: $my-color; }"
sass(input = list(var, rule1, rule2))
## /* CSS */
## .class-1 {
##   color: purple;
## }
## 
## .class-2 {
##   background-color: purple;
## }

Add the default! tag after the variable definition, if you want to let others modify it, that is "$my-color: purple !default;".

7.1.2 Partials and Modules

It is best practice to save useful code snippets in one place, and reuse them at anytime and anywhere. Sass allows to define partials, like _partial.css, with the leading underscore, which avoids it to be converted into CSS. Partials are subsequently called with @import <PARTIAL_NAME> (you may also find @use, the latter not being handled by LibSass, which fuelssass), thereby significantly reducing code duplication.

Modules are pieces of Sass files that are later converted into CSS, reducing file size to a minimum. Below is an example of the bootstrap.scss file:

/*!
 * Bootstrap v5.0.0-beta1 (https://getbootstrap.com/)
 * Copyright 2011-2021 The Bootstrap Authors
 * Copyright 2011-2021 Twitter, Inc.
 * Licensed under MIT 
 * (https://github.com/twbs/bootstrap/blob/main/LICENSE)
 */

// scss-docs-start import-stack
// Configuration
@import "functions";
@import "variables";
@import "mixins";
@import "utilities";

// Layout & components
@import "root";
@import "reboot";
...

// Helpers
@import "helpers";

// Utilities
@import "utilities/api";
// scss-docs-end import-stack

which is easier to read and maintain than the original bootstrap.css with 10717 lines of code! In practice, we often end up with a main Sass file and compile it as follows:

sass(sass_file("main.scss"))

7.1.3 Mixins and Functions

Another great advantage of Sass is the ability to generate reusable units of code, also known as mixins or functions.

7.1.3.1 Mixins

To make a 90 degrees rotation in CSS, we have to write:

.element {
  -webkit-transform: rotate(90deg);
  -ms-transform: rotate(90deg);
  transform: rotate(90deg);
}

which is already tedious. Mixins allow to encapsulate the logic into a reusable unit:

@mixin transform($property, ...) {
  -webkit-transform: $property;
  -ms-transform: $property;
  transform: $property;
}

.element1 { 
  @include transform(rotate(90deg)); 
}

The mixin starts with a @mixin keyword followed by its name and parameters. It is called with @include <MIXIN_NAME(PARMS)>, very similar to a function declaration, excepts that it must return a CSS rule.

mixin <- "@mixin transform($property) {
  -webkit-transform: $property;
  -ms-transform: $property;
  transform: $property;
}"
rule <- ".element1 { @include transform(rotate(90deg)); }"
sass(input = list(mixin, rule))
## /* CSS */
## .element1 {
##   -webkit-transform: rotate(90deg);
##   -ms-transform: rotate(90deg);
##   transform: rotate(90deg);
## }

7.1.3.2 Functions

Sass offers many built-In modules containing ready to use functions for colors, numbers, strings, lists, maps, … Some functions like rgb are global, so that we don’t have to import the corresponding module.

sass(".pouet { color: rgb(0, 255, 0); }")
## /* CSS */
## .pouet {
##   color: lime;
## }

It is definitely possible to design custom functions with @function, whose syntax is very close the mixins:

@function name($parm1, $parm2) {
  /* logic */
  @return value;
}

While debugging functions, it might be useful to capture intermediate elements. @debug allows this:

$my-var: 1;
@debug myvar;
sass("
  @function multiply($parm1, $parm2) {
    @debug 'parm1 is #{$parm1}';
    @debug 'parm2 is #{$parm2}';
    
    @return $parm1 * $parm2;
  }
  
  .my-class {
    width: multiply(2, 4) * 1px; 
  }
")
## /* CSS */
## .my-class {
##   width: 8px;
## }

7.1.4 Extend/Inheritance

We consider two alerts with the color as only difference. As we can’t capture multiple properties inside one single Sass variable, we introduce the extend concept, which permits to import CSS properties inside multiple rules. We first define a generic alerts-common rule, prefixed by the % symbol. It contains several rules and variables:

%alerts-common {
  position: relative;
  padding: $alert-padding-y $alert-padding-x;
  margin-bottom: $alert-margin-bottom;
}


.alert-red {
  @extend %alerts-common;
  color: red;
}

.alert-green {
  @extend %alerts-common;
  color: green;
}

Let’s translate this into R:

y_padding <- "$alert-padding-y: 5px;"
x_padding <- "$alert-padding-x: 10px;"
b_margin <- "$alert-margin-bottom: 2px;"
common <- "%alerts-common {
  position: relative;
  padding: $alert-padding-y $alert-padding-x;
  margin-bottom: $alert-margin-bottom;
}"
alert_red <- ".alert-red {
  @extend %alerts-common;
  color: red;
}
"
alert_green <- ".alert-green {
  @extend %alerts-common;
  color: green;
}
"
sass(input = list(
  y_padding, 
  x_padding, 
  b_margin, 
  common, 
  alert_red, 
  alert_green
))
## /* CSS */
## .alert-green, .alert-red {
##   position: relative;
##   padding: 5px 10px;
##   margin-bottom: 2px;
## }
## 
## .alert-red {
##   color: red;
## }
## 
## .alert-green {
##   color: green;
## }

This method avoids to multiply classes on elements such as .alert-common .alert-red .... Yet, we could have programmatically generated the two alert classes with a loop, to avoid duplication.

7.1.5 Flow Controls

These are elements aiming at fine tuning mixins and functions behavior.

7.1.5.1 if and else

Like in every programming language if and else control the execution of a code block, depending on some conditions. Below, we only want to conditionally control a shadow property, depending on the alert color:

@mixin add-shadow($box-color) {
  @if $box-color == red {
    box-shadow: 
      0 4px 10px 0 rgb(255, 0, 0), 
      0 4px 20px 0 rgb(255, 0, 0);
  } @else if $box-color == green {
    box-shadow: 
      0 4px 10px 0 rgb(0, 255, 0), 
      0 4px 20px 0 rgb(0, 255, 0);
  }
}

.alert-red {
  @extend %alerts-common;
  color: red;
  @include add-shadow($box-color: red);
}

.alert-green {
  @extend %alerts-common;
  color: green;
  @include add-shadow($box-color: green);
}
add_shadow <- "@mixin add-shadow($box-color) {
  @if $box-color == red {
    box-shadow: 
      0 4px 10px 0 rgb(255, 0, 0), 
      0 4px 20px 0 rgb(255, 0, 0);
  } @else if $box-color == green {
    box-shadow: 
      0 4px 10px 0 rgb(0, 255, 0), 
      0 4px 20px 0 rgb(0, 255, 0);
  }
}
"
y_padding <- "$alert-padding-y: 5px;"
x_padding <- "$alert-padding-x: 10px;"
b_margin <- "$alert-margin-bottom: 2px;"
common <- "%alerts-common {
  position: relative;
  padding: $alert-padding-y $alert-padding-x;
  margin-bottom: $alert-margin-bottom;
}"
alert_red <- ".alert-red {
  @extend %alerts-common;
  color: red;
  @include add-shadow($box-color: red);
}
"
alert_green <- ".alert-green {
  @extend %alerts-common;
  color: green;
  @include add-shadow($box-color: green);
}
"
sass(input = list(
  y_padding, 
  x_padding, 
  b_margin, 
  common, 
  add_shadow, 
  alert_red, 
  alert_green
))
## /* CSS */
## .alert-green, .alert-red {
##   position: relative;
##   padding: 5px 10px;
##   margin-bottom: 2px;
## }
## 
## .alert-red {
##   color: red;
##   box-shadow: 0 4px 10px 0 red, 0 4px 20px 0 red;
## }
## 
## .alert-green {
##   color: green;
##   box-shadow: 0 4px 10px 0 lime, 0 4px 20px 0 lime;
## }

7.1.5.2 Loops

7.1.5.2.1 Each

We would like to create the alert class with only one rule. We first define a list of colors in Sass and call the @each:

$colors: red, green;

@each $color in $colors {
  .alert-#{$color} {
    color: green;
    @include add-shadow($box-color: $color);
  }
}

The structure is the same as the JavaScript loop. You’ll also notice the #{...} which is called interpolation and allows to insert any Sass expression in a string. As another example, if we want to create a background-image property within a mixin, we could do background-image: url("/some_path/#{$name}.svg"), where #{$name} holds the file name.

add_shadow <- "@mixin add-shadow($box-color) {
  @if $box-color == red {
    box-shadow: 
      0 4px 10px 0 rgb(255, 0, 0), 
      0 4px 20px 0 rgb(255, 0, 0);
  } @else if $box-color == green {
    box-shadow: 
      0 4px 10px 0 rgb(0, 255, 0), 
      0 4px 20px 0 rgb(0, 255, 0);
  }
}
"
y_padding <- "$alert-padding-y: 5px;"
x_padding <- "$alert-padding-x: 10px;"
b_margin <- "$alert-margin-bottom: 2px;"
common <- "%alerts-common {
  position: relative;
  padding: $alert-padding-y $alert-padding-x;
  margin-bottom: $alert-margin-bottom;
}"
alerts_rule <- "$colors: red, green;
@each $color in $colors {
  .alert-#{$color} {
    @extend %alerts-common;
    color: green;
    @include add-shadow($box-color: $color);
  }
}
"
sass(input = list(
  y_padding, 
  x_padding, 
  b_margin, 
  common, 
  add_shadow, 
  alerts_rule
))
## /* CSS */
## .alert-green, .alert-red {
##   position: relative;
##   padding: 5px 10px;
##   margin-bottom: 2px;
## }
## 
## .alert-red {
##   color: green;
##   box-shadow: 0 4px 10px 0 red, 0 4px 20px 0 red;
## }
## 
## .alert-green {
##   color: green;
##   box-shadow: 0 4px 10px 0 lime, 0 4px 20px 0 lime;
## }

It becomes even more powerful while working with maps like $font-weights: ("regular": 400, "medium": 500, "bold": 700);, i.e by key/value pairs. @each is as convenient as lapply or map functions to chain repetitive rules creation.

7.1.5.2.2 For

However, it is not straightforward to count up or down with @each. This is precisely where @for fills the gap. The generic scheme is:

@for <variable> from <expression> to <expression> { ... } 
@for <variable> from <expression> through <expression> { ... }

to excludes the last number while through includes it.

7.2 {sass} best practices

As it’s best practice, especially for debugging purposes, to include assets as HTML dependencies, it is a good idea to organize the Sass variable definition, function/mixins in layers, leveraging the sass_layer() function:

var <- "$my-color: purple !default;"
rule1 <- ".class-1{ color: $my-color; }"
rule2 <- ".class-2{ background-color: $my-color; }"
layer1 <- sass_layer(
  default = var,
  rules = c(rule1, rule2)
)
## /* Sass Bundle */
## $my-color: purple !default;
## .class-1{ color: $my-color; }
## .class-2{ background-color: $my-color; }
## /* *** */

Besides, sass_layer() provide options like:

  • declarations containing any function, mixin elements, in a sass_file for instance.
  • html_deps that attaches a single or a list of HTML dependencies to the provided Sass code, as shown below.
sass_layer(
  html_deps = htmltools::htmlDependency(
    name = "my-dep", 
    version = "1.0.0",
    package = "mypkg",
    src = "path",
    ...
  )
)

Ultimately, multiple layers may be bundled with sass_bundle():

var2 <- "$my-color: blue !default;"
layer2 <- sass_layer(
  default = var2,
  rules = c(rule1, rule2)
)


my_bundle <- sass_bundle(layer1 = layer1, layer2 = layer2)
my_bundle
## /* Sass Bundle: layer1, layer2 */
## $my-color: blue !default;
## $my-color: purple !default;
## .class-1{ color: $my-color; }
## .class-2{ background-color: $my-color; }
## .class-1{ color: $my-color; }
## .class-2{ background-color: $my-color; }
## /* *** */
## /* CSS */
## .class-1 {
##   color: blue;
## }
## 
## .class-2 {
##   background-color: blue;
## }
## 
## .class-1 {
##   color: blue;
## }
## 
## .class-2 {
##   background-color: blue;
## }

sass_bundle_remove() removes a given layer from the bundle, provided that you passed a named list to sass_bundle(). This allows other developers to reuse and modify predefined layers:

my_bundle <- sass_bundle_remove(my_bundle, "layer2")
my_bundle
## /* Sass Bundle: layer1 */
## $my-color: purple !default;
## .class-1{ color: $my-color; }
## .class-2{ background-color: $my-color; }
## /* *** */
sass(my_bundle)
## /* CSS */
## .class-1 {
##   color: purple;
## }
## 
## .class-2 {
##   background-color: purple;
## }

7.3 From Sass to CSS

sass() can generate CSS from Sass by passing an output parameter pointing to the path where to generate the CSS file. Best practice consists in enabling compression and source maps. We discuss this later in the book in Chapter 22. Overall those steps makes the code faster to load and easier to debug:

sass(
  list(
   "$color: pink;",
   ".a { color: $color; }" 
  ),
  options = sass_options(
    output_style = "compressed",
    source_map_embed = TRUE
  )
)
sass(
  sass_file("main.scss"),
  "<OUTPUT PATH>",
  options = sass_options(
    output_style = "compressed",
    source_map_embed = TRUE
  )
)

7.4 Sass and Shiny

Now let’s go back to Shiny! How do we include Sass code in a Shiny app? There are multiple situations:

  1. You simply want to style a shiny app.
  2. You developed a template with custom JS and Sass/CSS to be reused by other developers.

The first option is rather simple since the Sass code is compiled with sass() before the shiny apps is launched. The resulting code may be either a string or a CSS file (within the www folder), to be included in the head. We assume to be located at the app folder level:

sass(
  list(
   c("$color: pink;", "$size: 30px;"),
   c(".awesome-link { 
        color: $color; 
        font-size: $size; 
        &:hover{
          color: green;
        }
      }"
    )
  ), 
  output = sprintf("www/main.min.css"),
  options = sass_options(
    output_style = "compressed",
    source_map_embed = TRUE
  )
)

Source maps allow us to see the original Sass code, as shown Figure 7.1. sass_options() gives the flexibility to fine tune the CSS output and source map configuration.

For complex projects where the CSS compilation may take time, we strongly advise to process the CSS independently from the app startup.

Inspect Sass code in the web browser

FIGURE 7.1: Inspect Sass code in the web browser

The second option requires to run sass() passing an output file within the package. Then, the generated CSS is included in an HTML dependency, ready to be shipped with the template:

sass(
  sass_file("main.scss"),
  "<OUTPUT PATH>/main.css",
  options = sass_options(
    output_style = "compressed",
    source_map_embed = TRUE
  )
)

my_css_deps <- htmltools::htmlDependency(
  name = "my-style", 
  version = "1.0.0",
  package = "mypkg",
  src = "<OUTPUT PATH>",
  stylesheet = "main.css"
)

7.5 Examples

7.5.1 Customize {bs4Dash} colors

bs4Dash is a Bootstrap 4 dashboard template built on top of the AdminLTE3 HTML template. shinydashboard is powered by the previous version, that is AdminLTE2, which make it somehow bs4Dash’s big brother! AdminLTE3 relies on Sass and all files are stored here. Particularly, all variables are located in the _variables.scss partial. Since we can decompose our Sass code in multiple layers, we seamlessly customize the theme color variables listed below:

$blue: #0073b7 !default;
$lightblue: #3c8dbc !default;
$navy: #001f3f !default;
$teal: #39cccc !default;
$olive: #3d9970 !default;
$lime: #01ff70 !default;
$orange: #ff851b !default;
$fuchsia: #f012be !default;
$purple: #605ca8 !default;
$maroon: #d81b60 !default;
$black: #111 !default;
$gray-x-light: #d2d6de !default;

Let’s provide our own defaults with some custom colors:

$blue: #136377 !default;
$olive: #d8bc66 !default;
$lime: #fcec0c !default;
$orange: #978d01 !default;
$maroon: #58482c !default;
$gray-x-light: #d1c5c0 !default;

Now we would have to recompile the whole AdminLTE3 Sass code to account for these changes. It means, all scss assets must be accessible somewhere: this is what the fresh package is doing under the hoods. No worries, we’ll come back on fresh in the next Chapter. For now, we rely on the fresh Sass code stored at system.file("assets/AdminLTE-3.1.0/AdminLTE.scss", package = "fresh"):

library(bs4Dash)

css <- sass(
  sass_layer(
    default = c(
      "$lightblue: #136377 !default;",
      "$olive: #d8bc66 !default;",
      "$lime: #fcec0c !default;",
      "$orange: #978d01 !default;",
      "$maroon: #58482c !default;",
      "$gray-x-light: #d1c5c0 !default;"
    ),
    rules = sass_file(
      input = system.file(
        "assets/AdminLTE-3.1.0/AdminLTE.scss", 
        package = "fresh"
      )
    )
  )
)


ui <- dashboardPage(
  dashboardHeader(title = "Custom colors"),
  dashboardSidebar(),
  dashboardBody(
    tags$head(tags$style(css)),
    # Boxes need to be put in a row (or column)
    fluidRow(
      box(
        solidHeader = TRUE, 
        plotOutput("plot1", height = 250), 
        status = "olive"
        ),
      box(
        solidHeader = TRUE,
        status = "lightblue",
        title = "Controls",
        sliderInput(
          "slider", 
          "Number of observations:", 
          1, 
          100, 
          50
        )
      )
    )
  )
)

server <- function(input, output) {
  set.seed(122)
  histdata <- rnorm(500)

  output$plot1 <- renderPlot({
    data <- histdata[seq_len(input$slider)]
    hist(data)
  })
}

shinyApp(ui, server)

You probably noticed a potential issue. We indeed have to rely on a specific AdminLTE version, namely 3.1.0, that is not necessarily the one we want. Therefore, an alternative would be to download the Sass files from AdminLTE3, store them in a package, ideally the /inst folder, and recompile the code from that folder with new variables. As AdminLTE3 depends on Bootstrap 4, we would have to recover those dependencies in a separate folder, making sure it is accessible to the AdminLTE Sass code.

7.5.2 Customize {shinybulma}

For convenience, the Sass code is already included in the book side package. The goal is to change the main color palette that comprises:

  • primary
  • info
  • success
  • warning
  • danger

In total, bulma exposes 419 Sass variables!

Among all files, we locate the main variables file and select the relevant variables we want to modify. Notice we can retrieve all those information: initial variables and derived variables.

We assign them new values:

$turquoise: #03a4ff;
$cyan: #e705be;
$green: #f3d6e9;
$yellow: #fdaf2c;
$red: #ff483e;
$scheme-main: hsl(0, 0%, 4%);

Particularly, we target the main body color stored in $scheme-main. Instead of pointing to $white, we change its value to the default $black. We then Compile the new CSS with sass():

css <- sass(
  sass_layer(
    default = c(
      "$turquoise: #03a4ff;",
      "$cyan: #e705be;",
      "$green: #f3d6e9;",
      "$yellow: #fdaf2c;",
      "$red: #ff483e;",
      "$scheme-main: hsl(0, 0%, 10%);"
      
    ),
    rules = sass_file(input = system.file(
      "sass/bulma/bulma.sass", 
      package = "OSUICode"
    ))
  )
)

Finally, we try the new theme in the following app, shown on Figure 7.2:

#remotes::install_github("RinteRface/shinybulma")

library(shinybulma)

shinyApp(
  ui = bulmaPage(
    tags$head(tags$style(css)),
    bulmaSection(
      bulmaTileAncestor(
        bulmaTileParent(
          vertical = TRUE,
          bulmaTileChild(
            bulmaTitle("Tile 1"),
            p("Put some data here"),
            color = "link"
          ),
          bulmaTileChild(
            bulmaTitle("Tile 2"),
            "Hi Bulma!",
            color = "danger"
          )
        ),
        bulmaTileParent(
          vertical = TRUE,
          bulmaTileChild(
            bulmaTitle("Tile 3"),
            p("Put some data here"),
            color = "warning"
          ),
          bulmaTileChild(
            bulmaTitle("Tile 3"),
            ("Put some data here"),
            color = "info"
          )
        )
      )
    )
  ),
  server = function(input, output) {}
)
Custom bulma theme

FIGURE 7.2: Custom bulma theme