User interfaces

With Shiny, you can create a wide variety of user interfaces (UI), including dashboards. Here, we’ll use the following dashboard as motivation to learn about some important UI components (e.g., cards, value boxes) and layouts (e.g., columns).

A Shiny dashboard with visuals for exploring restaurant tips (see here for the code).
More UI design inspiration

See the gallery, layouts, and components for more UI design inspiration.

Basic dashboard

Before we walk through a more sophisticated dashboard, consider this basic dashboard with a header (i.e., page title) and a sidebar layout. In the sidebar, there are a couple inputs for getting different views of the data, and in the main content area, is a plotly output. That output is also placed in a card to give it some depth and the ability to go full screen. The card isn’t critical when there is only one output, but they come highly recommended when there are multiple outputs to display.

#| standalone: true
#| components: [viewer, editor]
#| layout: vertical
#| viewerHeight: 500
from shiny.express import input, render, ui
from shinywidgets import render_plotly

ui.page_opts(title="Penguins dashboard", fillable=True)

with ui.sidebar():
    ui.input_selectize(
        "var", "Select variable",
        ["bill_length_mm", "bill_depth_mm", "flipper_length_mm", "body_mass_g", "year"]
    )
    ui.input_numeric("bins", "Number of bins", 30)

with ui.card(full_screen=True):
    @render_plotly
    def hist():
        import plotly.express as px
        from palmerpenguins import load_penguins
        return px.histogram(load_penguins(), x=input.var(), nbins=input.bins())

Sophisticated dashboard

Now let’s work up to a more sophisticated dashboard by walking through components and layouts that are useful for dashboards step-by-step.

Cards

Cards are great for visually grouping together related content, and it’s best practice to place related components together in a card. Here you’ll also have an opportunity to add a header, footer, add full_screen capability, and more. As we’ll see later, cards are also useful for making outputs stand out from one another when there are multiple outputs to display.

#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 300
from shiny.express import render, ui

with ui.card(full_screen=True):
    ui.card_header("A card with a header")
    @render.plot
    def plot():
        import matplotlib.pyplot as plt
        return plt.scatter([1, 2, 3], [4, 5, 6])

with ui.card():
    ui.markdown("Another card with some _markdown_.")

Value boxes

Value boxes are great for highlighting important summaries. They require at least two values (the title and value) and also support a showcase argument for adding a visual representation of the value. The showcase argument can technically be any UI element, but is often a faicons (i.e., fontawesome) icon.

#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 300
from shiny.express import render, ui
from faicons import icon_svg as icon

with ui.value_box(showcase=icon("piggy-bank")):
    "Total sales"
    "$1,000,000"

with ui.value_box(showcase=icon("person")):
    "Total customers"
    @render.ui
    def customers():
        return f"{1000:,}"
Note

Under the hood, value boxes are built on cards, so you can leverage similar options like full_screen.

Multi-column layout

Create a multi-column layout based on a 12-column grid system by using ui.layout_columns(). By default, an intelligent equal-width layout is created, but each column width can be specified (in units of 1/12) using col_widths:

#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 200
from shiny.express import ui

with ui.layout_columns(col_widths=[6, 6, 12]):
    with ui.card():
        "Card 1"
    with ui.card():
        "Card 2"
    with ui.card():
        "Card 3"
Responsive layout

By default, col_widths doesn’t apply on smaller width (i.e., mobile) screens (in that case, columns go full-width). However, col_widths also accepts a dictionary of column widths for different screen sizes (e.g., col_widths=dict(sm=6, md=4) yields 2 columns on small screens and 3 columns on medium (or larger) screens).

Filling layout

Set ui.page_opts(fillable=True) to encourage content to fill the screen. Many of Shiny’s layouts and components automatically fill the screen when this option is set, which is often desirable for dashboards, but may not be what you want for things like value boxes or cards with a textual description. You can override the filling behavior on a per-component basis by setting fill=False or by specifying a height:

#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 300
from shiny.express import ui

ui.page_opts(fillable = True)

with ui.layout_column_wrap(fill=False):
    with ui.value_box():
        "Value box"
        "$1,000,000"

    with ui.value_box():
        "Value box"
        "$1,000,000"

with ui.card():
    "Card that fills remaining space..."
Resizable viewer

Did you know the app viewer above is resizable? Try resizing it to see how the layout responds (the card fills the remaining space).

Tooltips and popovers

Tooltips and popovers are a useful means for both displaying and interacting with additional information in a non-obtrusive way. Tooltips are shown on hover, whereas popovers are shown on click, making them more suitable for interactive content like inputs. In the actual dashboard, we’ll leverage a popover to effectively add a toolbar with additional inputs controls to card headers.

#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 100
from shiny.express import ui
from faicons import icon_svg as icon

"Hover this icon: "

with ui.tooltip():
    icon("circle-info")
    "Tooltip message"

ui.br()

"Click this icon: "

with ui.popover(title="Popover title"):
    icon("circle-info")
    "Popover message"

All together now

Let’s put it all together to create a dashboard for exploring restaurant tipping data.

Here we use a sidebar to hold our “global” inputs, and place outputs in cards. These cards are laid out column-wise, and value boxes highlight the most important numbers. Finally, inputs that are specific to each are placed in a popover so that they are unobtrusive and don’t distract the user from the main application content.

#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 800

import faicons as fa
import plotly.express as px
from shinywidgets import render_plotly

from shiny import reactive, render, req
from shiny.express import input, ui

# Load data and compute static values
tips = px.data.tips()
bill_rng = (min(tips.total_bill), max(tips.total_bill))

# Add page title and sidebar
ui.page_opts(title="Restaurant tipping", fillable=True)
with ui.sidebar(open="desktop"):
    ui.input_slider("total_bill", "Bill amount", min=bill_rng[0], max=bill_rng[1], value=bill_rng, pre="$")
    ui.input_checkbox_group("time", "Food service", ["Lunch", "Dinner"], selected=["Lunch", "Dinner"], inline=True)
    ui.input_action_button("reset", "Reset filter")

# Add main content
ICONS = {
    "user": fa.icon_svg("user", "regular"),
    "wallet": fa.icon_svg("wallet"),
    "currency-dollar": fa.icon_svg("dollar-sign"),
    "gear": fa.icon_svg("gear")
}

with ui.layout_columns(fill=False):

    with ui.value_box(showcase=ICONS["user"]):
        "Total tippers"
        @render.express
        def total_tippers():
            tips_data().shape[0]

    with ui.value_box(showcase=ICONS["wallet"]):
        "Average tip"
        @render.express
        def average_tip():
            d = tips_data()
            if d.shape[0] > 0:
                perc = d.tip / d.total_bill
                f"{perc.mean():.1%}"

    with ui.value_box(showcase=ICONS["currency-dollar"]):
        "Average bill"
        @render.express
        def average_bill():
            d = tips_data()
            if d.shape[0] > 0:
                bill = d.total_bill.mean()
                f"${bill:.2f}"


with ui.layout_columns(col_widths=[6, 6, 12]):

    with ui.card(full_screen=True):
        ui.card_header("Tips data")
        @render.data_frame
        def table():
            return render.DataGrid(tips_data())

    with ui.card(full_screen=True):
        with ui.card_header(class_="d-flex justify-content-between align-items-center"):
            "Total bill vs tip"
            with ui.popover(title="Add a color variable", placement="top"):
                ICONS["gear"]
                ui.input_radio_buttons(
                    "scatter_color", None,
                    ["none", "sex", "smoker", "day", "time"],
                    inline=True
                )

        @render_plotly
        def scatterplot():
            color = input.scatter_color()
            return px.scatter(
                tips_data(),
                x="total_bill",
                y="tip",
                color=None if color == "none" else color,
                trendline="lowess"
            )

    with ui.card(full_screen=True):
        with ui.card_header(class_="d-flex justify-content-between align-items-center"):
            "Tip percentages"
            with ui.popover(title="Add a color variable"):
                ICONS["gear"]
                ui.input_radio_buttons(
                    "tip_perc_y", "Split by:",
                    ["sex", "smoker", "day", "time"],
                    selected="day",
                    inline=True
                )

        @render_plotly
        def tip_perc():
            from ridgeplot import ridgeplot
            # Must make a copy of this pandas dataframe before we mutate it!
            # See https://shiny.posit.co/py/docs/reactive-mutable.html
            dat = tips_data().copy()
            dat["percent"] = dat.tip / dat.total_bill
            yvar = input.tip_perc_y()
            uvals = dat[yvar].unique()

            samples = [
                [ dat.percent[dat[yvar] == val] ]
                for val in uvals
            ]

            plt = ridgeplot(
                samples=samples, labels=uvals, bandwidth=0.01,
                colorscale="viridis", colormode="row-index"
            )

            plt.update_layout(
                legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="center", x=0.5)
            )

            return plt


# --------------------------------------------------------
# Reactive calculations and effects
# --------------------------------------------------------

@reactive.calc
def tips_data():
    bill = input.total_bill()
    idx1 = tips.total_bill.between(bill[0], bill[1])
    idx2 = tips.time.isin(input.time())
    return tips[idx1 & idx2]

@reactive.effect
@reactive.event(input.reset)
def _():
    ui.update_slider("total_bill", value=bill_rng)
    ui.update_checkbox_group("time", selected=["Lunch", "Dinner"])

## file: requirements.txt
ridgeplot==0.1.25

Next steps

If you’d like to start developing Shiny apps locally, see the next section on workflow.

Otherwise, to keep learning more about some of the topics covered here, see the following:

  • User interfaces: Learn more about the Shiny’s UI tooling.
  • Reactivity: Learn how to manage code execution in response to user input.
  • Jupyter Widgets: Learn how all about Shiny’s Jupyter Widgets integration.