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: 400

from shiny import App, render, ui
import matplotlib.pyplot as plt

app_ui = ui.page_fluid(
    ui.card(
        ui.card_header(
            "Sales Trend",
            class_="bg-dark",
        ),
        ui.output_plot("plot"),
        full_screen=True,
        height="350px",
    )
)

def server(input, output, session):
    @render.plot
    def plot():
        # Generate sample sales data
        months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
                  'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
        sales = [45, 52, 48, 61, 58, 67, 71, 69, 74, 78, 82, 88]

        fig, ax = plt.subplots(figsize=(7, 3.5))
        ax.plot(months, sales, marker='o', linewidth=2, markersize=5, color='steelblue')
        ax.fill_between(range(len(months)), sales, alpha=0.3, color='steelblue')
        ax.set_title("Monthly Sales Trend", fontsize=10, pad=8)
        ax.set_xlabel("Month", fontsize=8)
        ax.set_ylabel("Sales ($K)", fontsize=8)
        ax.grid(True, alpha=0.3)
        ax.tick_params(labelsize=7)
        plt.xticks(rotation=45)
        plt.tight_layout(pad=2.0)
        return fig

app = App(app_ui, server)
from shiny.express import render, ui
import matplotlib.pyplot as plt

with ui.card(full_screen=True, height="350px"):  
    ui.card_header(  
        "Sales Trend",  
        class_="bg-dark",  
    )  

    @render.plot  
    def plot():  
        # Generate sample sales data
        months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
                  'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
        sales = [45, 52, 48, 61, 58, 67, 71, 69, 74, 78, 82, 88]

        fig, ax = plt.subplots(figsize=(7, 3.5))
        ax.plot(months, sales, marker='o', linewidth=2, markersize=5, color='steelblue')
        ax.fill_between(range(len(months)), sales, alpha=0.3, color='steelblue')
        ax.set_title("Monthly Sales Trend", fontsize=10, pad=8)
        ax.set_xlabel("Month", fontsize=8)
        ax.set_ylabel("Sales ($K)", fontsize=8)
        ax.grid(True, alpha=0.3)
        ax.tick_params(labelsize=7)
        plt.xticks(rotation=45)
        plt.tight_layout(pad=2.0)
        return fig  
from shiny import App, render, ui
import matplotlib.pyplot as plt

app_ui = ui.page_fluid(
    ui.card(  
        ui.card_header(  
            "Sales Trend",  
            class_="bg-dark",  
        ),  
        ui.output_plot("plot"),  
        full_screen=True,  
        height="350px",  
    )  
)

def server(input, output, session):
    @render.plot  
    def plot():  
        # Generate sample sales data
        months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
                  'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
        sales = [45, 52, 48, 61, 58, 67, 71, 69, 74, 78, 82, 88]

        fig, ax = plt.subplots(figsize=(7, 3.5))
        ax.plot(months, sales, marker='o', linewidth=2, markersize=5, color='steelblue')
        ax.fill_between(range(len(months)), sales, alpha=0.3, color='steelblue')
        ax.set_title("Monthly Sales Trend", fontsize=10, pad=8)
        ax.set_xlabel("Month", fontsize=8)
        ax.set_ylabel("Sales ($K)", fontsize=8)
        ax.grid(True, alpha=0.3)
        ax.tick_params(labelsize=7)
        plt.xticks(rotation=45)
        plt.tight_layout(pad=2.0)
        return fig  

app = App(app_ui, server)
No matching items

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=..., **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)

No matching items

Details

A card is a rectangular container with borders and padding that groups related UI elements together.

To add a card to your app:

  1. Add ui.card() to your UI.
  2. Place UI elements inside the card as direct children, or use ui.card_header(), ui.card_body(), and ui.card_footer() to organize content into distinct sections.

If you don’t use ui.card_body(), any direct children of ui.card() that aren’t card items (like ui.card_header() or ui.card_footer()) are automatically wrapped in an implicit ui.card_body().

Card customization

Cards and card items accept Bootstrap utility classes for customization. Use the class_ parameter to apply classes for colors, text, borders, and more.

Card size

By default, a card grows to accommodate its contents. Set the height or max_height parameter to control the card’s size. If your card contains large amounts of text, tables, or other content, setting a height or max_height enables scrolling. When laying out multiple cards, let the layout function determine the card heights rather than setting fixed heights.

