Designing Gadget UI
By: Joe Cheng
Shiny 0.13 introduces the concept of Shiny Gadgets, which are Shiny apps that are designed to be used directly from the R console (or invoked as an RStudio Add-in) to provide helpful functionality while you’re analyzing data or coding.
While technically, any kind of Shiny UI could be used for a Shiny Gadget, we’ve created a
miniUI package that we think is particularly well suited for Gadget use. We recommend that you start with
miniUI based UI for your gadget, unless you have a specific reason not to.
You can install
miniUI from GitHub using the
miniUI is inspired by the excellent Ratchet CSS library for mobile web apps.
Each miniUI interface starts with
miniPage is fundamentally different than the traditional
shiny::fluidPage in that
miniPage tries to exactly fill its containing browser window or frame. Other than setting up an empty container,
miniPage does nothing but display its children; it has no visible UI of its own.
miniPage(..., title = NULL, theme = NULL)
Unfortunately, you can’t put just any UI object directly inside a
miniPage. This is because
miniPage is a special kind of container called a “flex box”; any of its direct children need to be aware of that and include some extra CSS properties to indicate how they’re supposed to interact with the flex box. In practice, the only UI objects you’re likely to put directly inside
miniContentPanel is especially important, as it is capable of hosting non-flex-aware UI objects. Think of it as a bridge between the flex box style layout, and every other kind of UI object. We’ll go into more detail about
Almost every Gadget will want to use
gadgetTitleBar as the first child of
miniPage. It adds a header that displays the name of the gadget, plus “Cancel” and “Done” buttons.
gadgetTitleBar(title, left = miniTitleBarCancelButton(),
right = miniTitleBarButton("done", "Done", primary = TRUE))
As noted in the example above, you need to handle
input$done, but Cancel is handled automatically by
You can customize or omit the Cancel and Done buttons by passing
right arguments to
gadgetTitleBar. For example, to replace the text “Done” with “Accept”:
miniTitleBar, which is the same as
gadgetTitleBar but it omits the Cancel and Done buttons by default. It’s mostly useful if you want to use
miniUI for a regular Shiny app, not a Gadget.)
As we noted earlier, think of
miniContentPanel as the “bridge” between the strange flex box layout that
miniPage creates, and normal HTML elements and controls that aren’t flex box aware.
miniContentPanel(..., padding = 15, scrollable = TRUE)
Most simply, you can put a plot inside of
We specify a height of 100% because the default is a fixed 400 pixels. That will technically work, but having the 100% height means the plot will exactly fill the space allocated to the
miniContentPanel, which is much nicer.
What if you have two plots? This doesn’t work:
You end up with a scrollable mess, exactly what we were trying to avoid. You could set each height to 50%, and that would probably work. But a more robust and general solution is available in the form of
fillRow/fillCol, explained below.
miniContentPanel needs to have children that scale up to fill it. You can put other types of HTML content in
miniContentPanel, such as a fixed-height plot, a regular paragraph or two of text, or a table of data. By default, if a miniContentPanel’s contents are bigger than its own dimensions, it will display horizontal and/or vertical scrollbars. You can choose never to display scrollbars with the argument
miniContentPanel adds some whitespace padding around its content. You can modify the amount of padding with the
padding argument; set
padding = 0 to eliminate the padding altogether, or set specific pixel values for the individual sides with
padding = c(top, right, bottom, left).
tabsetPanel is extremely useful for giving your app multiple screens. It isn’t suited to small window sizes, though; its tabs take up too much space. Enter
miniTabstripPanel, which is coded in a similar way, but has a much different appearance.
miniTabstripPanel(..., id = NULL, selected = NULL, between = NULL)
miniTabPanel(title, ..., value = title, icon = NULL)
(See the full example at https://github.com/rstudio/miniUI/blob/master/examples/tabs.R)
The main difference between using
tabPanel is that the former requires you to provide an icon for every tab, and you’ll generally need to wrap the tab contents in
miniContentPanel. Anything that works as a direct child of
miniPage also works as a direct child of
miniButtonBlock; all other UI elements need to be wrapped in
miniButtonBlock inserts a full-width row for parking one or more
actionButtons. When used within a button block, action buttons will evenly divide the space among themselves; a single button will span the entire available width, two buttons will each take 50% of the width, and so on.
miniButtonBlock(..., border = "top")
(See the full example at https://github.com/rstudio/miniUI/blob/master/examples/simple.R)
From a UX perspective, we recommend you generally put button blocks at or near the bottom of the page. (If you decide to put button blocks at the top anyway, be sure to pass
border = "bottom" to ensure that a grey border is drawn underneath the button block, rather than above it.)
If your app uses
miniTabstripPanel, the tabstrip should be at the bottom of the screen, with the button block just above it. If the button block in question should only appear when a specific tab is selected, then put the button block inside the
miniTabPanel itself. But if the button block should appear regardless of tab selection, then you need to pass it as the
between argument of
These two functions are not in the
miniUI package, but in the core
shiny package itself. They are useful for dividing an area up into rows and columns that grow and shrink proportionally to fill the available space, as opposed to
col which don’t grow and shrink vertically.
fillRow(..., flex = 1, width = "100%", height = "100%")
fillCol(..., flex = 1, width = "100%", height = "100%")
When called with child elements and no other arguments,
fillCol will divide the available space (horizontally and vertically, respectively) evenly.
fillRow(a, b, c)
Note that while
fillCol will create containers for its children that fill the available space, just like
miniContentPanel it can’t force the child elements to actually fully use that space. Again,
plotOutput needs a
height = "100%" argument, otherwise it will fix its height at 400 pixels, and the same is true for most HTML widgets.
You can nest
fillCol within each other to create more complicated layouts:
Controlling weights with
When the simple “divide available space evenly between children” behavior is not enough, you can pass a
flex argument to
fillCol to have more control.
flex argument should be a numeric vector that contains one element for each child you pass to
fillCol. The values you pass will determine how the available space will be apportioned. A child with a flex value of
2 will get twice as much space as a child with flex value
fillRow(flex = c(1, 2), a, b)
You can also assign a child element the flex value of
NA; this means, use the natural size of the element. If a
fillCol contains one or more
NA flex values, then space will be assigned to those children first, and the remaining space will then be divided among the children with non-
NA flex values.
NA flex value feature is particularly valuable when used with
fillCol to park a fixed-height input (like a slider) above or below a plot (or two) that fills the rest of the space. When the gadget is resized, the slider keeps the same height while the plots scale, as shown in the animation below.
For more on this topic, see the following resources:
We love it when R users help each other, but RStudio does not monitor or answer the comments in this thread. If you'd like to get specific help, we recommend the RStudio Community as well as the Shiny Discussion Forum for in depth discussion of Shiny related questions and How to get help article for a list of the best ways to get help with R code.comments powered by Disqus