1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389
|
---
title: "Overview of the sass R package"
author: "Carson Sievert"
date: "`r Sys.Date()`"
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{Overview of the sass R package}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---
<style>
pre {
border: 1px solid #eee;
}
pre.r {
background-color: #ffffff;
margin-bottom: -0.5em;
}
pre.r code {
background-color: #ffffff;
}
pre.css {
background-color: #f8f8f8;
border-radius: 0;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
</style>
## Why Sass? {#why}
[Sass](https://sass-lang.com/) is the most widely used, feature rich, and stable CSS extension language available. It has become an essential tool in modern web development because of its ability to _reduce complexity_ and _increase composability_ when styling a website. For a basic example, suppose you want to use the same color for multiple styling rules in your website (e.g., hyperlinks and buttons). With CSS, you'd have to repeat that color every time it is used in styling rule, so in a large project, changing at a later point can be tedious and error-prone.
```css
a {
color: salmon;
}
button {
background-color: salmon;
}
```
With Sass, you could store this color in a Sass variable and use it in the styling rules to produce the same CSS, resulting in a single entry point for the color's value. This simple Sass tool can make CSS styling a lot easier to reason about, making styles a lot easier to customize and maintain.
```css
$main-color: salmon;
a {
color: $main-color;
}
button {
background-color: $main-color;
}
```
Sass variables are not the only Sass tool useful for reducing complexity. For Sass and **sass** newcomers, this vignette first covers how to use basic Sass tools like [variables](#variables), [mixins](#mixins), and [functions](#functions) in the **sass** R package. After the basics, you'll also learn how to write [composable **sass**](#layering) that allows users to easily override your styling defaults and developers to [include](#includes) your Sass in their styling projects. You'll also learn how to control the CSS output (e.g., [compress](#compression) and [cache](#caching) it), and how to use it in [**shiny**](#shiny) & [**rmarkdown**](#rmarkdown).
By mastering these concepts, you'll not only be able to leverage the advantages of using Sass over CSS, but you'll also have the basis needed to develop R interfaces to Sass projects that allow users to easily customize your styling templates _without any knowledge of Sass/CSS_. For an example, see the [**bslib** R package](https://github.com/rstudio/bslib), which provides a interface to [Bootstrap Sass](https://getbootstrap.com/docs/4.0/getting-started/theming/) through easy-to-use functions like `bs_theme_add_variables()`.
## Sass Basics {#basics}
```{r setup, include=FALSE}
knitr::opts_chunk$set(
collapse = TRUE,
message = FALSE,
fig.align = "center",
out.width = "80%",
class.output = "css",
comment = ""
)
```
### Variables
[Sass variables](https://sass-lang.com/guide#topic-2) are a great mechanism for simplifying and exposing CSS logic to users. To create a variable, assign a value (likely a CSS [property value](https://developer.mozilla.org/en-US/docs/Learn/Getting_started_with_the_web/CSS_basics)) to a name, then refer to it by name in downstream Sass code. In this minimal example, we create a `body-bg` variable then use it to generate a single [style rule](https://sass-lang.com/documentation/style-rules), but as we'll see later, variables can also be used inside of other arbitrary Sass code (e.g., functions, mixins, etc).
```{r}
library(sass)
variable <- "$body-bg: red;"
rule <- "body { background-color: $body-bg; }"
sass(input = list(variable, rule))
```
A more convenient and readable way to create Sass variables in R is to use a named `list()`. Also, it's a good idea to add the ` !default` flag after the value since it provides users of your Sass an opportunity to set their own value. We'll learn more about defaults in [layering](#layering), but for now, just note that the `!default` flag says use this value only if that variable isn't already defined:
```{r}
user_default <- list("body-bg" = "blue !default")
default <- list("body-bg" = "red !default")
sass(input = list(user_default, default, rule))
```
### Functions {#functions}
Sass comes with a variety of [built-in functions](https://sass-lang.com/documentation/modules) (i.e., you don't have to [import](#imports) anything to start using them) which are useful for working with CSS values ([colors](https://sass-lang.com/documentation/modules/color), [numbers](https://sass-lang.com/documentation/modules/math), [strings](https://sass-lang.com/documentation/modules/string), etc). These built-in functions are primarily useful modifying or combining CSS values in such a way that isn't possible with CSS. Here we use the `rgba()` to add alpha blending to `black` and assign the result to a variable.^[Oh, by the way, the value of a variable may be an expression.]
```{r}
variable <- list("body-bg" = "rgba(black, 0.8)")
sass(input = list(variable, rule))
```
Sass also provides the ability to define your own functions through the [`@function` at-rule](https://sass-lang.com/documentation/at-rules/function). Like functions in most languages, there are four main components to a function definition: (1) the function `name`, (2) the function argument/inputs (e.g., `arg1`, `arg2`), (3) the function body which contains statements (i.e., `statement1`, `statement2`, etc.), and finally (4) a return `value`.
```r
@function name(arg1, arg2) {
statement1;
statement2;
@return value;
}
```
<br>
For an example of where creating your function becomes useful, consider this `color-contrast()` function, inspired by [this SO answer](https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color/3943023#3943023) to a common problem that arises when allowing users control over background color of something (e.g., the document body). We'd like to strive for styling rules that are smart enough to overlay white text on a dark colored background and black text on a light colored background. `color-contrast()` helps us achieve this since, given a dark color, it returns white; and given a light color, it returns black.
```r
@function color-contrast($color) {
@return if(
red($color) * 0.299 + green($color) * 0.587 + blue($color) * 0.114 > 186,
black, white
);
}
```
<br>
By saving this function to a file named `color-contrast.scss`, it can then be [imported](#imports) and used in the following way. For a live example of this in action, consider [this Shiny app](https://gallery.shinyapps.io/sass-font/) which allows the user to interactively choose a background color and the title's font color automatically updates to an appropriate color contrast. [See here](#shiny-dynamic) for more on allowing **shiny** users to influence styling on the page using **sass**.
```{r}
sass(
list(
variable,
sass_file("color-contrast.scss"),
"body {
background-color: $body-bg;
color: color-contrast($body-bg);
}"
)
)
```
**NOTE**: [Bootstrap 4 Sass](https://getbootstrap.com/docs/4.0/getting-started/theming/) provides it's own color contrasting function, named `color-yiq()`, which does essentially the same thing as `color-contrast()`, and is easy to use via `bootstrap_sass()` function the **bslib** package (this function automatically imports Bootstrap's variables, functions, and mixins):
```{r, eval=FALSE, ref.label='bs_sass'}
```
### Importing {#imports}
In practice, you'll want to write your Sass code in `.scss` ([or `.sass`](https://sass-lang.com/documentation/syntax)) files (instead of inside R strings). That way you can leverage things like syntax highlighting in RStudio (or your favorite IDE) and make it easier for others to import your Sass into their workflow. For example, if I have `.scss` file in my working directory, say `my-style.scss`, I can compile it this way:
```r
sass(sass_file("my-style.scss"))
```
```{r, echo = FALSE, out.width='50%'}
knitr::include_graphics('my-style.png')
```
```css
body {
background-color: rgba(0, 0, 0, 0.8);
}
```
This works because `sass_file()` uses `sass_import()` to generate an `@import` at-rule. If you visit the [docs for `@import`](https://sass-lang.com/documentation/at-rules/import), you'll notice there's more you can do that import local `.scss`, like import local or remote `.css` files, import font files, and more.
### Mixins {#mixins}
Similar to how functions are useful for encapsulating _computation_ in a reusable unit, [mixins](https://sass-lang.com/documentation/at-rules/mixin) are useful for doing the same with _styling rules_ (i.e., packaging them into a reusable unit).
Technically speaking, mixins are similar to functions in that they require a `name`, may have arguments, as well as any number of statements. However, they differ in that they require the return value to be a [style rule](https://sass-lang.com/documentation/style-rules), and when called, need to be `@include`d in a larger style rule in order to generate any CSS.
For some examples, please see the [Sass mixin documentation](https://sass-lang.com/documentation/at-rules/mixin).
### More basics
This vignette intentionally doesn't try to re-invent the existing and wonderful [Sass documentation](https://sass-lang.com/documentation). There you'll find many more useful things as you start to write more Sass, such as [control flow](https://sass-lang.com/documentation/at-rules/control), [lists](https://sass-lang.com/documentation/values/lists), [maps](https://sass-lang.com/documentation/values/maps), [interpolation](https://sass-lang.com/documentation/interpolation), and more.
## Composable sass {#layering}
To make Sass code more composable with other Sass code (e.g., allowing others to change your variable defaults or import a function or mixin you've defined), consider partitioning your Sass code into a `sass::sass_layer()`. The main idea is to split your Sass into 3 parts: `defaults` (i.e. variable defaults), `declarations` (i.e., [functions](#functions), [mixins](#mixins), etc), and `rules` (i.e., styling rules).
```{r}
layer1 <- sass_layer(
defaults = list("body-bg" = "white !default"),
declarations = sass_file("color-contrast.scss"),
rules = "body{background-color: $body-bg; color: color-contrast($body-bg)}"
)
sass(layer1)
```
This allows downstream `sass_layer()`s to be `sass_bundle()`d into a single layer, where those downstream layers are granted higher priority. More specifically, this means:
* `defaults` for `layer2` are placed _before_ `defaults` for `layer1`.
* Allowing downstream Sass to override variable defaults in upstream Sass.
* `declarations` for `layer2` are placed _after_ `declarations` for `layer1`.
* Allowing downstream Sass to use functions and mixins defined in upstream Sass.
* `rules` for `layer2` are placed _after_ `rules` for `layer1`.
* Allows downstream rules to take precedence over upstream rules ([precedence](https://css-tricks.com/precedence-css-order-css-matters/) matters when there are multiple rules with the same level of [specificity](https://css-tricks.com/specifics-on-css-specificity/)).
```{r}
layer2 <- sass_layer(
defaults = list("body-bg" = "white !default")
)
sass(sass_bundle(layer1, layer2))
```
### Resolving relative imports {#imports-relative}
Another problem that `sass_layer()` helps solve is that sometimes your Sass code might want to [import](#imports) a local file using a relative path that _you_ know how to resolve, but not necessarily the person who eventually compiles your Sass. To solve this issue, provide a named character vector to `file_attachments`, pointing the relevant relative path(s) to the appropriate absolute path(s). Here's a contrived example of how that might look ([here's](https://github.com/rstudio/bslib/blob/9c973e9/R/layers.R#L75-L77) a more real example of using it in an R package).
```r
sass_layer(
declarations = "@import url('fonts/Source_Sans_Pro_300.ttf')",
file_attachments = c(
fonts = '/full/path/to/my/local/fonts'
)
)
```
### Attaching HTML dependencies {#html-dependencies}
Another problem that `sass_layer()` helps solve is that sometimes you want to attach other HTML dependencies to your Sass/CSS (e.g., JavaScript, other CSS, etc). For this reason, `sass_layer()` has a `html_deps` argument to which you can provide `htmltools::htmlDependency()` objects. `sass()` preserves these, as well as any other HTML dependencies attached to it's input, by including them in the return value. This ensures that, when you include `sass()` in [**rmarkdown**](#rmarkdown) or [**shiny**](#shiny-string) those dependencies come along for the ride.
**DISCLAIMER**: If you want to use this feature _and_ include [CSS as a file in shiny](#shiny-file), you'll need to call `htmltools::htmlDependencies()` on the return value of `sass()` to get the dependencies, then include them in your user interface definition.
## CSS output options {#options}
The `sass()` function provides a few arguments for controlling the CSS output it generates, including `output`, `options`, and `cache_options`. The following covers some of the most important options available.
### Output to a file {#output-file}
If the CSS generated from `sass()` can be useful in more than one place, consider writing it to a file (instead of returning it as a string). To write CSS to a file, give a suitable file path to `sass()`'s `output` argument.
```r
sass(
sass_file("my-style.scss"),
output = "my-style.css"
)
```
### Compression {#compression}
By default, `sass()` outputs `'expanded'` CSS meaning there are lots of white-space and line-breaks included to make it more readable by humans. Computers don't need all those unnecessary characters, so to speed up your page load time when you go to include the CSS in [**shiny**](#shiny) or [**rmarkdown**](#rmarkdown), consider removing them altogether with `output_style = "compressed"`:
```{r}
sass(
sass_file("my-style.scss"),
options = sass_options(output_style = "compressed")
)
```
### Source maps {#source-maps}
When compressing the CSS output, it can be useful to include a [source map](https://www.richfinelli.com/what-is-a-css-source-maps/) so that it's easier to inspect the CSS from the website. The easiest way to include a source map is to set `source_map_embed = TRUE`:
```r
sass(
sass_file("my-style.scss"),
options = sass_options(
output_style = "compressed",
source_map_embed = TRUE
)
)
```
### Caching {#caching}
Sometimes calling `sass()` can be computationally expensive, in which case, it can be useful to leverage its caching capability. Caching is automatically enabled in non-interactive sessions, but you can set the `sass.cache` option to `TRUE` to enable it universally, and/or use `sass_cache_options(cache = TRUE)` to enable for specific calls to `sass()`. See `sass_cache_options()` for more details about how caching works.
```r
options(sass.cache = TRUE)
sass(
sass_file("my-style.scss"),
cache_options = sass_cache_options(cache = TRUE)
)
```
## In shiny {#shiny}
There are two basic approaches to including the CSS that `sass()` returns as HTML
in your **shiny** app. If you're curious, the official [shiny article on CSS](https://shiny.rstudio.com/articles/css.html) has more details with a couple different approaches. Regardless of the approach, consider leveraging [compressing](#compression) and [caching](#caching) the CSS output to make your app faster to load.
### As a string {#shiny-string}
The character string that `sass()` returns is already marked as `HTML()`^[Including [dynamic user input](#shiny-dynamic) directly as `HTML()`, especially with free form inputs like `textInput()`, is a security vulnerability. Avoid it if you can.], so to include it in your **shiny** app, wrap it in a `<style>` tag. It's not necessary to place this tag in the `<head>` of the document, but it's good practice:
```r
library(shiny)
css <- sass(sass_file("my-style.scss"))
fluidPage(
tags$head(tags$style(css),
...
)
```
### As a file {#shiny-file}
To write CSS to a file, give a suitable file path to `sass()`'s `output` argument. Here we write to a specially named `www/` subdirectory so that **shiny** will automatically make those file(s) available to the web app.
```r
library(shiny)
sass(
sass_file("my-style.scss"),
output = "www/my-style.css"
)
fluidPage(
tags$head(
tags$link(href = "my-style.css", rel = "stylesheet", type = "text/css")
),
...
)
```
### As a dynamic input {#shiny-dynamic}
Sometimes it's useful to allow users of your **shiny** app to be able to influence your app's styling via **shiny** input(s). One way this can be done is via [dynamic UI](https://shiny.rstudio.com/articles/dynamic-ui.html), where you use `renderUI()`/`uiOutput()` to dynamically insert [CSS as an HTML string](#shiny-string) whenever a relevant input changes. Be aware, however, that whenever you allow dynamic user input to generate `HTML()`, you're leaving yourself open to security vulnerabilites; so try to avoid it, and _never_ allow clients to enter free form `textInput()` without any sort of sanitation of the user input.
Consider this basic example of using a `colourInput()` widget (from the **colourpicker** package) to choose the body's background color, which triggers a call to `sass()`:
```r
library(shiny)
ui <- fluidPage(
headerPanel("Sass Color Example"),
colourpicker::colourInput("color", "Background Color", value = "#6498d2", showColour = "text"),
uiOutput("sass")
)
server <- function(input, output) {
output$sass <- renderUI({
tags$head(tags$style(css()))
})
css <- reactive({
sass::sass(list(
list(color = input$color),
"body { background-color: $color; }"
))
})
}
shinyApp(ui, server)
```
<br>
```{r, echo = FALSE}
knitr::include_graphics("https://i.imgur.com/5cUEifg.gif")
```
Below are a few more sophisticated examples of dynamic input:
* Font Color
* https://gallery.shinyapps.io/sass-font
* `shiny::runApp(system.file("sass-font", package = "sass"))`
* Sizing
* https://gallery.shinyapps.io/sass-size
* `shiny::runApp(system.file("sass-size", package = "sass"))`
* Themes
* https://gallery.shinyapps.io/sass-theme
* `shiny::runApp(system.file("sass-theme", package = "sass"))`
## In rmarkdown {#rmarkdown}
**knitr** [recently gained](https://github.com/yihui/knitr/pull/1666) a Sass engine powered by the **sass** package. This means you can write Sass code directly into a code `sass` chunk and the resulting CSS will be included in the resulting document (The `echo = FALSE` prevents the Sass code from being shown in the resulting document).
```{sass, echo = FALSE}`r ''`
$body-bg: red;
body{
background-color: $body-bg;
}
```
If you like to write R **sass** code instead, you can do that as well, and by default it works similarly to the `sass` engine (except that the sass-specific code chunk options will be ignored, but you can specify those [options](#options) via `sass::sass()` instead).
```{r, echo = FALSE}`r ''`
sass(sass_file("my-style.scss"))
```
If syntax highlighting is enabled in your output document , it's also possible to display the generated CSS (instead of embedding it as HTML) with syntax highlighting by setting the code chunk options `class.output='css'` and `comment=''`. Unfortunately, for some output formats, like `rmarkdown::html_vignette()`, syntax hightlighting isn't supported, but for output formats like `rmarkdown::html_document()`, you can enable syntax highlighting by setting the `highlight` parameter to a non-default value (e.g., `tango`).
```{r, class.output='css', comment=''}`r ''`
sass(sass_file("my-style.scss"))
```
|