Use full_screen=True to add an icon that expands the card to fill the browser window. Pair max_height with full_screen=True to provide an expansion icon. When users click the icon, the card fills the browser window. The max_height constraint only applies to the normal view, not the full-screen view.

Filling layouts

Cards support filling layouts where outputs automatically resize to fit the available space. When a fill item like ui.output_plot() appears as a direct child of a ui.card_body(), it resizes to match the card’s height. For example, setting height="250px" on a card reduces the plot’s height from its default 400px to approximately 200px. When expanded to full-screen, the plot grows to match the 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: 500

from shiny import App, render, ui
import matplotlib.pyplot as plt

app_ui = ui.page_fluid(
    ui.card(
        ui.card_header("Sales Trend"),
        ui.output_plot("plot"),
        height="400px",
        full_screen=True
    )
)

def server(input, output, session):
    @render.plot
    def plot():
        # Generate sample data
        months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
        sales = [45, 52, 48, 61, 58, 67]

        fig, ax = plt.subplots(figsize=(8, 6))
        ax.plot(months, sales, marker='o', linewidth=2, markersize=8, color='steelblue')
        ax.set_title("Monthly Sales Trend")
        ax.set_xlabel("Month")
        ax.set_ylabel("Sales ($K)")
        ax.grid(True, alpha=0.3)
        return fig

app = App(app_ui, server)
from shiny.express import ui, render
import matplotlib.pyplot as plt

with ui.card(height="400px", full_screen=True):
    ui.card_header("Sales Trend")

    @render.plot
    def plot():
        # Generate sample data
        months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
        sales = [45, 52, 48, 61, 58, 67]

        fig, ax = plt.subplots(figsize=(8, 6))
        ax.plot(months, sales, marker='o', linewidth=2, markersize=8, color='steelblue')
        ax.set_title("Monthly Sales Trend")
        ax.set_xlabel("Month")
        ax.set_ylabel("Sales ($K)")
        ax.grid(True, alpha=0.3)
        return fig
from shiny import App, render, ui
import matplotlib.pyplot as plt

app_ui = ui.page_fluid(
    ui.card(
        ui.card_header("Sales Trend"),
        ui.output_plot("plot"),
        height="400px",
        full_screen=True
    )
)

def server(input, output, session):
    @render.plot
    def plot():
        # Generate sample data
        months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
        sales = [45, 52, 48, 61, 58, 67]

        fig, ax = plt.subplots(figsize=(8, 6))
        ax.plot(months, sales, marker='o', linewidth=2, markersize=8, color='steelblue')
        ax.set_title("Monthly Sales Trend")
        ax.set_xlabel("Month")
        ax.set_ylabel("Sales ($K)")
        ax.grid(True, alpha=0.3)
        return fig

app = App(app_ui, server)
No matching items

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)
No matching items

Card with sidebar

Use ui.layout_sidebar() inside a card to create a card with a sidebar layout. This pattern is useful for cards that need input controls or filters alongside their main 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: 425

import pandas as pd
from shiny import App, reactive, render, ui

# Create a small flower dataset
flowers = pd.DataFrame({
    "flower": ["Rose", "Tulip", "Daisy", "Orchid", "Lily", "Sunflower", "Violet", "Poppy"],
    "species": ["Rosa", "Tulipa", "Bellis", "Orchidaceae", "Lilium", "Helianthus", "Viola", "Papaver"],
    "color": ["Red", "Yellow", "White", "Purple", "Pink", "Yellow", "Purple", "Red"],
    "petal_length": [5.2, 3.8, 2.1, 6.3, 4.5, 8.1, 2.8, 3.5],
})

app_ui = ui.page_fluid(
    ui.card(
        ui.card_header("Flower Data Explorer"),
        ui.layout_sidebar(
            ui.sidebar(
                ui.input_select(
                    "color_filter",
                    "Filter by color",
                    ["All", "Red", "Yellow", "White", "Purple", "Pink"],
                ),
                ui.input_slider("rows", "Rows to show", 1, 8, 5),
                ui.input_action_button("reset", "Reset"),
            ),
            ui.card_body(
                ui.output_data_frame("flower_table"),
            ),
        ),
        full_screen=True,
        height="350px",
    )
)

def server(input, output, session):
    @render.data_frame
    def flower_table():
        # Filter by color
        if input.color_filter() == "All":
            df = flowers
        else:
            df = flowers[flowers["color"] == input.color_filter()]

        # Limit rows
        return df.head(input.rows())

    @reactive.effect
    @reactive.event(input.reset)
    def _():
        ui.update_select("color_filter", selected="All")
        ui.update_slider("rows", value=5)

app = App(app_ui, server)
import pandas as pd
from shiny.express import input, render, ui
from shiny import reactive

# Create a small flower dataset
flowers = pd.DataFrame({
    "flower": ["Rose", "Tulip", "Daisy", "Orchid", "Lily", "Sunflower", "Violet", "Poppy"],
    "species": ["Rosa", "Tulipa", "Bellis", "Orchidaceae", "Lilium", "Helianthus", "Viola", "Papaver"],
    "color": ["Red", "Yellow", "White", "Purple", "Pink", "Yellow", "Purple", "Red"],
    "petal_length": [5.2, 3.8, 2.1, 6.3, 4.5, 8.1, 2.8, 3.5],
})

with ui.card(full_screen=True, height="350px"):
    ui.card_header("Flower Data Explorer")
    with ui.layout_sidebar():
        with ui.sidebar():
            ui.input_select(
                "color_filter",
                "Filter by color",
                ["All", "Red", "Yellow", "White", "Purple", "Pink"],
            )
            ui.input_slider("rows", "Rows to show", 1, 8, 5)
            ui.input_action_button("reset", "Reset")
        with ui.card_body():

            @render.data_frame
            def flower_table():
                # Filter by color
                if input.color_filter() == "All":
                    df = flowers
                else:
                    df = flowers[flowers["color"] == input.color_filter()]

                # Limit rows
                return df.head(input.rows())

        @reactive.effect
        @reactive.event(input.reset)
        def _():
            ui.update_select("color_filter", selected="All")
            ui.update_slider("rows", value=5)
import pandas as pd
from shiny import App, reactive, render, ui

# Create a small flower dataset
flowers = pd.DataFrame({
    "flower": ["Rose", "Tulip", "Daisy", "Orchid", "Lily", "Sunflower", "Violet", "Poppy"],
    "species": ["Rosa", "Tulipa", "Bellis", "Orchidaceae", "Lilium", "Helianthus", "Viola", "Papaver"],
    "color": ["Red", "Yellow", "White", "Purple", "Pink", "Yellow", "Purple", "Red"],
    "petal_length": [5.2, 3.8, 2.1, 6.3, 4.5, 8.1, 2.8, 3.5],
})

app_ui = ui.page_fluid(
    ui.card(
        ui.card_header("Flower Data Explorer"),
        ui.layout_sidebar(
            ui.sidebar(
                ui.input_select(
                    "color_filter",
                    "Filter by color",
                    ["All", "Red", "Yellow", "White", "Purple", "Pink"],
                ),
                ui.input_slider("rows", "Rows to show", 1, 8, 5),
                ui.input_action_button("reset", "Reset"),
            ),
            ui.card_body(
                ui.output_data_frame("flower_table"),
            ),
        ),
        full_screen=True,
        height="350px",
    )
)

def server(input, output, session):
    @render.data_frame
    def flower_table():
        # Filter by color
        if input.color_filter() == "All":
            df = flowers
        else:
            df = flowers[flowers["color"] == input.color_filter()]

        # Limit rows
        return df.head(input.rows())

    @reactive.effect
    @reactive.event(input.reset)
    def _():
        ui.update_select("color_filter", selected="All")
        ui.update_slider("rows", value=5)

app = App(app_ui, server)
No matching items

Multiple cards

Use ui.layout_columns() to arrange multiple cards in a multi-column layout. Cards in the same row automatically share the same height. See the variations below for examples of multiple-card layouts.

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| viewerHeight: 500

import pandas as pd
from shiny import App, render, ui

