Tutorial: Build a Rhino application using shiny.fluent
Source:vignettes/st-shiny-fluent-and-rhino.Rmd
st-shiny-fluent-and-rhino.RmdHello World with rhino and
shiny.fluent
Getting started with our development environment
Let us first install and create our rhino project
structure. Note: Ensure Rhino v1.4.0 or later is installed for this
tutorial.
To install rhino, please run the following in the R
console:
# In R console
install.packages("rhino")
Now, initialize a new rhino application. You can do it either by
using RStudio Wizard or a function rhino::init().
More details on how to create a Rhino app can be found in the Rhino tutorial.
You will notice that the working directory now has a proper folder structure along with some other files.
Rhino relies on the renv package for
dependency management. To add the shiny.fluent package to
our project, first, we need to install it.
Additionally, we will be also using the dplyr,
ggplot2, plotly and tibble
package for this application. To save us time in the tutorial we will
install them all here. Please note that for every new package you add,
you will need to install them, add them to dependencies.R,
and run renv::snapshot()
to update the renv.lock file.
# In R console
renv::install(c("dplyr", "ggplot2", "plotly", "shiny.fluent", "tibble"))
Now that the packages are installed, head over to the
dependencies.R file and add shiny.fluent to it
along with the others as shown below.
# dependencies.R
# This file allows packrat (used by rsconnect during deployment) to pick up dependencies.
library(dplyr)
library(ggplot2)
library(plotly)
library(rhino)
library(shiny.fluent)
library(tibble)
Upon saving the modified file, please take a snapshot of the
dependencies so that the renv.lock file is updated.
# In R console
renv::snaphot()
If you now check the renv.lock file, you will see it is
updated with the shiny.fluent and other packages.
Our first development on the app
Upon viewing the default app/main.R file, you will
notice that the code does not have any usage of
shiny.fluent package to generate the UI. So, let us
introduce shiny.fluent and use it in our code to generate
some text!
# app/main.R
box::use(
shiny[moduleServer, NS],
shiny.fluent[fluentPage, Text],
)
#' @export
ui <- function(id) {
ns <- NS(id)
fluentPage(
Text("Sales Data Dashboard", variant = "xxLarge")
)
}
#' @export
server <- function(id) {
moduleServer(id, function(input, output, session) {
})
}
The above code uses fluentPage
and Text
from shiny.fluent package to render the UI and the sentence
- “Sales Data Dashboard”. You will also notice the NS(id)
used in the code to return a namespace function, which has been saved as
ns and will be invoked later in the shiny module.
Now when you run the app, you should be seeing this -

Note: If you check the built-in testcases once changes are
introduced, running rhino::test_r() will give you errors.
To avoid failed GitHub Actions for now (in case you are pushing your
code to GitHub), you may empty the test cases present in
tests/testthat/test-main.R.
Show Data in a Table via a rhino module
So, in this section, we will introduce a table to our application
that displays random imaginary sales deals. The dataset comes from the
shiny.fluent package itself - fluentSalesDeals.
Creating the module
In the app/view/ folder, let us create a new file and
name it datatable.R
This app/view/datatable.R file will be used to display
the data table on the UI and we will call this module in the
app/main.R file once done. Here is the code
# app/view/datatable.R
box::use(
shiny[div, moduleServer, NS, renderUI, uiOutput],
shiny.fluent[DetailsList, Text, fluentSalesDeals],
)
#' @export
ui <- function(id) {
ns <- NS(id)
div(
Text("A randomly generated dataset of imaginary sales deals", variant = "large"),
uiOutput(ns("datatable"))
)
}
#' @export
server <- function(id) {
moduleServer(id, function(input, output, session) {
output$datatable <- renderUI({
DetailsList(items = fluentSalesDeals)
})
})
}
fluentSalesDeals is a dataset available with the
shiny.fluent package.
So, here we are using DetailsList
function in the server side to render the table on the UI.
Calling the module
Our shiny module app/view/datatable.R is ready, but
unless we call it in the app/main.R file, we won’t see any
changes on the app. Let us thus go to app/main.R file and
edit it with the following code -
# app/main.R
box::use(
shiny[moduleServer, NS],
shiny.fluent[fluentPage, Text],
)
box::use(
app/view/datatable,
)
#' @export
ui <- function(id) {
ns <- NS(id)
fluentPage(
Text("Sales Data Dashboard", variant = "xxLarge"),
datatable$ui(ns("datatable"))
)
}
#' @export
server <- function(id) {
moduleServer(id, function(input, output, session) {
datatable$server("datatable")
})
}
This code in the app/main.R file is based the concept of
Shiny
modules where box
modules in rhino are used.
Notice here that we have called the datatable module by
importing it to our main application file first. It is now, that the
main module will be able to use exported functions from
app/view/datatable.R module.
Now run the app. You should be seeing something like this -

