Shiny Express: Basics

Note

This document was written and updated during the development of Shiny Express, and some parts of it may now be out of date.

Please see the official documentation for Shiny Express.

Shiny Express is a new way of writing Shiny apps that is intended to be easier to learn and quicker to write. We think that writing Shiny Express is comparable to Streamlit in terms of how easily you can create an app, but Shiny Express does not limit you the way that Streamlit does – there is a much higher ceiling to what you can do with Shiny Express.

Shiny Express is still Shiny, just with a simpler syntax.

Quickstart

The best way to introduce Shiny Express is by example. Here is a simple “Hello World” app:

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

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

ui.input_slider("n", "N", min=1, max=50, value=30)

@render.code
def txt():
    return f"Hello! n*2 is {input.n() * 2}."

The first thing to notice is that ui is imported from shiny.express, as opposed to being imported from shiny.

The slider input comes from ui.input_slider(), and the text output is a function that’s decorated with @render.code and returns a string.

If you’ve seen traditional Shiny applications (which we’ll now refer to as “Shiny Core” apps), you’ll notice some important differences. Here’s the same app written in Shiny Core form:

#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
from shiny import App, ui, render

app_ui = ui.page_fixed(
    ui.input_slider("n", "N", min=1, max=50, value=30),
    ui.output_code("txt"),
)

def server(input, output, session):
    @render.code
    def txt():
        return f"Hello! n*2 is {input.n() * 2}."

app = App(app_ui, server)

Not only is there significantly less code in the Express version, but there are fewer concepts to (mis)understand or be intimidated by.

Here’s what’s different in the Core app:

  • from shiny import ui, instead of from shiny.express import ui.
  • The UI is created explicitly in a variable named app_ui, using nested calls to UI component functions.
  • There is an explicitly defined server function. (This function is executed once for each browser session that connects to the app).
  • The code output is created with output_code("txt"). For Shiny Express mode, we didn’t have to create that output – it’s created automatically when it sees the @render.code.
  • There is an explicitly created object named app, which is a shiny.App() object.

Shiny Express apps do these things implicitly, instead of requiring you to do them explicitly.

Installation

As of this writing, Shiny Express is in Shiny 0.6.1, which is on PyPI. However, there have been changes to Shiny Express in the development version since then, and this document reflects those changes. It can also be used on shinylive.io. (For embedding Shinylive applications in Quarto documents, it is technically possible, as this document shows, but it is a manual process as of this writing.)

To run these examples, you can use shinylive.io, or you can install shiny and htmltools locally:

pip install shiny

Basic app with a plot

The example above shows a very bare-bones Shiny application. Here’s one that’s a little more sophisticated, with a container component (a sidebar), and a plot.

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

with ui.sidebar():
    ui.input_slider("n", "Number of points", min=1, max=20, value=10)

@render.plot
def plot():
    plt.scatter(range(input.n()), range(input.n()))

Contrast the Shiny Express code above with the Shiny Core equivalent below:

#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 300
import matplotlib.pyplot as plt
from shiny import App, ui, render

app_ui = ui.page_sidebar(
    ui.sidebar(
        ui.input_slider("n", "Number of points", min=1, max=20, value=10),
    ),
    ui.output_plot("plot"),
)

def server(input, output, session):
    @render.plot
    def plot():
        plt.scatter(range(input.n()), range(input.n()))

app = App(app_ui, server)

Some things to notice:

  • In the Core app, we first used the page-level component ui.page_sidebar(), and nested inside of that, a ui.sidebar().
  • In the Express app, we created a sidebar using with ui.sidebar(). (Under the hood, this component tells Shiny Express that the parent page component is ui.page_sidebar()).
  • Notice that with the Express app used express.ui.sidebar(), while the Core app used ui.sidebar(). These are not quite the same thing – the function in express.ui is actually a special wrapper for the function in ui which can be used as a context manager – that is it can be used with with.

Motivation

With Shiny Express, our hope is to introduce a gentler simplicity/power tradeoff, that is nearly as easy as Streamlit but 1) not nearly as limited, and 2) leads you most of the way to Shiny Core.

