Arrange Elements

Layout elements into rows and columns that responsively adapt to a wide range of screen sizes.

Shiny provides two core functions for arranging elements into rows and columns:

You can use these functions directly to create your own layout. Grid layouts can be used within a page, panel, or card and can even be nested within each other.

Relevant Functions

  • ui.layout_columns
    ui.layout_columns(*args, col_widths=None, row_heights=None, fill=True, fillable=True, gap=None, class_=None, height=None, min_height=None, max_height=None, **kwargs)

  • ui.layout_column_wrap
    ui.layout_column_wrap(*args, width=MISSING, fixed_width=False, heights_equal='all', fill=True, fillable=True, height=None, min_height=None, max_height=None, height_mobile=None, gap=None, class_=None, **kwargs)

  • ui.page_fixed
    ui.page_fixed(*args, title=None, lang=None, theme=None, **kwargs)

  • ui.page_fillable
    ui.page_fillable(*args, padding=None, gap=None, fillable_mobile=False, title=None, lang=None, theme=None, **kwargs)

  • ui.page_fluid
    ui.page_fluid(*args, title=None, lang=None, theme=None, **kwargs)

  • ui.page_html
    ui.page_html(html, *, extra_deps=None, deps_replace_pattern=DEPS_PLACEHOLDER)

No matching items

Grid Layouts

Both ui.layout_columns() and ui.layout_column_wrap() follow the same general pattern: pass each function a series of elements to have them arranged into a grid layout.

#| '!! 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: 175

## file: app.py
from shiny import App, ui

app_ui = ui.page_fillable(
    "ui.layout_columns()",
    ui.layout_columns(
        ui.card("Card 1"),
        ui.card("Card 2"),
        ui.card("Card 3"),
    ),
)


def server(input, output, session):
    pass


app = App(app_ui, server=server)
from shiny.express import ui

ui.page_opts(fillable=True)

"ui.layout_columns()"

with ui.layout_columns():
    with ui.card():
        "Card 1"
    with ui.card():
        "Card 2"
    with ui.card():
        "Card 3"
from shiny import App, ui

app_ui = ui.page_fillable(
    "ui.layout_columns()",
    ui.layout_columns(
        ui.card("Card 1"),
        ui.card("Card 2"),
        ui.card("Card 3"),
    ),
)


def server(input, output, session):
    pass


