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, **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=None, 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, **kwargs) -
ui.panel_well
ui.panel_well(*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.panel_well(
ui.panel_title("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.panel_well():
ui.h2("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.panel_well(
ui.panel_title("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).