Caveats

While we believe that Shiny Express will turn out to be an effective tool for those new to Shiny, we also believe that it will not be appropriate for all use cases–hence our continuing belief in the Shiny Core model. A recurring theme you will find in the finer points below is that Shiny Express is easier to write but harder to manipulate and reason about, while Shiny Core demands more up-front learning and some small inconveniences in return for being easier to read and reason about as your apps get larger.

We also want to acknowledge the inherent risk of introducing a second (or depending on how you count, also a third and fourth!) way of writing Shiny apps. One risk is that Shiny Express will lead users into a learning cul-de-sac that is then harder to grow out of (and into Shiny Core) than if they had just learned Shiny in the first place–as we see Streamlit users cling to it long after they have left the domain where Streamlit works well. Another risk is that having two ways of doing things is just going to be confusing (see Panel, or on the mostly-positive side, Matplotlib).

Differences between Express and Core

The main differences between Shiny Express and Core are the following:

  • There is no separation between UI and server code. The UI and server code is mixed together.
  • In Shiny Express, UI components can be nested by writing with ui.xx(), where ui.xx() is a component that can contain other UI elements. In Shiny Core, you use nested function calls, like ui.xx(ui.yy()).
  • Shiny Express apps have from shiny.express import ..., import shiny.express, or from shiny import express. The presence of any of these statements tells Shiny that the app should be run in Express mode.

No separation between UI and server code

In Core, UI and server logic are declared separately, with the UI containing ui.output_xxx objects to indicate where each output goes and the server containing the logic in a @render.xx function indicating what each output is.

#| standalone: true
#| components: [editor, viewer]
# Core
from shiny import ui, render, reactive, App
from datetime import datetime

app_ui = ui.page_fixed(
    ui.h1("Title"),
    ui.output_code("greeting"),
)

def server(input, output, session):
    @reactive.Calc
    def time():
        reactive.invalidate_later(1)
        return datetime.now()

    @render.code
    def greeting():
        return f"Hello, world!\nIt's currently {time()}."

app = App(app_ui, server)

In Shiny Express, the top level of the Python file can contain both UI expressions and server declarations, in any order. By default, declaring a render function causes it to appear right in that spot.

#| standalone: true
#| components: [editor, viewer]
# Express
from shiny import ui, render, reactive, App
import shiny.express
from datetime import datetime

ui.h1("Title")

@reactive.Calc
def time():
    reactive.invalidate_later(1)
    return datetime.now()

@render.code
def greeting():
    return f"Hello, world!\nIt's currently {time()}."

Notice how greeting in this app does not have a corresponding call to output_code("greeting"). This is because in Shiny Express, the render functions automatically invoke that output function and add it to the page – no need to do it manually.

Express advantages:

  • It’s nice for beginners not to have to learn about the difference between UI and server.
  • Avoids having to write code in two different places for a single output, and having to make the IDs match up.
  • No need to write nested function declarations (i.e. functions inside the server function), which can be surprising to Python programmers.

Core advantages:

  • UI structure is clearer to read, reorder, and restructure. This advantage grows as app UIs grow larger.
  • Explicit server function declaration gave us a natural place to put code that should only execute at startup (top level) versus for each session (server function body).

Container components using with ui.xx()

Broadly speaking, there are two kinds of UI components in Shiny: container components, which, as the name suggests, contain other components, and non-container components, which don’t. (You can also think of the UI as a tree data structure; container components have children, while non-container components are leaf, or terminal nodes in the tree.)

Here are some examples of container components:

  • sidebar()
  • card()
  • layout_columns()
  • div()

Here are some examples of non-container components:

  • input_text()
  • output_plot()

In Shiny Core, all components are available from the ui submodule, for example, ui.sidebar(), and ui.input_text(), and to nest the components, you nest the function calls, like ui.sidebar(ui.input_text()). You might create a simple app UI like this:

#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
# Core
from shiny import ui, render, App

app_ui = ui.page_sidebar(
    ui.sidebar(
        ui.input_text("txt_in", "Type something here:"),
        fg="white",
        bg="black",
    ),
    ui.card(
        ui.output_code("result"),
    )
)