app_ui = ui.page_fluid(
    ui.layout_columns(
        ui.card(
            ui.card_header(
                "Sales Data",
                ui.toolbar(
                    ui.toolbar_input_select(
                        id="region_filter",
                        label="Region",
                        choices=["All", "North", "South", "East", "West"],
                        selected="All"
                    )
                )
            ),
            ui.card_body(
                ui.output_data_frame("sales_table")
            ),
            full_screen=True
        ),
        ui.card(
            ui.card_header("Key Insights"),
            ui.card_body(
                ui.markdown("""
                **Performance Summary**

                - Sales increased by 96% over 6 months
                - April showed a temporary dip
                - Strong recovery in May and June
                - Current trajectory suggests continued growth

                *Data updated: June 2024*
                """)
            ),
            full_screen=True
        )
    )
)

def server(input, output, session):
    @render.data_frame
    def sales_table():
        data = pd.DataFrame({
            "Product": ["Widget A", "Widget B", "Widget C", "Widget D", "Widget E"],
            "Region": ["North", "South", "East", "West", "North"],
            "Sales": [15_200, 12_800, 9_500, 11_300, 8_700],
            "Growth": ["+12%", "+8%", "+5%", "+10%", "+3%"]
        })

        if input.region_filter() != "All":
            data = data[data["Region"] == input.region_filter()]

        return data

app = App(app_ui, server)
import pandas as pd
from shiny.express import input, render, ui

with ui.layout_columns():
    with ui.card(full_screen=True):
        with ui.card_header():
            "Sales Data"
            with ui.toolbar():
                ui.toolbar_input_select(
                    id="region_filter",
                    label="Region",
                    choices=["All", "North", "South", "East", "West"],
                    selected="All"
                )

        @render.data_frame
        def sales_table():
            data = pd.DataFrame({
                "Product": ["Widget A", "Widget B", "Widget C", "Widget D", "Widget E"],
                "Region": ["North", "South", "East", "West", "North"],
                "Sales": [15_200, 12_800, 9_500, 11_300, 8_700],
                "Growth": ["+12%", "+8%", "+5%", "+10%", "+3%"]
            })

            if input.region_filter() != "All":
                data = data[data["Region"] == input.region_filter()]

            return data

    with ui.card(full_screen=True):
        ui.card_header("Key Insights")
        ui.markdown("""
        **Performance Summary**

        - Sales increased by 96% over 6 months
        - April showed a temporary dip
        - Strong recovery in May and June
        - Current trajectory suggests continued growth

        *Data updated: June 2024*
        """)
import pandas as pd
from shiny import App, render, ui

app_ui = ui.page_fluid(
    ui.layout_columns(
        ui.card(
            ui.card_header(
                "Sales Data",
                ui.toolbar(
                    ui.toolbar_input_select(
                        id="region_filter",
                        label="Region",
                        choices=["All", "North", "South", "East", "West"],
                        selected="All"
                    )
                )
            ),
            ui.card_body(
                ui.output_data_frame("sales_table")
            ),
            full_screen=True
        ),
        ui.card(
            ui.card_header("Key Insights"),
            ui.card_body(
                ui.markdown("""
                **Performance Summary**

                - Sales increased by 96% over 6 months
                - April showed a temporary dip
                - Strong recovery in May and June
                - Current trajectory suggests continued growth

                *Data updated: June 2024*
                """)
            ),
            full_screen=True
        )
    )
)

def server(input, output, session):
    @render.data_frame
    def sales_table():
        data = pd.DataFrame({
            "Product": ["Widget A", "Widget B", "Widget C", "Widget D", "Widget E"],
            "Region": ["North", "South", "East", "West", "North"],
            "Sales": [15_200, 12_800, 9_500, 11_300, 8_700],
            "Growth": ["+12%", "+8%", "+5%", "+10%", "+3%"]
        })

        if input.region_filter() != "All":
            data = data[data["Region"] == input.region_filter()]

        return data

app = App(app_ui, server)
No matching items

Toolbars in cards

Add toolbars to card headers and footers to provide interactive controls. Use ui.toolbar() within a ui.card_header() or ui.card_footer() to create a compact row of buttons or inputs. Toolbars are particularly useful for actions like refreshing data, downloading reports, or accessing settings.

By default, toolbars align to the right. Set align="left" to position controls on the left side instead. Use ui.toolbar_spacer() and ui.toolbar_divider() to manipulate toolbar visual spacing.