Cards and panels
Use cards and panels to define areas of related content in your Shiny app.
Use cards and panels to define areas of related content.
Relevant Functions
-
ui.card
ui.card(*args, full_screen=False, height=None, max_height=None, min_height=None, fill=True, class_=None, id=None, **kwargs) -
ui.card_header
ui.card_header(*args, container=tags.div, **kwargs) -
ui.card_body
ui.card_body(*args, fillable=True, min_height=None, max_height=None, max_height_full_screen=MISSING, height=None, padding=None, gap=None, fill=True, class_=None, **kwargs) -
ui.card_footer
ui.card_footer(*args, **kwargs) -
ui.panel_absolute
ui.panel_absolute(*args, top=None, left=None, right=None, bottom=None, width=None, height=None, draggable=False, fixed=False, cursor='auto', **kwargs) -
ui.panel_fixed
ui.panel_fixed(*args, top=None, left=None, right=None, bottom=None, width=None, height=None, draggable=False, cursor='auto', **kwargs) -
ui.panel_title
ui.panel_title(title, window_title=MISSING) -
ui.panel_conditional
ui.panel_conditional(condition, *args, **kwargs)
Cards
Cards are a common organizing unit for modern user interfaces (UI). At their core, they’re just rectangular containers with borders and padding. However, when utilized properly to group related information, they help users better digest, engage, and navigate through content.
Hello card()
A ui.card() is designed to handle any number of “known” card items (e.g., ui.card_header(), ui.card_body(), etc) as unnamed arguments (i.e., children). As we’ll see shortly, ui.card() also has some useful named arguments (e.g., full_screen, height, etc).
At their core, ui.card() and card items are just an HTML div() with a special Bootstrap class, so you can use Bootstrap’s utility classes to customize things like colors, text, borders, etc.
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| viewerHeight: 150
from shiny import App, ui
app_ui = ui.page_fluid(
ui.card(
ui.card_header(
"A header",
class_="bg-dark"
),
ui.card_body(
ui.markdown("Some text with a [link](https://github.com)")
)
)
)
def server(input, output, session):
pass
app = App(app_ui, server)Implicit card_body()
If you find yourself using ui.card_body() without changing any of its defaults, consider dropping it altogether since any direct children of ui.card() that aren’t “known” ui.card() items are wrapped together into an implicit ui.card_body() call.
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| viewerHeight: 150
from shiny import App, ui
app_ui = ui.page_fluid(
ui.card(
ui.card_header(
"A header",
class_="bg-dark"
),
ui.markdown("Some text with a [link](https://github.com).")
)
)
def server(input, output, session):
pass
app = App(app_ui, server)Restricting growth
By default, a ui.card()’s size grows to accommodate the size of its contents. Thus, if a ui.card_body() contains a large amount of text, tables, etc., you may want to specify a height or max_height. That said, when laying out multiple cards, it’s likely best not to specify height on the ui.card(), and instead, let the layout determine the height.
Although scrolling is convenient for reducing the amount of space required to park lots of content, it can also be a nuisance to the user. To help reduce the need for scrolling, consider pairing scrolling with full_screen=True (which adds an icon to expand the card’s size to the browser window). Notice how, when the card is expanded to full-screen, max_height/height won’t affect the full-screen size of the card.
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| viewerHeight: 300
from shiny import App, ui
app_ui = ui.page_fluid(
ui.card(
ui.card_header("A long, scrolling, description"),
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris. " * 15,
max_height="250px",
full_screen=True
)
)
def server(input, output, session):
pass
app = App(app_ui, server)Filling outputs
A ui.card()’s default behavior is optimized for facilitating filling layouts. More specifically, if a fill item (e.g., ui.output_plot()), appears as a direct child of a ui.card_body(), it resizes to fit the ui.card()’s specified height. This means, by specifying height="250px" we’ve effectively shrunk the plot’s height from its default of 400 down to about 200 pixels. And, when expanded to full_screen, the plot grows to match the ui.card()’s new size.
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| viewerHeight: 275
from shiny import App, render, ui
import matplotlib.pyplot as plt
app_ui = ui.page_fluid(
ui.card(
ui.card_header("A filling plot"),
ui.card_body(ui.output_plot("plot")),
height="250px",
full_screen=True
)
)
def server(input, output, session):
@render.plot
def plot():
fig, ax = plt.subplots()
ax.plot([1, 2, 3], [1, 4, 9])
return fig
app = App(app_ui, server)from shiny import App, render, ui
import matplotlib.pyplot as plt
app_ui = ui.page_fluid(
ui.card(
ui.card_header("A filling plot"),
ui.card_body(ui.output_plot("plot")),
height="250px",
full_screen=True
)
)
def server(input, output, session):
@render.plot
def plot():
fig, ax = plt.subplots()
ax.plot([1, 2, 3], [1, 4, 9])
return fig
app = App(app_ui, server)Multiple cards
ui.layout_columns() is especially nice for laying out multiple cards since each card in a particular row will have the same height (by default).
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| viewerHeight: 350
from shiny import App, render, ui
app_ui = ui.page_fluid(
ui.layout_columns(
ui.card(
ui.card_header("User Settings"),
ui.card_body(
ui.input_text("name", "Name", "John Doe"),
ui.input_slider("age", "Age", 18, 100, 30),
),
ui.card_footer(
ui.input_action_button("save", "Save Changes", class_="btn-primary")
),
full_screen=True,
),
ui.card(
ui.card_header("Activity Summary"),
ui.card_body(
ui.output_text("summary"),
ui.tags.ul(
ui.tags.li("Last login: 2 hours ago"),
ui.tags.li("Messages: 12 unread"),
ui.tags.li("Tasks: 5 pending"),
),
),
full_screen=True,
),
col_widths=[6, 6],
height="300px",
)
)
def server(input, output, session):
@render.text
def summary():
return f"Hello, {input.name()}!"
app = App(app_ui, server)from shiny.express import input, render, ui
with ui.layout_columns(col_widths=[6, 6], height="300px"):
with ui.card(full_screen=True):
ui.card_header("User Settings")
with ui.card_body():
ui.input_text("name", "Name", "John Doe")
ui.input_slider("age", "Age", 18, 100, 30)
with ui.card_footer():
ui.input_action_button("save", "Save Changes", class_="btn-primary")
with ui.card(full_screen=True):
ui.card_header("Activity Summary")
with ui.card_body():
@render.text
def summary():
return f"Hello, {input.name()}!"
ui.tags.ul(
ui.tags.li("Last login: 2 hours ago"),
ui.tags.li("Messages: 12 unread"),
ui.tags.li("Tasks: 5 pending"),
)from shiny import App, render, ui
app_ui = ui.page_fluid(
ui.layout_columns(
ui.card(
ui.card_header("User Settings"),
ui.card_body(
ui.input_text("name", "Name", "John Doe"),
ui.input_slider("age", "Age", 18, 100, 30),
),
ui.card_footer(
ui.input_action_button("save", "Save Changes", class_="btn-primary")
),
full_screen=True,
),
ui.card(
ui.card_header("Activity Summary"),
ui.card_body(
ui.output_text("summary"),
ui.tags.ul(
ui.tags.li("Last login: 2 hours ago"),
ui.tags.li("Messages: 12 unread"),
ui.tags.li("Tasks: 5 pending"),
),
),
full_screen=True,
),
col_widths=[6, 6],
height="300px",
)
)
def server(input, output, session):
@render.text
def summary():
return f"Hello, {input.name()}!"
app = App(app_ui, server)Follow these steps to create an app with content separated into cards:
Use
ui.layout_columns()to arrange multiple cards in a grid. Setcol_widthsto control column widths using Bootstrap’s 12-column grid (e.g.,col_widths=[6, 6]for two equal-width cards per row).Add
ui.card()calls insideui.layout_columns(). You can useui.card_header()andui.card_footer()to create card headers and footers.Control the appearance and functionality of each card by passing additional arguments to
ui.card(). For example, setfull_screen=Trueto allow users to expand the card to fullscreen.
Tabbed card
Use ui.navset_card_underline() inside a card to create tabbed content.
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| viewerHeight: 250
from shiny import App, ui
app_ui = ui.page_fluid(
ui.navset_card_underline(
ui.nav_spacer(),
ui.nav_panel(
"Overview",
"This project has 3 milestones and 12 tasks.",
ui.br(),
"Current status: On track",
),
ui.nav_panel(
"Team",
ui.tags.ul(
ui.tags.li("Alice (Lead)"),
ui.tags.li("Bob (Developer)"),
ui.tags.li("Carol (Designer)"),
),
),
ui.nav_panel(
"Timeline",
"Start: January 2026",
ui.br(),
"Expected completion: June 2026",
),
title="Project Dashboard",
),
full_screen=True,
height="300px",
)
def server(input, output, session):
pass
app = App(app_ui, server)from shiny.express import ui
ui.page_opts(full_screen=True, height="300px")
with ui.navset_card_underline(title="Project Dashboard"):
ui.nav_spacer()
with ui.nav_panel("Overview"):
"This project has 3 milestones and 12 tasks."
ui.br()
"Current status: On track"
with ui.nav_panel("Team"):
ui.tags.ul(
ui.tags.li("Alice (Lead)"),
ui.tags.li("Bob (Developer)"),
ui.tags.li("Carol (Designer)"),
)
with ui.nav_panel("Timeline"):
"Start: January 2026"
ui.br()
"Expected completion: June 2026"from shiny import App, ui
app_ui = ui.page_fluid(
ui.navset_card_underline(
ui.nav_spacer(),
ui.nav_panel(
"Overview",
"This project has 3 milestones and 12 tasks.",
ui.br(),
"Current status: On track",
),
ui.nav_panel(
"Team",
ui.tags.ul(
ui.tags.li("Alice (Lead)"),
ui.tags.li("Bob (Developer)"),
ui.tags.li("Carol (Designer)"),
),
),
ui.nav_panel(
"Timeline",
"Start: January 2026",
ui.br(),
"Expected completion: June 2026",
),
title="Project Dashboard",
),
full_screen=True,
height="300px",
)
def server(input, output, session):
pass
app = App(app_ui, server)Floating panel
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| layout: horizontal
#| viewerHeight: 320
## file: app.py
from htmltools import css
from shiny import App, ui
app_ui = ui.page_fillable(
ui.panel_absolute(
ui.card(
ui.card_header("Draggable panel"),
"Move this panel anywhere you want.",
),
width="300px",
right="50px",
top="50px",
draggable=True,
),
style=css(
background_image="url(https://unsplash.com/photos/XKXGghL7GQc/download?force=true&w=1920)",
background_repeat="no-repeat",
background_size="cover",
background_position="center bottom",
),
)
def server(input, output, session):
pass
app = App(app_ui, server)
from functools import partial
from htmltools import css
from shiny.express import ui
from shiny.ui import page_fillable
page_style = css(
background_image="url(https://unsplash.com/photos/XKXGghL7GQc/download?force=true&w=1920)",
background_repeat="no-repeat",
background_size="cover",
background_position="center bottom",
)
ui.page_opts(page_fn=partial(page_fillable, style=page_style))
with ui.panel_absolute(
width="300px",
right="50px",
top="50px",
draggable=True,
):
with ui.card():
ui.card_header("Draggable panel")
"Move this panel anywhere you want."from htmltools import css
from shiny import App, ui
app_ui = ui.page_fillable(
ui.panel_absolute(
ui.card(
ui.card_header("Draggable panel"),
"Move this panel anywhere you want.",
),
width="300px",
right="50px",
top="50px",
draggable=True,
),
style=css(
background_image="url(https://unsplash.com/photos/XKXGghL7GQc/download?force=true&w=1920)",
background_repeat="no-repeat",
background_size="cover",
background_position="center bottom",
),
)
def server(input, output, session):
pass
app = App(app_ui, server)Follow these steps to create an app that has a panel floating over a main image.
First, to create the floating panel:
Pass
ui.panel_absolute()as the second argument of your Shiny UI page method, afterui.img(). Pass elements that you want to appear inside the panel toui.panel_absolute().Position the panel using the
top,bottom,left, and/orrightparameters. Set the size of the panel using theheightand/orwidthparameters.If you want the panel to be draggable, set the
draggableparameter toTrue.
In the example above, we used CSS to add a scaling background image to the page. You can also use ui.img() to create this effect:
Pass
ui.img()to any Shiny UI page method (e.g.,ui.page_fluid()).ui.img()creates an image.Pass the path or URL of your desired image to
ui.img()’ssrcparameter. Set additional parameters to control the appearance of the image (e.g.,widthandheight).
See also: ui.panel_fixed(). ui.panel_fixed() is equivalent to calling ui.panel_absolute() with fixed=True (i.e., the panel does not scroll with the rest of the page).
Conditional panel
Use ui.panel_conditional() to show or hide a group of UI elements based on the state of your inputs, without a round trip to the server.
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| layout: horizontal
#| viewerHeight: 300
## file: app.py
from shiny import App, ui
app_ui = ui.page_fluid(
ui.input_checkbox("show", "Show advanced options", False),
ui.panel_conditional(
"input.show",
ui.input_radio_buttons("kind", "Control kind", ["slider", "select"]),
),
ui.panel_conditional(
"input.show && input.kind === 'slider'",
ui.input_slider("slider", None, min=0, max=100, value=50),
),
ui.panel_conditional(
"input.show && input.kind === 'select'",
ui.input_select("select", None, ["A", "B", "C"]),
),
)
def server(input, output, session):
pass
app = App(app_ui, server)
from shiny.express import ui
ui.input_checkbox("show", "Show advanced options", False)
with ui.panel_conditional("input.show"):
ui.input_radio_buttons("kind", "Control kind", ["slider", "select"])
with ui.panel_conditional("input.show && input.kind === 'slider'"):
ui.input_slider("slider", None, min=0, max=100, value=50)
with ui.panel_conditional("input.show && input.kind === 'select'"):
ui.input_select("select", None, ["A", "B", "C"])from shiny import App, ui
app_ui = ui.page_fluid(
ui.input_checkbox("show", "Show advanced options", False),
ui.panel_conditional(
"input.show",
ui.input_radio_buttons("kind", "Control kind", ["slider", "select"]),
),
ui.panel_conditional(
"input.show && input.kind === 'slider'",
ui.input_slider("slider", None, min=0, max=100, value=50),
),
ui.panel_conditional(
"input.show && input.kind === 'select'",
ui.input_select("select", None, ["A", "B", "C"]),
),
)
def server(input, output, session):
pass
app = App(app_ui, server)Follow these steps to add a conditional panel to your app:
Add
ui.panel_conditional()inside any Shiny UI page method (e.g.,ui.page_fluid()).Pass a condition as the first argument. This is a JavaScript expression written as a string, evaluated in the browser. The panel’s contents are shown whenever the expression is truthy.
Pass the UI elements you want to show or hide as the remaining arguments.
Inside the condition, refer to inputs as input.<id> — for example "input.show". Conditions can combine inputs with JavaScript operators, as in "input.show && input.kind === 'slider'".
This is the most common source of surprises with ui.panel_conditional(). The string is evaluated by the browser, so it follows JavaScript rules, not Python ones:
- Use
&&,||, and!— notand,or, andnot. - Compare with
===/!==, and quote string values:"input.kind === 'slider'". - JavaScript booleans are
trueandfalse, lowercase. - Access inputs as
input.my_id— dot notation, and no trailing().
A mistake in the expression is not a Python error, so your app still starts normally and the panel simply never appears. The browser console reports what went wrong — writing "input.show and true" logs Error parsing expression: input.show and true. Check there first when a conditional panel refuses to show.
Because the condition never reaches Python, showing and hiding is instant and does not invalidate any reactive outputs. But it also means the condition cannot use reactive values or any other server-side state. When your visibility logic depends on something only the server knows, render the UI dynamically with @render.ui instead.
ui.panel_conditional() records the module’s namespace alongside the condition, and Shiny narrows the input object to that module before evaluating it. So within a module you write the same "input.show" you would write in a standalone app — not the namespaced "input['my_module-show']", which will not match anything.
Hidden content is still sent to the browser and still exists in the page — ui.panel_conditional() only controls visibility. Do not use it to hide sensitive values.