Show informative columns with meaningful names
We will no more display all the columns that come with the dataset.
Instead, we will display the
"rep_name", "date", "deal_amount", "city", "is_closed"
columns.
But for that we need to make the names more readable. So, we will
assign proper names for every fields using the tibble
package.
columns <- tibble(
fieldName = c("rep_name", "date", "deal_amount", "city", "is_closed"),
name = c("Sales Rep", "Date", "Deal Amount", "City", "Is Closed?")
)
Also, we need to mention in the DetailsList() function
to only display the names we just assigned in the columns
object. Here is how the code gets modified -
# app/view/datatable.R
box::use(
shiny[div, moduleServer, NS, renderUI, uiOutput],
shiny.fluent[DetailsList, Text, fluentSalesDeals],
tibble[tibble],
)
columns <- tibble(
fieldName = c("rep_name", "date", "deal_amount", "city", "is_closed"),
name = c("Sales Rep", "Date", "Deal Amount", "City", "Is Closed?")
)
#' @export
ui <- function(id) {
ns <- NS(id)
div(
Text("A randomly generated dataset of imaginary sales deals", variant = "large"),
uiOutput(ns("datatable"))
)
}
#' @export
server <- function(id) {
moduleServer(id, function(input, output, session) {
output$datatable <- renderUI({
# This issue should be solved in the next `box` release.
DetailsList(items = fluentSalesDeals, columns = columns)
})
})
}
Remember to include tibble inside
box::use(), else the code will not be able to use the
package. Upon running the app with all these changes, you should see the
improved table -

Add a simple toggle input feature for the table with basic styling
Till now, we could only view table with all the data it has. We will now add a functionality to view those rows in the table where value of the “Is Closed?” column is 1 by default and provide a toggle to the user to also include results where the value is 0. The “Is Closed?” column basically indicates whether the deal is closed or not with values 0 and 1 where 0 means False/No and 1 means True/Yes.
We will make use of the dplyr package now. We will use
Toggle.shinyInput
from shiny.fluent package which represents a physical
switch that allows someone to choose between two mutually exclusive
options. For example,“On/Off”,“Show/Hide”. Choosing an option should
produce an immediate result.
In the UI, we include the code for showing the toggle for input -
Toggle.shinyInput(ns("includeOpen"), label = "Include open deals"),
and work on the input provided inside the server section
So, the app/view/datatable.R file now is modified to
-
# app/view/datatable.R
box::use(
dplyr[filter],
shiny[div, moduleServer, NS, reactive, renderUI, uiOutput],
shiny.fluent[DetailsList, Text, Toggle.shinyInput, fluentSalesDeals],
tibble[tibble],
)
columns <- tibble(
fieldName = c("rep_name", "date", "deal_amount", "city", "is_closed"),
name = c("Sales Rep", "Date", "Deal Amount", "City", "Is Closed?")
)
#' @export
ui <- function(id) {
ns <- NS(id)
div(
Text("A randomly generated dataset of imaginary sales deals", variant = "large"),
div(
class = "ms-depth-4",
Toggle.shinyInput(ns("includeOpen"), label = "Include open deals")
),
uiOutput(ns("datatable")),
)
}
#' @export
server <- function(id) {
moduleServer(id, function(input, output, session) {
filtered_deals <- reactive({
fluentSalesDeals |> filter(
is_closed | input$includeOpen
)
})
output$datatable <- renderUI({
DetailsList(items = filtered_deals(), columns = columns)
})
})
}
This code is being used to determine whether open deals (i.e. deals
that have not been closed) should be included in the filtered data or
not. The | symbol between the two conditions is the logical
OR operator, which means that the filter() function will
return all rows where either is_closed is TRUE or
input$includeOpen is TRUE.
Also, please notice that we have added an additional style to the
class ms-depth-4 in the app/style/main.scss
file. ms-depth-X
classes are a part of Microsoft’s Fluent UI and provide elevation
(depth) to components.
Here is the app/style/main.scss file now -
// app/style/main.scss
.ms-depth-4 {
display: flex;
flex-wrap: wrap;
padding: 5px;
}
If you try running the application right now, you will not see any
changes. That is because rhino uses minified
app/static/app.min.css for styling. To use it, you will
need to build it using the rhino function -
# in R console
rhino::build_sass()
Please refer to rhino documentation for more details on styling.
So, this is how the app looks and works now!