def server(input, output, session):
    @render.code
    def result():
        return f"You entered '{input.txt_in()}'."

app = App(app_ui, server)

In Express apps, there are the following differences:

  • Instead of from shiny import ui, you use from shiny.express import ui. (Almost all functions from shiny.ui have corresponding functions in shiny.express.ui.)
  • There’s no need to call page_sidebar() – if you simply use ui.sidebar(), Shiny will infer that it needs to use page_sidebar().
  • Container components, like ui.sidebar() are context managers, and used via with ui.sidebar(). Their child components go within the with block.
  • You can put the server code (like @reactive.calc and @render.code) inside of the with statement blocks.
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
# Express
from shiny import render, App
from shiny.express import input, ui

with ui.sidebar(fg="white", bg="black"):
    ui.input_text("txt_in", "Type something here:")

with ui.card():
    @render.code
    def result():
        return f"You entered '{input.txt_in()}'."

In Shiny Express, container components are usually used as context managers, using with.

Note

In unusual situations, you might want to create HTML content that doesn’t use context managers. HTML tag functions, like div() and span() can actually be used as context managers or as regular functions, so the following are equivalent:

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

with ui.div():
    with ui.pre(style="background-color: #eff;"):
        "Hello!"

ui.div(
    ui.pre(
        "Hello!",
        style="background-color: #eff;",
    ),
)

More complex component functions, such as sidebar() and card(), can only be used as context managers.

Page-level containers and options

In a Core app, the UI always has a top-level page_ container, like page_fluid(), page_fillable(), or page_sidebar().

With Shiny Express, you normally don’t have to think about which page_ function to use.

Instead of you deciding which specific page_ function to use, Shiny decides, based on:

  • The contents of the page – for example, if there is a sidebar() at the top level, it will automatically use page_sidebar().
  • Options that have been set with the page_opts() function.

The page_opts() function can be used to set the title of the page, as well as the filling behavior of the contents.

page_opts(
    title="Data app",
    fillable=True
)

With page_opts(fillable=False), the contents of the page will display at their “natural” size. For example, with a plot, the default height is 400 pixels. If there is more content than fits in the window, a scroll bar will show up.

#| standalone: true
#| components: [viewer]
#| layout: vertical
#| viewerHeight: 300
import matplotlib.pyplot as plt
from shiny import render
from shiny.express import input, ui

ui.page_opts(fillable=False)

with ui.sidebar():
    ui.input_slider("n", "Number of points", min=1, max=20, value=10)

with ui.pre():
    "ui.page_opts(fillable=False)"

@render.plot
def plot():
    plt.scatter(range(input.n()), range(input.n()))

With page_opts(fillable=True), the contents will try to scale to fit the window, so that no scroll bar will be present. Some components, like plots, are “flexy” and can stretch to fit; other components, like text, are not flexy, and will stay their natural size.

#| standalone: true
#| components: [viewer]
#| layout: vertical
#| viewerHeight: 300
import matplotlib.pyplot as plt
from shiny import render
from shiny.express import input, ui

ui.page_opts(fillable=True)

with ui.sidebar():
    ui.input_slider("n", "Number of points", min=1, max=20, value=10)

with ui.pre():
    "ui.page_opts(fillable=True)"

@render.plot
def plot():
    plt.scatter(range(input.n()), range(input.n()))
Note

These settings for page_opts() are passed to the shiny.ui.page_auto() function.

Deploying Shiny Express apps

To deploy Shiny Express apps on a Connect server or shinyapps.io, you will need to install rsconnect-python 1.22.0 or later:

pip install rsconnect-python --upgrade

You will also need to provide a requirements.txt file which tells the server to install htmltools and shiny from GitHub:

# requirements.txt
htmltools@git+https://github.com/posit-dev/py-htmltools.git@main
shiny@git+https://github.com/posit-dev/py-shiny.git@main

Then deploy the app as usual. If you are in the directory containing the app, use the following command, replacing <server name> with the nickname for your server.

rsconnect deploy shiny . -n <server name>