
Pedro Silva | Styling Shiny with CSS & SASS and Speeding Up Shiny Apps | Posit
From rstudio::global(2021) Shiny X-Sessions, sponsored by Appsilon: in the first part of this talk I will discuss how to use CSS to give your application a fresh and unique look, while keeping your codebase clean and organized with SASS. During the second half I will discuss how to leverage Shiny update functions, proxy objects and JavaScript messages to speed up your dashboards. About Pedro Silva: Pedro has nearly a decade of experience combining frontend and backend technologies, and is an expert on augmenting R Shiny dashboards with CSS and JavaScript. Learn more about the rstudio::global(2021) X-Sessions: https://blog.rstudio.com/2021/01/11/x-sessions-at-rstudio-global/
image: thumbnail.jpg
Transcript#
This transcript was generated automatically and may contain errors.
I'm really also really interested in BSLib. I've actually started playing around with it a tiny bit before in a previous project. I think there's a lot of potential there.
For my presentation today, I would actually like to talk to you more about how do you actually do custom solutions. It's very common in Shiny that you end up having to create custom components, create custom widgets, that you end up having to style them again. But because there's no package for them, they're not part of base Shiny, you end up having to do your own custom styling.
With that, we can get started. I will just give a quick introduction about me. I've been a Shiny developer with Appsilon for closing in on three years now. Before that, I was a web developer for the better part of eight years. There's a lot of the skills that I actually got as a web developer kind of translate very well into Shiny. And also, I'm an open source creator and contributor with Appsilon as well.
You can reach me in any of those social medias. If you'd like to check out my code, take a look at GitHub. If you just have a question or would like to get in touch, you can also reach me. For today, I will give you a small introduction of what CSS is, in case you don't know. I will also talk about the different ways that you can add it to Shiny. I will talk a bit about Sass, a package from RStudio. I'll give you a short introduction, just to get you started, and also how you can use it to Shiny.
CSS crash course
Like a very quick introduction, the crash course to CSS, what is it? CSS is basically how the internet describes the way that elements are displayed on a page. Basically, it lets you control multiple elements by the usage of classes and selectors. And basically, it's made of very small instructions, which are called statements. Statements do simply two things. They identify an object in the page, and they declare a value for a specific property of that element.
This is a very simple example of what a CSS statement would look like. You can see we're styling something that has a class of search box, and we're styling the border in the background of that specific element. The cool thing with CSS is if you have two elements, or ten elements, or 50 elements with this specific class, they will all be affected by this specific group.
So, why is this really important for Shiny? The thing is, every web page right now, in some way or another, uses CSS, and Shiny is no exception. So, even though you're programming in R, and you're building this Shiny application, behind the scenes, what's actually happening is that Shiny is generating some JavaScript, some HTML, some CSS, and the key part here is the CSS. So, because some CSS is being generated, we can also add our own on top of it.
Ways to add CSS to Shiny
So, how would we actually go about doing this? So, with Shiny itself, there's three very specific ways that you can do it. So, you can add the style directly to your tags when you're building your UI. You can add it as, let's say, a separate tag in the header of your page, or your application, but you can also store it in files and link those files directly to the Shiny application.
So, for the first one, whatever you create in your UI, as long as it's an HTML tag, you can always pass a special style attribute, which lets you basically pass any kind of CSS statement that will affect that specific element. So, here, there's no statements, there's no classes, there's nothing. This is style that you want to apply to that specific element. And, this is fine. This is okay. But, to be honest, this is probably, this should be your last resort when it comes to adding CSS to Shiny.
The truth is that, by adding, it's fine if you have one or two elements, but when you get to 10, 20, 50 elements, when you start having very complex UIs, you lose track of where you style the specific thing. You cannot reuse any rules. So, if you have three titles, you need to style the three titles. It's very hard to keep consistency between all the elements. This is something that, if you use BSLib, for example, it's all for you, because there's general rules that affect every element, but if you're doing these individual changes, you won't be able to actually keep track of them as your project grows.
The second way would be to add CSS to your header. So, this is slightly better. You can add a style tag with some text inside, and that text can be CSS text. This is slightly better. This does mean that you can use those selectors. You can apply the same styling to a lot of elements at the same time, but at the same time, you won't really be able to leverage the browser here, because you're generating the CSS whenever the page is created. This actually means that your browser won't be able to cache these. By caching, I mean saving them for future usage when you reopen the application later. So, you end up having a slightly better approach, but at the same time, it's still not the ideal approach.
So, we can always do the third option. So, this would be to create a file with all of our CSS on the side, link this file to our Shiny application, and then the browser actually has something that it can save for the future, and at the same time, we have a specific place where we have all our styling neatly saved, packed away from our code, from our logic. This is much better. This is probably the best approach you can take if you're using just regular CSS. You can reuse the code, you can reuse the rules, you can cache these files, your browser can leverage the cache to actually save these files for later. It also allows you to kind of separate your styling into multiple files and start adding a bit of structure to how you're saving styling for different elements, for different modules. However, CSS is still something that can get very complex, and it's kind of this thing that grows out of control very fast as your project grows. So, there's still a bit of room for improvement here.
Introducing Sass
So, one way that you can improve this workflow even better is to use Sass. So, Sass is, as Tom mentioned before, this is a language built on top of CSS, and the idea here is that it's preprocessor to CSS, so any Sass that you create always ends up being CSS, but there's this layer of obstruction where you have a few different tools, and you don't really need to worry about creating very complex or managing very complex CSS, because Sass gives you a couple of ways of managing it a bit better.
So, a couple of differences here between Sass and CSS are Sass is object-oriented, so this means that you can have nesting, it's more about the concept of what is a specific element that you're trying to target, not the specific element that you're targeting. It also allows variables. I know that CSS3 is actually pushing forward with variables in CSS. This is, I think, a very nice bonus, but it's one of the things that Sass, at least my understanding, still does much better than just CSS. And also, you can always use Sass to actually generate CSS variables to reuse in the future.
So, a couple of really interesting things that you can do with Sass. So, we talked about nesting, so what nesting means is that you can simplify a lot of the rules that you do by nesting them. So, as you can see, CSS doesn't, CSS on the right side doesn't really allow for this, so if you're targeting two different elements, in this case, for example, a UL and an LI element inside of an app bar, you're going to need to create the full selector every time you create a rule. Sass kind of solves this by allowing nesting, so if you have two elements inside the same elements, you can kind of nest them together and simplify your code even a tiny bit more.
We also talked about variables, so CSS does allow variables, but Sass does it in a way that you can kind of declare them, even in separate files, completely different. There's no scope required, which is something that CSS variables need, and you can simply use them by assigning a name to a property that starts with a dollar sign, and then just this becomes a variable that you can use throughout your code.
It also has two very important mechanisms, which are extents and mixins, so extents is kind of, and this is something that is very well explained with an example, so it basically allows you to share styles between selectors, and the way that you would do this normally in CSS, if you would have two elements that you would like to style, even though they are very similar, is that you would simply do the styling for both of them. By using extend, you can actually create a partial part of code by using the percentage as a marker for the name of that block of code, and you can then extend for each element, you can simply extend the name, the extent rule that you created, and the code will actually be copied into that selector, so this means that you can reuse parts of styling that you use very often by simply extracting them into a separate name that you can then reuse.
The second one is mixins, so mixins are slightly similar, but these work more like functions, so the idea here is that instead of simply taking out a piece of a block of code and saving it with a name, with a mixin, you're actually having a function, so this means that you can pass specific arguments to those functions that you're creating.
Using Sass with R
So, a small introduction of Sass, a very big, a very crash course-y kind of thing, but hopefully it shows you some of the advantages that you actually have. So, how do you actually use this with R? So, the best way, of course, is to use a library, and RStudio was kind enough to actually create a really nice package for us, so you can use RSass to not have to worry about all the pre-processing, you just need to write code, this is all you want to do, and in general, the package gives you one big function called sass, and this sass function can take either a string or a full file and just returns the CSS to you.
So, all of these techniques that we've been talking about, you know, I've accessed them inside of R directly without actually having to work outside of our files. The second way is actually, you can actually pass it a reference to a file. So, here, for example, we're simply pointing the sass function to our style's main file, and this file has a lot of internal stuff for sass, and this way, you can kind of tile all of your, you can kind of split all of your styling into something that behaves a bit more like code managing, and actually lets you reuse a lot of the styles and kind of keep a grasp of styles when it comes to very big projects.
A second argument that's very important in this function is the output. So, the output will generate that CSS to a specific file, and then this is something that you can simply add to your UI, and now you have all of your styles saved in a project folder structure that has everything well sorted, and you can kind of manage them. If you would like to see an example of sass being used very extensively, I will point you to my Shiny contest entry from last year. I've used sass quite a lot, and hopefully it will guide you in the right way to get started.
Yeah, and I think my 15 minutes are up, and I would just like to thank you for this, and I also, I know that the slides will be available later, so I've also added a couple of resources here to get you started with sass, and in case you really want to get into CSS, there's also a couple of bonus slides with some of the selectors that I think are at least the basic ones that you need to know for CSS. So, hopefully this will get you started, and in one year, you'll be thinking, how have I been using CSS without sass?
Speeding up Shiny apps
Let's talk about speeding up Shiny apps. So, small disclaimer, this is going to be just some of the, I think, very important details that you always need to take care of. It doesn't mean that you'll be able to speed up everything using just these tips, but hopefully it will get you started when it comes to at least identify some of the issues that your Shiny app might have. So, I'm going to talk about three very important details when it comes to speeding up Shiny applications. The first one is update functions. I will also talk a bit about proxy objects and proxy functions, and then I will finish up by letting you know a bit about custom messages and browser offloading. I won't get into too much detail, and, of course, every package is different and needs different types of optimisations, but hopefully this will help you get started.
So, update functions. What are update functions? So, update functions are very simple functions that let you update existing Shiny widgets. So, the important thing here is that base Shiny has these, but it's very possible that you're not always using them. So, as an example, we have here a very simple Shiny application that has just a checkbox and a drop-down, and you can see that in our UI, we define a checkbox and we define a UI output, and then in our server, we simply observe that checkbox and we change our select input label and values depending on the state of the checkbox. This feels very standard. This is something that you probably did quite a lot of times already.
However, you can see that we're not really using any update functions. So, what do I mean by that? What's happening here is that every time we trigger a change with the checkbox, we're actually re-rendering the drop-down. So, you can see that, because we're using a UI output, and then filling in that UI output with our drop-down, what's actually happening behind the scenes is that, whenever this output changes, Shiny is removing all the bindings that it created in the background. It creates a new set of bindings for a new drop-down that just, it does have the same ID, but it's a completely new drop-down, and has to rebuild the whole structure. So, this, we can probably do better here, and this is where update functions come in.
So, if we simply change our codes to, instead of using a UI output, to simply render a select input, and then, instead of filling in the UI output, we simply use the update select input function, this is completely different. So, in this case, what's happening is that there's a call from Shiny being sent to the browser that says, hey, update this specific input that already exists in the browser with a new label and some new values. So, you can see here that these are very small changes, but it's already a slightly different approach to how we're actually building the application.
But does this really matter? And just to give you an example of how, what kind of improvements we're talking about, the difference, even for this simple drop-down, is actually that for the page to be completely re-rendered and ready for the user to use, there's a, it goes down from 15 milliseconds to 10 milliseconds. So, just a simple, and we're not even talking about the very complex thing that we're doing here, it's just a simple drop-down. It's already a massive improvement. So, you can imagine that if we're talking about 50 drop-downs, 50 elements that we're re-rendering, these really add up very quickly.
So, why is this actually better? Like I said, the update function is actually just sending a message to the browser and then updating that specific element. So, there's no need to rebuild anything. It just uses the bindings that Shiny already created for that specific element.
When it comes to update functions, these are present in most Shiny widgets out of the box. So, if you're using even just base Shiny, any kind of input that you create, it probably has a corresponding update function. So, action buttons always have an update action button. Select inputs always have an update select input. And you can see here that there's a bit of a standard where the update function is always the name of the UI definition of the widget with a prefix update. Of course, always make sure to check the documentation. Usually, these are linked in the bottom. After the function definition, you have some information about what kind of updates actually exist and what kind of arguments can actually be updated with that function.
Proxy objects
So, these update functions are nice, but what happens when we have slightly more complex widgets? And this is the case of when you have, for example, a DT table or an e-charts chart or a leaflet map. These usually work slightly different because we're talking about very complex widgets that a simple update function is probably not enough for everything.
So, this is where proxies come in. So, proxies are basically a way to reference an existing widget, an existing complex widget. And then, usually, they have their own set of functions that behave similar to update functions, but are applied to that specific instant proxy instance of the widget itself.
So, a lot of what usually happens is that an example is DT. So, you render, you create your data table output, you fill it in with render data table, it creates a table by using... The way to access the proxy to that element that we just created is actually to use data table proxy. So, this is a function that also exists in DT and lets you save a reference to a specific table with a specific ID that was defined somewhere else. In this case, it uses the same ID as the data table output. And you can see that this is basically an example of how you could use proxy. So, we define our data table output, we fill it in with our render table and some, in this case, the iris data set. And then, if we save the reference to the proxy of that element using data table proxy, we can just manipulate it. So, in this case, we have a small checkbox and every time the checkbox changes, we call a specific proxy update function, which in this case is select rows, and we just select or don't select the first five rows of the table.
Exactly. So, one important thing to know here is that different packages also have different update functions. So, it's important to know to figure out these functions whenever you need to use them. Again, documentation is always your friend. So, if you check for this data table proxy function, you'll have a lot of information about all the update functions that come with that proxy object. And internally, proxy actually works very similar to the update functions that I mentioned before. The difference here is that instead of having one update function, we actually have a family of update functions and an object to apply it to.
Custom messages and browser offloading
Again, it's important to check the documentation. You won't always be able to update every single thing. So, it's very important to know the family of proxy update functions that exist. Hopefully, there will be one that does what you need it to do. If not, we'll actually talk about it now, which is custom messages. So, if something doesn't really exist, you can always create it. And this is part of the beauty of Shiny generating that HTML, CSS, and JavaScript that we were talking about before. Because the fact that deep down, you're still working with a web page means that you can use JavaScript by itself to actually augment Shiny even more.
So, what are custom messages? So, when I mentioned the update functions, the update functions are basically a one-way call to the browser where internally, there's some code being run, something that you don't really know, but you know that it's going to trigger something in the browser and update it. What's actually happening down there is similar to how custom messages work. So, you can trigger JavaScript functions from R directly, or you can trigger updates in Shiny from the browser. There's three functions that give you the whole behavior of custom messages. So, in R, this is the send custom message function. In JavaScript, this is the add custom message handler and the on input change functions.
So, I will give you another example of Shiny decisions, my Shiny contest entry from last year. So, one of the things that I had to do was I had the leaflet map that was displaying some markers, it was being updated. That's fine. There's always an update function for leaflet that lets me change the data that way. But I wanted to have this extra behavior of actually changing the saturation of the map, depending on how bad you're actually doing in the game. So, this isn't something that is standard to leaflets. This had to be custom built. And this is where custom messages kind of came in.
So, from the JavaScript side, remember, there's the add custom message handler function. And basically, what this does is it lets you run a function whenever a specific ID is triggered in Shiny. So, what I mean by that is that it kind of registers the handler so that it can be triggered from Shiny. So, in this case, I had this update map style handler that will call this update map style function. And the update map function will just create some CSS that kind of gives you that filter that gets a bit more evident whenever your scores are very low in the game, or it goes up whenever and goes back to the normal colors whenever you're doing better. So, now we have some JavaScript behavior, and we have a Shiny handler to trigger that behavior. So, on the R side, we just need to send the custom message for the same ID with whatever values we want to pass to the JavaScript function. So, two simple functions, and we already have a leaflet behavior that doesn't really exist before.
As a very small proof of concept, the way this is actually working is that from R, the way the all flow works is that from R, you're sending a custom message. That custom message is looking for a specific handler. That handler is triggering a specific JavaScript function. That's it. There's no magic here. So, now we can, from Shiny, trigger things in JavaScript.
But what happens if we want to do the opposite thing? So, we actually want to, and I just realized these are actually braided. So, the first one is actually JavaScript. The second one is actually Shiny. So, if we have a JavaScript function that's doing some work in the browser, how do we actually tell Shiny that something is happening, that something happened? So, we can use this set input value. So, this set input value works very similar. It has an ID and a message, and what it does is that it sends a message to Shiny that can be observable in the list of inputs in the Shiny session. So, this means that our input JS value will actually be changed into the message that we pass from JavaScript. So, we can just observe it, do whatever we need. So, now we can pass messages from the browser back to Shiny.
And another example of something that uses this kind of behavior in the same application. So, here I was observing whenever the user made a choice, and then I was just sending, using this set input value so that I could observe it in Shiny and actually know that something changed.
So, why are these important when it comes to speeding up Shiny? So, because these allow you to actually pass a lot of the, to run whatever you want directly in the browser. This means that you can free your Shiny process to actually do only the important things. So, if you have very small changes that are just UI changes, and don't really need to go to the server, this is one way that you can trigger them. It also means that if you have some kind of code that already exists in JavaScript, you can actually just trigger that code instead of having to rebuild it in Shiny. So, this speeds up not only Shiny itself, but actually the Shiny development, because you won't need to just redo the same function again in a different language. It also allows you to extend these update functions that we talked about before, simply because there's no need to write a corresponding Shiny function, a corresponding update function, if this is something very specific to your application.
It also allows you to extend these update functions that we talked about before, simply because there's no need to write a corresponding Shiny function, a corresponding update function, if this is something very specific to your application.
So, it's very easy to get started and very powerful. Of course, keep in mind that you will need to know a bit of JavaScript, at least to get started. But if you don't know it by now, I definitely recommend you to at least know some of the basics, because it's very, very powerful when it comes to Shiny. And that's it from my part. Thank you. If you have any questions, just reach out.