app = App(app_ui, server=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: 175

## file: app.py
from shiny import App, ui

app_ui = ui.page_fillable(
    "ui.layout_column_wrap()",
    ui.layout_column_wrap(
        ui.card("Card 1"),
        ui.card("Card 2"),
        ui.card("Card 3"),
    ),
)


def server(input, output, session):
    pass


app = App(app_ui, server=server)
from shiny.express import ui

ui.page_opts(fillable=True)

"ui.layout_column_wrap()"

with ui.layout_column_wrap():
    with ui.card():
        "Card 1"
    with ui.card():
        "Card 2"
    with ui.card():
        "Card 3"
from shiny import App, ui

app_ui = ui.page_fillable(
    "ui.layout_column_wrap()",
    ui.layout_column_wrap(
        ui.card("Card 1"),
        ui.card("Card 2"),
        ui.card("Card 3"),
    ),
)


def server(input, output, session):
    pass


app = App(app_ui, server=server)

Notice two things about the above apps:

  1. Both approaches create one or more rows, filling the rows with columns to hold its children.

  2. Use the handle in the bottom right corner of the apps above to view each layout at different screen sizes (this works best on desktop screens). How do the layouts shift as the screen gets smaller or larger?

How to choose between layout_columns() and layout_column_wrap()

For simple layouts, it can be hard to tell ui.layout_columns() and ui.layout_column_wrap() apart. Here’s a short set of questions to ask yourself to help you decide which function to use:

  1. Are all of the items in your arrangement the same, such as a set of cards or plots? And do you want them all to have the same width and height?

    If yes: ui.layout_column_wrap() is best for a regularly-sized grid for a homogenous collection of elements.

  2. Do you want to customize the layout’s column widths, or to have different layouts at different screen sizes (mobile vs desktop vs ultrawide)?

    If yes: ui.layout_columns() give you the tools to create highly customized, resposive column layouts.

  3. For all other uses, pick the function you like the most. I happen to like ui.layout_columns() but you might prefer ui.layout_column_wrap().

Responsive, configurable column layouts

In the example above, we saw that, without your intervention, ui.layout_columns() will arrange the elements given to it into evenly sized columns. In the default case, ui.layout_columns() makes a choice about the column widths that make the most sense based on the number of items provided.

For example, when given three items, ui.layout_columns() places each item in a column with 4 width units. You can use the col_widths argument to choose a different allocation of column width units. Keep in mind that Bootstrap’s CSS Grid uses 12 units per row. In the example below, we use col_widths=(2, 4, 6) to create a row with a small, medium, and large card.

#| '!! 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: 240

## file: app.py
from shiny import App, ui

app_ui = ui.page_fillable(
    ui.layout_columns(
        ui.card("Card 1"),
        ui.card("Card 2"),
        ui.card("Card 3"),
        col_widths=(2, 4, 6),
    ),
)


def server(input, output, session):
    pass


app = App(app_ui, server=server)
from shiny.express import ui

ui.page_opts(fillable=True)

with ui.layout_columns(col_widths=(2, 4, 6)):
    with ui.card():
        "Card 1"
    with ui.card():
        "Card 2"
    with ui.card():
        "Card 3"
from shiny import App, ui

app_ui = ui.page_fillable(
    ui.layout_columns(
        ui.card("Card 1"),
        ui.card("Card 2"),
        ui.card("Card 3"),
        col_widths=(2, 4, 6),
    ),
)


def server(input, output, session):
    pass


app = App(app_ui, server=server)
TipAdvanced layouts

There’s a lot more that layout_columns() can do with col_widths to make highly customized layouts.

  • Choose different layouts with a dictionary of column widths at specific breakpoints, e.g. col_widths={"sm": 4, "md": (2, 4, 6)}.

  • Insert empty space between items with a negative column width, e.g. col_widths=c(4, -4, 4) creates two columns 4 units wide with an empty space of 4 units between them.

Learn more in the API reference: Express | Core

Uniform grid layouts

ui.layout_column_wrap() arranges elements into a uniform grid, so its width argument applies a single value to all elements in the grid.

When width is a fractional each item is given an equal portion of the row’s width. For example, width=1 / 2 gives each item half of the row width.

#| '!! 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: 240

## file: app.py
from shiny import App, ui

app_ui = ui.page_fillable(
    ui.layout_column_wrap(  
        ui.card("Card 1"),
        ui.card("Card 2"),
        ui.card("Card 3"),
        ui.card("Card 4"),
        width=1 / 2,  
    ),
)


def server(input, output, session):
    pass


app = App(app_ui, server=server)
from shiny.express import ui

ui.page_opts(fillable=True)

with ui.layout_column_wrap(width=1 / 2):  
    with ui.card():
        "Card 1"
    with ui.card():
        "Card 2"
    with ui.card():
        "Card 3"
    with ui.card():
        "Card 4"
from shiny import App, ui

app_ui = ui.page_fillable(
    ui.layout_column_wrap(  
        ui.card("Card 1"),
        ui.card("Card 2"),
        ui.card("Card 3"),
        ui.card("Card 4"),
        width=1 / 2,  
    ),
)


def server(input, output, session):
    pass


app = App(app_ui, server=server)

When width is a CSS unit, like "300px", ui.layout_column_wrap() ensures that every item is at least as wide as width. The items in the grid expand to fill horizontal space or contract when the layout container is smaller than width.

Try resizing the app preview below to see how the layout shifts at different container widths.

#| '!! 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: 240

## file: app.py
from shiny import App, ui

app_ui = ui.page_fillable(
    ui.layout_column_wrap(  
        ui.card("Card 1"),
        ui.card("Card 2"),
        ui.card("Card 3"),
        ui.card("Card 4"),
        width="300px",  
    ),
)


def server(input, output, session):
    pass


app = App(app_ui, server=server)
from shiny.express import ui

ui.page_opts(fillable=True)

with ui.layout_column_wrap(width="300px"):  
    with ui.card():
        "Card 1"
    with ui.card():
        "Card 2"
    with ui.card():
        "Card 3"
    with ui.card():
        "Card 4"
from shiny import App, ui

app_ui = ui.page_fillable(
    ui.layout_column_wrap(  
        ui.card("Card 1"),
        ui.card("Card 2"),
        ui.card("Card 3"),
        ui.card("Card 4"),
        width="300px",  
    ),
)


def server(input, output, session):
    pass


app = App(app_ui, server=server)

Column nesting

Both ui.layout_columns() and ui.layout_column_wrap() can be nested, even within each other.

Consider this page layout:

#| '!! 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: 200

## file: app.py
from shiny import App, ui

app_ui = ui.page_fillable(
    ui.layout_columns(
        ui.card("Card 1"),
        ui.card(
            "Card 2",
            ui.layout_columns(
                ui.card("Card 2.1"),
                ui.card("Card 2.2"),
                width=1 / 2,
            ),
        ),
        col_widths=(4, 8),
    ),
)


def server(input, output, session):
    return None


app = App(app_ui, server)
from shiny.express import ui

ui.page_opts(fillable=True)

with ui.layout_columns(col_widths=(4, 8)):
    with ui.card():
        "Card 1"
    with ui.card():
        "Card 2"

        with ui.layout_column_wrap(width=1 / 2):
            with ui.card():
                "Card 2.1"
            with ui.card():
                "Card 2.2"
from shiny import App, ui

app_ui = ui.page_fillable(
    ui.layout_columns(
        ui.card("Card 1"),
        ui.card(
            "Card 2",
            ui.layout_columns(
                ui.card("Card 2.1"),
                ui.card("Card 2.2"),
                width=1 / 2,
            ),
        ),
        col_widths=(4, 8),
    ),
)


def server(input, output, session):
    return None


app = App(app_ui, server)

Controlling for page width and height

There are multiple types of Shiny grids: fluid, fixed, and fillable. The fluid grid system is recommended for most applications and is the default for Shiny functions like ui.page_navbar() and ui.page_sidebar(). So far, all the examples on this page have used the fluid grid system.

Each grid system uses a flexibly sub-dividable 12-column grid for layout. They differ in how they interact with the viewer’s browser window:

  • The fluid system always occupies the full width of the web page and re-sizes its components dynamically as the size of the page changes.

  • The fixed system occupies a fixed width of 940 pixels by default and may assume other widths when Bootstrap’s responsive layout kicks in (e.g., when on a tablet).

  • The fillable system always occupies the full width and height of the web page and re-sizes its components dynamically as the size of the page changes.

“Differences between the three systems”

Fluid Grid System

#| '!! 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: 500

## file: app.py
import matplotlib.pyplot as plt
import numpy as np
from shiny import App, render, ui

app_ui = ui.page_fluid(  
    ui.layout_sidebar(
        ui.sidebar(
            ui.input_slider("n", "N", 0, 100, 20),
        ),
        ui.output_plot("histogram"),
    ),
)  


def server(input, output, session):
    @output
    @render.plot(alt="A histogram")
    def histogram():
        np.random.seed(19680801)
        x = 100 + 15 * np.random.randn(437)
        plt.hist(x, input.n(), density=True)


app = App(app_ui, server, debug=True)
import matplotlib.pyplot as plt
import numpy as np
from shiny.express import input, render, ui

with ui.sidebar():
    ui.input_slider("n", "N", 0, 100, 20)


@render.plot(alt="A histogram")
def histogram():
    np.random.seed(19680801)
    x = 100 + 15 * np.random.randn(437)
    plt.hist(x, input.n(), density=True)
import matplotlib.pyplot as plt
import numpy as np
from shiny import App, render, ui

app_ui = ui.page_fluid(  
    ui.layout_sidebar(
        ui.sidebar(
            ui.input_slider("n", "N", 0, 100, 20),
        ),
        ui.output_plot("histogram"),
    ),
)  


def server(input, output, session):
    @output
    @render.plot(alt="A histogram")
    def histogram():
        np.random.seed(19680801)
        x = 100 + 15 * np.random.randn(437)
        plt.hist(x, input.n(), density=True)


app = App(app_ui, server, debug=True)

To make a fluid grid that adapts to the width of the user’s viewing window, build your app UI with ui.page_fluid.

Fixed Grid System

#| '!! 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: 500

## file: app.py
import matplotlib.pyplot as plt
import numpy as np
from shiny import App, render, ui

app_ui = ui.page_fixed(  
    ui.layout_sidebar(
        ui.sidebar(
            ui.input_slider("n", "N", 0, 100, 20),
        ),
        ui.output_plot("histogram"),
    ),
)  


def server(input, output, session):
    @output
    @render.plot(alt="A histogram")
    def histogram():
        np.random.seed(19680801)
        x = 100 + 15 * np.random.randn(437)
        plt.hist(x, input.n(), density=True)


app = App(app_ui, server, debug=True)
import matplotlib.pyplot as plt
import numpy as np
from shiny.express import input, render, ui

with ui.sidebar():
    ui.input_slider("n", "N", 0, 100, 20)


@render.plot(alt="A histogram")
def histogram():
    np.random.seed(19680801)
    x = 100 + 15 * np.random.randn(437)
    plt.hist(x, input.n(), density=True)
import matplotlib.pyplot as plt
import numpy as np
from shiny import App, render, ui

app_ui = ui.page_fixed(  
    ui.layout_sidebar(
        ui.sidebar(
            ui.input_slider("n", "N", 0, 100, 20),
        ),
        ui.output_plot("histogram"),
    ),
)  


def server(input, output, session):
    @output
    @render.plot(alt="A histogram")
    def histogram():
        np.random.seed(19680801)
        x = 100 + 15 * np.random.randn(437)
        plt.hist(x, input.n(), density=True)


app = App(app_ui, server, debug=True)

To make a fixed grid that maintains a constant maximum width, build your app UI with ui.page_fluid.

The fixed grid system maintains a fixed page width of 940 pixels by default. If Bootstrap responsive features are enabled (they are by default in Shiny) then the grid will also adapt to be 724px or 1170px wide depending on your viewport (e.g., when on a tablet). The fixed system does this by using Bootstrap .containers, whereas the fluid system uses Bootstrap .container-fluids. Learn more about the difference here.

The main benefit of a fixed grid is that it provides stronger guarantees about how users will see the various elements of your UI laid out (because it’s not being dynamically laid out according to the width of the browser). The main drawback is that a fixed grid is more complex to work with. In general, we recommend using a fluid grid unless you absolutely require the lower level layout control afforded by a fixed grid.

Fillable Grid System

#| '!! 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: 425

## file: app.py
import matplotlib.pyplot as plt
import numpy as np
from shiny import App, render, ui

app_ui = ui.page_fillable(  
    ui.layout_sidebar(
        ui.sidebar(
            ui.input_slider("n", "N", 0, 100, 20),
        ),
        ui.output_plot("histogram", height="100%"),  
    ),
)  


def server(input, output, session):
    @output
    @render.plot(alt="A histogram")
    def histogram():
        np.random.seed(19680801)
        x = 100 + 15 * np.random.randn(437)
        plt.hist(x, input.n(), density=True)


app = App(app_ui, server, debug=True)
import matplotlib.pyplot as plt
import numpy as np
from shiny.express import input, render, ui

ui.page_opts(fillable=True)

with ui.sidebar():
    ui.input_slider("n", "N", 0, 100, 20)


@render.plot(alt="A histogram")
def histogram():
    np.random.seed(19680801)
    x = 100 + 15 * np.random.randn(437)
    plt.hist(x, input.n(), density=True)
import matplotlib.pyplot as plt
import numpy as np
from shiny import App, render, ui

app_ui = ui.page_fillable(  
    ui.layout_sidebar(
        ui.sidebar(
            ui.input_slider("n", "N", 0, 100, 20),
        ),
        ui.output_plot("histogram", height="100%"),  
    ),
)  


def server(input, output, session):
    @output
    @render.plot(alt="A histogram")
    def histogram():
        np.random.seed(19680801)
        x = 100 + 15 * np.random.randn(437)
        plt.hist(x, input.n(), density=True)


app = App(app_ui, server, debug=True)

To make a fixed grid that maintains a constant maximum width, build your app UI with ui.page_fillable.

ui.page_fluid() and ui.page_fixed() create web pages that are laid out from the top down, leaving whitespace at the bottom if the page content’s height is smaller than the browser window, and scrolling if the content is larger than the window.

ui.page_fillable() is designed to latch the document body’s size to the size of the window. This makes it possible to fill it with content that also scales to the size of the window.

For example, ui.page_fluid(ui.output_plot("plot", height="100%")) will not work as expected. The plot element’s effective height will be 0 because the plot’s containing elements (<div> and <body>) have automatic height. They determine their own height based on the height of their contained elements. However, ui.page_fillable(ui.output_plot("plot", height="100%")) will work as expected because ui.page_fillable() fixes the <body> height at 100% of the window height.

Note that ui.page_fillable(ui.output_plot("plot")) may not cause the plot to fill the page. Like most Shiny output widgets, ui.output_plot()’s default height is a fixed number of pixels. You must explicitly set height="100%" if you want a plot (or htmlwidget, say) to fill its container.

One must be careful what layouts/panels/elements come between the ui.page_fillable() and the plots/widgets. Any container that has an automatic height will cause children with height="100%" to misbehave. Stick to functions that are designed for fill layouts, such as the ones in the Shiny package.

Bringing your own HTML page

Sometimes you don’t want Shiny to build the page at all, because you already have a complete HTML document, such as the index.html your JavaScript bundler emits, or a design handed to you by someone else. ui.page_html() serves that document as-is: you own the markup, and Shiny only fills in its own HTML dependencies and wires up any elements whose id matches an input or output.

#| '!! 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: 220

## file: app.py
from pathlib import Path

from shiny import App, render, ui

app_ui = ui.page_html(Path(__file__).parent / "custom-page.html")  


def server(input, output, session):
    @render.text
    def greeting():
        return f"Hello, {input.name()}!"


app = App(app_ui, server)


## file: custom-page.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>A page Shiny did not build</title>
    <!-- Shiny's dependencies (and any `extra_deps=`) replace this tag -->
    <meta name="shiny-dependency-placeholder" content="">
  </head>
  <body class="p-4">
    <h4>A complete HTML document, served as-is</h4>
    <label class="form-label" for="name">Your name</label>
    <input id="name" type="text" class="form-control" value="Shiny">
    <p class="mt-3"><strong id="greeting" class="shiny-text-output"></strong></p>
  </body>
</html>
from pathlib import Path

from shiny.express import input, render, ui

ui.page_opts(html=Path(__file__).parent / "custom-page.html")  


@render.text
def greeting():
    return f"Hello, {input.name()}!"
from pathlib import Path

from shiny import App, render, ui

app_ui = ui.page_html(Path(__file__).parent / "custom-page.html")  


def server(input, output, session):
    @render.text
    def greeting():
        return f"Hello, {input.name()}!"


app = App(app_ui, server)

The document itself is ordinary HTML:

custom-page.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>A page Shiny did not build</title>
    <!-- Shiny's dependencies (and any `extra_deps=`) replace this tag -->
    <meta name="shiny-dependency-placeholder" content="">
  </head>
  <body class="p-4">
    <h4>A complete HTML document, served as-is</h4>
    <label class="form-label" for="name">Your name</label>
    <input id="name" type="text" class="form-control" value="Shiny">
    <p class="mt-3"><strong id="greeting" class="shiny-text-output"></strong></p>
  </body>
</html>

A few things to know:

  • Pass the document as a string, or as a pathlib.Path to an HTML file (read fresh on every call). Use App(ui=lambda request: ui.page_html(...)) rather than App(ui=ui.page_html(...)) if your app supports bookmarking, which requires a UI function.

  • Shiny’s HTML dependencies, plus anything you pass to extra_deps=, are inserted where the document has <meta name="shiny-dependency-placeholder" content="">. If your document marks that spot some other way, name it with deps_replace_pattern=. Without a placeholder Shiny has nowhere to put its CSS and JavaScript, and the page won’t be reactive.

  • In Express mode, pass the document to ui.page_opts(html=), which routes the whole app through ui.page_html(). Top-level UI markup you write in the app file is dropped, since the document already is the page, but the HTML dependencies of those elements are kept.

This is the Python counterpart to Shiny for R’s shinyApp(ui = htmlTemplate("index.html", document_ = TRUE)) with attachDependencies().