Improve visuals and layout for the table with elevated cards
We can create cards with elevations just like in Fluent UI documentation,
as we partially covered it above. For creating cards, we can use Stack
from shiny.fluent.
We will also style the data table area. The following code does the following -
- Confines the height and width of the table area.
- Creates an elevated card around the data table area.
# app/view/datatable.R
box::use(
dplyr[filter],
shiny[div, moduleServer, NS, reactive, renderUI, span, uiOutput],
shiny.fluent[DetailsList, Stack, Text, Toggle.shinyInput, fluentSalesDeals],
tibble[tibble],
)
columns <- tibble(
fieldName = c("rep_name", "date", "deal_amount", "city", "is_closed"),
name = c("Sales Rep", "Date", "Deal Amount", "City", "Is Closed?")
)
#' @export
ui <- function(id) {
ns <- NS(id)
div(
Text("A randomly generated dataset of imaginary sales deals", variant = "large"),
div(
class = "ms-depth-4",
Toggle.shinyInput(ns("includeOpen"), label = "Include open deals")
),
Stack(
horizontal = TRUE,
tokens = list(padding = 20, childrenGap = 10),
span(class = "ms-depth-8", uiOutput(ns("datatable")))
)
)
}
#' @export
server <- function(id) {
moduleServer(id, function(input, output, session) {
filtered_deals <- reactive({
fluentSalesDeals |> filter(
is_closed | input$includeOpen
)
})
output$datatable <- renderUI({
DetailsList(items = filtered_deals(), columns = columns)
})
})
}
The Stack element in the code allows for stacking
multiple elements vertically or horizontally.
The horizontal argument is set to TRUE to horizontally stack the two
elements. The tokens argument is a list that specifies the padding and
the gap between the two elements. In this case, the padding is set to 20
and the childrenGap is set to 10. This will add a padding of 20 pixels
and a gap of 10 pixels between the other span of the
Stack element.
The span element has a class attribute set to
ms-depth-8. This is a class provided by the Microsoft
fluent UI that adds a shadow effect to the element, giving it a depth
effect. The uiOutput element is used to render the
DetailsList element that contains the filtered data.
The additional style for the class ms-depth-8 is added
in the app/style/main.scss file. So it now is modified to
-
// app/style/main.scss
.ms-depth-4 {
display: flex;
flex-wrap: wrap;
padding: 5px;
}
.ms-depth-8 {
height: 500px;
overflow: overlay;
width: 50%;
display: inline-block;
}
The Sass code limits the height of the table to 500 pixels, width to
50% of the screen size and allows scrolling through the data. Similar to
the previous usecase, we would again have to repeat the Sass building
process with rhino as the file has been updated.
# in R console
rhino::build_sass()
So, this is how the UI should look now -
Note that we now
we have space beside the table for additional UI component(s).
Create separate rhino modules for toggle input and
datatable
Now that we have successfully introduced a datatable and a toggle input in our application, let us further create different Rhino modules for them!
We already have the datatable module with us. We will thus create one
more module - app/view/toggle.R and make some modifications
in app/view/datatable.
These modules will be called from the main file.
The app/view/toggle.R module
# app/view/toggle.R
box::use(
dplyr[filter],
shiny[div, moduleServer, NS, reactive],
shiny.fluent[Text, Toggle.shinyInput, fluentSalesDeals],
)
#' @export
ui <- function(id) {
ns <- NS(id)
div(
class = "ms-depth-4",
Toggle.shinyInput(ns("includeOpen"), label = "Include open deals")
)
}
#' @export
server <- function(id) {
moduleServer(id, function(input, output, session) {
filtered_deals <- reactive({
fluentSalesDeals |> filter(
is_closed | input$includeOpen
)
})
return(filtered_deals)
})
}
This module displays a toggle input item on the UI which the user can interact with. The styling remains the same as we discussed in the previous sections.
As the user interacts with the toggle input, the data is filtered in the server. This filtered data is returned to the main module.
The modified app/view/datatable.R module
As the toggle module returns the filtered data, it is
used in the datatable module so that the data is displayed
on a table accordingly. Hence, our datatable module is
modified as shown below -
# app/view/datatable.R
box::use(
shiny[moduleServer, NS, reactive, renderUI, uiOutput],
shiny.fluent[DetailsList],
tibble[tibble],
)
columns <- tibble(
fieldName = c("rep_name", "date", "deal_amount", "city", "is_closed"),
name = c("Sales Rep", "Date", "Deal Amount", "City", "Is Closed?")
)
#' @export
ui <- function(id) {
ns <- NS(id)
uiOutput(ns("datatable"))
}
#' @export
server <- function(id, data) {
moduleServer(id, function(input, output, session) {
output$datatable <- renderUI({
DetailsList(items = data(), columns = columns)
})
})
}
Please notice, that inside the server function, the response obtained
from the toggle module earlier is also passed on to be used
in displaying the data in a table.
We are not returning any information from this module to the main module as it’s sole purpose is to display given data in a table with desired columns in an easily readable form.
The modified app/main.R module
Since we modified the modules, app/main.R also has to be
modified.
# app/main.R
box::use(
shiny[moduleServer, NS, span],
shiny.fluent[fluentPage, Stack, Text],
)
box::use(
app/view/datatable,
app/view/toggle,
)
#' @export
ui <- function(id) {
ns <- NS(id)
fluentPage(
Text("Sales Data Dashboard", variant = "xxLarge"),
Text("A randomly generated dataset of imaginary sales deals", variant = "large"),
toggle$ui(ns("toggle")),
Stack(
horizontal = TRUE,
tokens = list(padding = 20, childrenGap = 10),
span(class = "ms-depth-8", datatable$ui(ns("datatable")))
)
)
}
#' @export
server <- function(id) {
moduleServer(id, function(input, output, session) {
data <-toggle$server("toggle")
datatable$server("datatable", data)
})
}
Here we have used the toggle and datatable
modules and called them from the code.
Create a rhino module to render a bar chart with logic
in a helper function
We will now add a bar chart that plots the summarized amount of deals
closed by the sales reps. For this, we will code a helper function and
keep it inside the app/logic folder.
Define the helper function
Go to the app/logic folder and create a new
.R file. Lets name it barchart.R. We shall use
the gglplot2 and plotly packages now (which we
installed in the beginning) for this purpose.
Here is the code -
# app/logic/barchart.R
box::use(
ggplot2[aes, element_text, geom_bar, ggplot, labs, theme, theme_light],
)
#' @export
generate_barchart <- function(filtered_data) {
ggplot(filtered_data, aes(x = rep_name, y = deal_amount, fill = rep_name)) +
geom_bar(stat = "identity") +
labs(x = "Sales Rep Name", y = "Deal Amount") +
theme_light() +
theme(axis.text.x = element_text(angle = 60, hjust = 1))
}
The above code block loads the ggplot2 package and
generates a barchart with the names of the sales reps on the X axis and
the summarized amount of deals on the Y axis.
Create a new rhino module and call the helper
function
Now let us create a new file in app/view folder and name
it plot.R.
This module, like the app/view/datatable.R module, also
obtains the data returned from the toggle module and
renders a barchart using the app/logic/barchart.R
logic.
# app/view/plot.R
box::use(
plotly[plotlyOutput, renderPlotly],
shiny[moduleServer, NS],
)
box::use(
app/logic/barchart[generate_barchart],
)
#' @export
ui <- function(id) {
ns <- NS(id)
plotlyOutput(ns("barchart"))
}
#' @export
server <- function(id, data) {
moduleServer(id, function(input, output, session) {
output$barchart <- renderPlotly({
generate_barchart(data())
})
})
}
Here, we have called the plotly library, and also the
function that we defined in the app/logic folder.
plotlyOutput will help us to render the graph.
Modify the app/main.R module
To view the barchart on the application, we need to use and call it from the main module.
# app/main.R
box::use(
shiny[moduleServer, NS, span],
shiny.fluent[fluentPage, Stack, Text],
)
box::use(
app/view/datatable,
app/view/toggle,
app/view/plot,
)
#' @export
ui <- function(id) {
ns <- NS(id)
fluentPage(
Text("Sales Data Dashboard", variant = "xxLarge"),
Text("A randomly generated dataset of imaginary sales deals", variant = "large"),
toggle$ui(ns("toggle")),
Stack(
horizontal = TRUE,
tokens = list(padding = 20, childrenGap = 10),
span(class = "ms-depth-8", datatable$ui(ns("datatable"))),
span(class = "ms-depth-8", plot$ui(ns("plot")))
)
)
}
#' @export
server <- function(id) {
moduleServer(id, function(input, output, session) {
data <-toggle$server("toggle")
datatable$server("datatable", data)
plot$server("plot", data)
})
}
The code now uses all the other modules that we just
created/modified, creates the cards in the UI using Stack
in a fluentPage from the shiny.fluent package,
calls the modules accordingly and passes on the relevant information to
each other in the server to have a working web application.
Once ready, on running the application, we should see the plotly bar chart rendering successfully and also responding to our interactions with the toggle input button.

Next Steps and Suggested Readings
By now, we have explored rhino and
shiny.fluent packages to an extent, but this is just the
beginning. It is strongly recommended for the reader to further explore
these packages and build great RShiny applications. Congratulations on
coming this far and all the best for the road that lies ahead!
Following are some resources which could be considered for further exploration: