Layout, themes, HTML

Single page layout

Page with sidebar

The following app that use ui.panel_sidebar() and ui.panel_main() provides a similar-looking app the Mastering Shiny book example.

examples/action-layout/sidebar/app-panel.py
from shiny import App, ui, render
import numpy as np
from matplotlib import pyplot as plt

app_ui = ui.page_fluid(
    ui.panel_title("Central limit theorem"),
    ui.layout_sidebar(
        ui.panel_sidebar(
            ui.input_numeric("m", "Number of samples:", 2, min=1, max=100),
        ),
        ui.panel_main(
            ui.output_plot("hist"),
        ),        
    ),
)

def server(input, output, session):
    @render.plot
    def hist():
        means = [np.mean(np.random.uniform(size=input.m())) for _ in range(10000)]

        fig, ax = plt.subplots()
        ax.hist(means, bins=20)
        return fig
    
app = App(app_ui, server)
Note

ui.panel_sidebar() and ui.panel_main() have been deprecated since 2023-10-30 release. Per recommendation in the change log, in ui.layout_sidebar() call, I used sidebar() as sidebar arguement and passed main panel contents directly to args.

ui.panel_sidebar() and ui.panel_main() have been deprecated since 2023-10-30 release, and the following code is based on newly recommended sidebar implementation.

examples/action-layout/sidebar/app.py
from shiny import App, ui, render
import numpy as np
from matplotlib import pyplot as plt

app_ui = ui.page_fluid(
    ui.panel_title("Central limit theorem"),
    ui.layout_sidebar(
        ui.sidebar(
            ui.input_numeric("m", "Number of samples:", 2, min=1, max=100)
        ),
        ui.output_plot("hist"),
    ),
)

def server(input, output, session):
    @render.plot
    def hist():
        means = [np.mean(np.random.uniform(size=input.m())) for _ in range(10000)]

        fig, ax = plt.subplots()
        ax.hist(means, bins=20)
        return fig
    
app = App(app_ui, server)

Exercises

  1. Re-create sidebar layout app’s appearance using ui.row() and ui.column().
solutions/action-layout/recreate-sidebar/app.py
from shiny import App, ui, render
import numpy as np
from matplotlib import pyplot as plt

app_ui = ui.page_fluid(
    ui.panel_title("Central limit theorem"),
    ui.row(
        ui.column(4,
            ui.input_numeric("m", "Number of samples:", 2, min=1, max=100),
        ),
        ui.column(8,
            ui.output_plot("hist"),
        ),
    ),
)

def server(input, output, session):
    @render.plot
    def hist():
        means = [np.mean(np.random.uniform(size=input.m())) for _ in range(10000)]

        fig, ax = plt.subplots()
        ax.hist(means, bins=20)
        return fig

app = App(app_ui, server)
Note

This app approximately recreates appearance of an old-style app that uses ui.panel_sidebar() and ui.panel_main(), but not a new-style app that uses ui.sidebar().

  1. Modify the central limit theorem app to put the sidebar on the right instead of the left.
solutions/action-layout/right-sidebar/app.py
from shiny import App, ui, render
import numpy as np
from matplotlib import pyplot as plt

app_ui = ui.page_fluid(
    ui.panel_title("Central limit theorem"),
    ui.layout_sidebar(
        ui.sidebar(
            ui.input_numeric("m", "Number of samples:", 2, min=1, max=100),
            position='right',
        ),
        ui.output_plot("hist"),
    ),
)

def server(input, output, session):
    @render.plot
    def hist():
        means = [np.mean(np.random.uniform(size=input.m())) for _ in range(10000)]

        fig, ax = plt.subplots()
        ax.hist(means, bins=20)
        return fig
    
app = App(app_ui, server)
  1. Create multirow layout app with two plots
solutions/action-layout/multirow/app.py
from shiny import App, ui, render, reactive
import numpy as np
from matplotlib import pyplot as plt

app_ui = ui.page_fluid(
    ui.panel_title("Central limit theorem"),
    ui.row(
        ui.column(6, 
            ui.output_plot("hist"),
        ),
        ui.column(6,
            ui.output_plot("freqploy"),
        ),
    ),
    ui.row(
        ui.input_numeric("m", "Number of samples:", 2, min=1, max=100),
    ),
)

def server(input, output, session):
    @reactive.calc
    def means():
        return [np.mean(np.random.uniform(size=input.m())) for _ in range(10000)]

    @render.plot
    def hist():
        fig, ax = plt.subplots()
        ax.hist(means(), bins=20)
        return fig
    
    @render.plot
    def freqploy():
        counts, bins = np.histogram(means(), bins=20)
        return plt.plot(bins[:-1], counts)
    
app = App(app_ui, server)

Multipage layouts

Tabsets

examples/action-layout/tabset-ui/app.py
from shiny import App, ui

app_ui = ui.page_fluid(
    ui.navset_tab(
        ui.nav_panel("Import data",
            ui.input_file("file", "Data", button_label="Upload..."),
            ui.input_text("delim", "Delimiter (leave blank to guess)", ""),
            ui.input_numeric("skip", "Rows to skip", 0, min=0),
            ui.input_numeric("rows", "Rows to preview", 10, min=1),
        ),
        ui.nav_panel("Set parameters"),
        ui.nav_panel("Visualise results"),
    )
)

def server(input, output, session):
    ...

app = App(app_ui, server)
Note

ui.nav() has been deprecated since 2023-12-18 in favor of ui.nav_panel().

examples/action-layout/tabset-server/app.py
from shiny import App, ui, render

app_ui = ui.page_fluid(
    ui.layout_sidebar(
        ui.sidebar(
            ui.output_text("panel"),
        ),
        ui.navset_tab(
            ui.nav_panel("panel 1", "one"),
            ui.nav_panel("panel 2", "two"),
            ui.nav_panel("panel 3", "three"),
            id="tabset",
        ),
    ),
)

def server(input, output, session):
    @render.text
    def panel():
        return f"Current panel: {input.tabset()}"

app = App(app_ui, server)

Themes

Shiny themes

Use {shinyswatch} package

examples/action-layout/shiny-theme/app.py
from shiny import App, ui
import shinyswatch

app_ui = ui.page_fluid(
    shinyswatch.theme.darkly(),
    ui.layout_sidebar(
        ui.sidebar(
            ui.input_text("txt", "Text input:", "text here"),
            ui.input_slider("slider", "Slider input:", min=1, max=100, value=30),
        ),
        ui.h1("Theme: darkly"),
        ui.h2("Header 2"),
        ui.p("Some text"),
    ),
)

app = App(app_ui, None)

Plot themes

Important

I could not find a way to make a theme to be consistent between app and plot. I hope to come back this topic once I become to know how to apply bottswatch theme to plot.

Under the hood

Let us first define CSS class in file:

css/my-style.css
.my-class {
    font-size: 3rem;
    background-color: pink;
    font-weight: bold
  }

Then, let us write HTML with the CSS and add to the UI by using ui.HTML().

examples/action-layout/html-css/app.py
from shiny import App, ui

app_ui = ui.page_fluid(
    ui.include_css("css/my-style.css"),
    ui.HTML(r"""
        <h1>This is a heading</h1>
        <p class="my-class">This is some text!</p>
        <ul>
            <li>First bullet</li>
            <li>Second bullet</li>
        </ul>        
        """)
)

app = App(app_ui, None)
Note

ui.include_css() is used to add CSS files.

You can use the HTML helper that Shiny provides.

examples/action-layout/html-helper/app.py
from shiny import App, ui

app_ui = ui.page_fluid(
    ui.include_css("css/my-style.css"),
    ui.h1("This is heading"),
    ui.p("This is some text", class_="my-class"),
    ui.tags.ul(
        ui.tags.li("First bullet"),
        ui.tags.li("Second bullet"),
    ),
)

app = App(app_ui, None)
Important

When applying a CSS class, note that the parameter name class_ includes underscore _. Parameter name without underscore will throw an error.

Note

For the most important elements tag functions h1() and p(), Shiny’s ui module provides a top level functions. Others can be asccessed via tags submodule.

An example of interweaving Shiny component into a custom structure.

examples/action-layout/html-shiny-component/app.py
from shiny import App, ui, render

app_ui = ui.page_fluid(
    ui.p(
        "You made ",
        ui.tags.b("$", ui.output_text("amount", inline=True)),
        " in the last ",
        ui.output_text("days", inline=True),
        " days ",
    ),
)

def server(input, output, session):
    @render.text
    def amount():
        return 249
    
    @render.text
    def days():
        return 7

app = App(app_ui, server)
Note

Call ui.output_text() with inline=True to display the output text inline with other elements.