Shiny Express: Basics
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 offrom 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 ashiny.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, aui.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 isui.page_sidebar()
). - Notice that with the Express app used
express.ui.sidebar()
, while the Core app usedui.sidebar()
. These are not quite the same thing – the function inexpress.ui
is actually a special wrapper for the function inui
which can be used as a context manager – that is it can be used withwith
.
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()
, whereui.xx()
is a component that can contain other UI elements. In Shiny Core, you use nested function calls, likeui.xx(ui.yy())
. - Shiny Express apps have
from shiny.express import ...
,import shiny.express
, orfrom 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 usefrom shiny.express import ui
. (Almost all functions fromshiny.ui
have corresponding functions inshiny.express.ui
.) - There’s no need to call
page_sidebar()
– if you simply useui.sidebar()
, Shiny will infer that it needs to usepage_sidebar()
. - Container components, like
ui.sidebar()
are context managers, and used viawith ui.sidebar()
. Their child components go within thewith
block. - You can put the server code (like
@reactive.calc
and@render.code
) inside of thewith
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
.
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 usepage_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(="Data app",
title=True
fillable )
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()))
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>