A spinner tells the user to wait. A skeleton shows the user what is coming: bars for a bar chart, rows for a table, lines for text. The page does not look empty, and the eye of the user can go to the place where the content will be.
Install
bones is not on CRAN yet. Install it from GitHub:
# install.packages("remotes")
remotes::install_github("tenmeh/bones")Wrap an output
Put withBones() around an output in the UI. That is
all:
library(shiny)
library(bones)
ui <- fluidPage(
withBones(plotOutput("chart")),
withBones(tableOutput("results"), rows = 8)
)bones finds the shape from the output. A
plotOutput() gets columns on an axis, a
tableOutput() gets a header row and body rows, and a
textOutput() gets lines of text. A table from DT,
reactable, gt or rhandsontable gets a shape that copies that package.
The Shapes page shows each one.
Wrap every output at once
bones_auto() looks through a UI and puts each output in
withBones(). Give it the whole page, or only a part of
it:
ui <- bones_auto(fluidPage(
plotOutput("chart"),
tableOutput("results"),
withBones(plotOutput("trend"), type = "line")
))Each output gets the shape of its kind. An output that is in
withBones() already keeps its own wrapper, so wrap one
output yourself to give it a type or other options.
Arguments after the UI go to each wrapper, and exclude
names outputs to leave alone:
ui <- bones_auto(
fluidPage(plotOutput("chart"), plotOutput("small")),
stale = FALSE,
exclude = "small"
)An inline output, such as
textOutput("n", inline = TRUE), is left alone, because a
wrapper would break its line. A renderUI() makes its
outputs later, so bones_auto() does not see them. Call
bones_auto() on the UI inside that renderUI()
too.
Name the shape when bones cannot find it
A plotOutput() only says that a plot will be there.
renderPlot() decides the kind of chart later, on the
server. So name the kind with type:
withBones(plotOutput("trend"), type = "line")
withBones(plotOutput("fit"), type = "scatter")A uiOutput() can hold anything, so give it a
type too:
The page does not move
Until the content arrives, the placeholder keeps the height of the
output. If the output sets a height, as plotOutput() does,
the placeholder uses it. If not, the height comes from
rows, lines or n, or from
height:
withBones(uiOutput("summary"), type = "text", lines = 5)
withBones(uiOutput("map"), type = "plot", height = 500)When the content arrives, the content sets the height, and no gap stays under it.
The browser also stores the real height of the content. On the next
visit, the placeholder keeps that height in place of the estimate, so
the page does not move at all. Turn this off with
remember = FALSE.
A fast load does not flicker
The skeleton waits for 300 ms before it appears, so a load that ends
sooner shows nothing. Once it has appeared, it stays for at least 500
ms, so it never flashes for one frame. Change the times with
delay and min_time:
withBones(plotOutput("chart"), delay = 150, min_time = 400)The second load keeps the old content
On the first load, the user sees a skeleton. When the output calculates again, the old content stays on the screen, dimmed, until the new content arrives. The user is possibly in the middle of reading it, and grey boxes in its place would remove information.
To show the skeleton again on each load, set
stale = FALSE:
withBones(plotOutput("chart"), stale = FALSE)Set the look for the whole app
bones_defaults() sets the animation, the colours and the
speed for the session. An argument to withBones() is
stronger than a default:
bones_defaults(
animation = "pulse", # "wave" (the default), "pulse", "cascade", "sweep" or "none"
radius = "0.5rem",
speed = 2,
stale = FALSE
)The default colours come from the theme of the page: the text colour of the bslib theme, made faint. A skeleton thus takes the tint of a branded theme, and turns light in a dark theme, with no configuration. With no Bootstrap 5 theme, the colours are neutral greys. Users who ask their system to reduce motion get no animation.
A placeholder with no output
bones_skeleton() makes a skeleton on its own, for a
place where there is no Shiny output:
output$panel <- renderUI({
if (is.null(data())) return(bones_skeleton("table", rows = 5))
tableOutput("results")
})