Offcanvas

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

from shiny import App, Inputs, Outputs, Session, render, ui

app_ui = ui.page_fluid(
    ui.offcanvas(
        ui.p("An offcanvas panel slides in from the edge of the page."),
        ui.p("Close it with the button, the backdrop, or the Escape key."),
        title="Details",
        trigger=ui.input_action_button("open", "Open panel"),
        id="panel",
    ),
    ui.output_code("state"),
)

def server(input: Inputs, output: Outputs, session: Session):
    @render.code
    def state():
        return f"Panel is {'open' if input.panel() else 'closed'}"

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

ui.offcanvas(  
    ui.p("An offcanvas panel slides in from the edge of the page."),
    ui.p("Close it with the button, the backdrop, or the Escape key."),
    title="Details",
    trigger=ui.input_action_button("open", "Open panel"),  
    id="panel",  
)

@render.code
def state():
    return f"Panel is {'open' if input.panel() else 'closed'}"  
from shiny import App, Inputs, Outputs, Session, render, ui

app_ui = ui.page_fluid(
    ui.offcanvas(  
        ui.p("An offcanvas panel slides in from the edge of the page."),
        ui.p("Close it with the button, the backdrop, or the Escape key."),
        title="Details",
        trigger=ui.input_action_button("open", "Open panel"),  
        id="panel",  
    ),
    ui.output_code("state"),
)

def server(input: Inputs, output: Outputs, session: Session):
    @render.code
    def state():
        return f"Panel is {'open' if input.panel() else 'closed'}"  

app = App(app_ui, server=server)
No matching items

Relevant Functions

  • ui.offcanvas
    ui.offcanvas(*args, title=None, footer=None, trigger=None, id=None, placement='right', width=None, height=None, close_button=True, backdrop=True, scroll=False, keyboard=True, **kwargs)

  • ui.show_offcanvas
    ui.show_offcanvas(offcanvas, *, session=None)

  • ui.hide_offcanvas
    ui.hide_offcanvas(id, *, session=None)

  • ui.toggle_offcanvas
    ui.toggle_offcanvas(id, show=None, *, session=None)

No matching items

Details

An offcanvas panel slides in from an edge of the page, on top of the rest of the app, and stays there until it is dismissed. It is a good place for content that is useful but secondary — filters, help text, details about the selected row — that you don’t want taking up room in the main layout.

Create one with ui.offcanvas(), passing the panel’s body as positional arguments. title adds a header, and footer adds content pinned below the body. In both Shiny Core and Shiny Express, ui.offcanvas() is a regular function call rather than a context manager: put it anywhere in your UI, and it renders nothing visible until it is opened.

Every panel needs a way to be opened, so ui.offcanvas() requires either a trigger or an id:

  • trigger takes a UI element — usually an action button or a link — that opens the panel when clicked. The trigger is rendered where you placed the ui.offcanvas() call, as a sibling of the panel itself.
  • id lets the server open and close the panel with ui.toggle_offcanvas() and ui.hide_offcanvas(), and makes the panel’s open/closed state available as a reactive value, input.<id>().

You can use both together, as the example above does: a button opens the panel, and input.panel() reports whether it is open.

Dismissing a panel

By default a panel can be dismissed in three ways: the close button in its header, clicking the backdrop behind it, or pressing Esc. Each is adjustable:

  • close_button=False removes the close button from the header.
  • backdrop=False renders no backdrop at all, leaving the rest of the app clickable while the panel is open; backdrop='static' keeps the backdrop but ignores clicks on it, so the panel can’t be closed by accident.
  • keyboard=False stops Esc from closing the panel.
  • scroll=True lets the page behind the panel keep scrolling while the panel is open.

If you turn off enough of these that the user has no way out, be sure the server closes the panel for them with ui.hide_offcanvas().

Building a panel from the server

ui.show_offcanvas() inserts a panel into the page and opens it in one step. Unlike the other two mutators it takes the object returned by ui.offcanvas(), not an id — ui.show_offcanvas("my_id") raises AttributeError. That makes it the right choice when the panel’s content depends on the state of the app; to open a panel that is already in your UI, use ui.toggle_offcanvas(id, show=True) instead. If you give that panel an id, it stays in the page after it is closed, and calling ui.show_offcanvas() again simply reveals it — the content is not re-rendered. To keep an id’d panel’s content current, put reactive outputs inside it and let them update; without an id, the panel is temporary and is removed from the page when it closes.

Accessibility

The panel is labelled by its title, so a panel without one is unlabelled for screen readers; ui.offcanvas() warns in that case. If you intentionally omit title, pass an aria-label (or aria-labelledby) attribute instead:

ui.offcanvas(
    ui.p("No visible header."),
    trigger=ui.input_action_button("open", "Open panel"),
    **{"aria-label": "Filters"},
)

Prefer a natively keyboard-accessible trigger, such as a button or a link, so the panel can be opened without a mouse.

See Also: a sidebar holds secondary content permanently alongside your app rather than on top of it, and collapses on narrow screens. Modal messages also cover the page, but demand attention now rather than offering content on the side. Popovers and tooltips attach a small amount of content to a specific element.

Variations

Choose which edge the panel comes from

placement sets the edge the panel slides in from: 'right' (the default), 'left', 'top', or 'bottom'. Size a left or right panel with width, and a top or bottom panel with height. Setting width on a top or bottom panel (or height on a left or right one) has no effect and emits a warning.

#| '!! 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.offcanvas(
        ui.p("This panel slides in from the left."),
        title="Left",
        placement="left",
        width=300,
        trigger=ui.input_action_button("open_left", "From the left"),
        id="left_panel",
    ),
    ui.offcanvas(
        ui.p("This panel slides in from the top."),
        title="Top",
        placement="top",
        height=200,
        trigger=ui.input_action_button("open_top", "From the top"),
        id="top_panel",
    ),
    ui.offcanvas(
        ui.p("This panel slides in from the bottom."),
        title="Bottom",
        placement="bottom",
        height=200,
        trigger=ui.input_action_button("open_bottom", "From the bottom"),
        id="bottom_panel",
    ),
)

def server(input, output, session):
    pass

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

ui.offcanvas(
    ui.p("This panel slides in from the left."),
    title="Left",
    placement="left",  
    width=300,  
    trigger=ui.input_action_button("open_left", "From the left"),
    id="left_panel",
)

ui.offcanvas(
    ui.p("This panel slides in from the top."),
    title="Top",
    placement="top",  
    height=200,  
    trigger=ui.input_action_button("open_top", "From the top"),
    id="top_panel",
)

ui.offcanvas(
    ui.p("This panel slides in from the bottom."),
    title="Bottom",
    placement="bottom",  
    height=200,  
    trigger=ui.input_action_button("open_bottom", "From the bottom"),
    id="bottom_panel",
)
from shiny import App, ui

app_ui = ui.page_fluid(
    ui.offcanvas(
        ui.p("This panel slides in from the left."),
        title="Left",
        placement="left",  
        width=300,  
        trigger=ui.input_action_button("open_left", "From the left"),
        id="left_panel",
    ),
    ui.offcanvas(
        ui.p("This panel slides in from the top."),
        title="Top",
        placement="top",  
        height=200,  
        trigger=ui.input_action_button("open_top", "From the top"),
        id="top_panel",
    ),
    ui.offcanvas(
        ui.p("This panel slides in from the bottom."),
        title="Bottom",
        placement="bottom",  
        height=200,  
        trigger=ui.input_action_button("open_bottom", "From the bottom"),
        id="bottom_panel",
    ),
)

def server(input, output, session):
    pass

app = App(app_ui, server)

Open and close the panel from the server

Give the panel an id and it no longer needs a trigger of its own: call ui.toggle_offcanvas() to flip it (or pass show=True/show=False to force a direction) and ui.hide_offcanvas() to close it. The same id is also a reactive value, input.<id>(), that reports whether the panel is open. Note that opening a panel by id is ui.toggle_offcanvas(id, show=True), not ui.show_offcanvas() — that one takes the object returned by ui.offcanvas(), and passing it an id string raises AttributeError. Note also where the Hide button lives: while the panel is open, its backdrop covers the rest of the page, so a control that closes the panel belongs inside the panel.

#| '!! 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, Inputs, Outputs, Session, reactive, render, ui

app_ui = ui.page_fluid(
    ui.offcanvas(
        ui.p("This panel has no trigger of its own; the server opens and closes it."),
        ui.input_action_button("hide", "Hide"),
        title="Server controlled",
        id="panel",
    ),
    ui.input_action_button("show", "Show"),
    ui.input_action_button("toggle", "Toggle"),
    ui.output_code("state"),
)

def server(input: Inputs, output: Outputs, session: Session):
    @reactive.effect
    @reactive.event(input.show)
    def _():
        ui.toggle_offcanvas("panel", show=True)

    @reactive.effect
    @reactive.event(input.hide)
    def _():
        ui.hide_offcanvas("panel")

    @reactive.effect
    @reactive.event(input.toggle)
    def _():
        ui.toggle_offcanvas("panel")

    @render.code
    def state():
        return f"Panel is {'open' if input.panel() else 'closed'}"

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

ui.offcanvas(
    ui.p("This panel has no trigger of its own; the server opens and closes it."),
    ui.input_action_button("hide", "Hide"),  
    title="Server controlled",
    id="panel",  
)

ui.input_action_button("show", "Show")
ui.input_action_button("toggle", "Toggle")

@render.code
def state():
    return f"Panel is {'open' if input.panel() else 'closed'}"

@reactive.effect
@reactive.event(input.show)
def show_panel():
    ui.toggle_offcanvas("panel", show=True)  

@reactive.effect
@reactive.event(input.hide)
def hide_panel():
    ui.hide_offcanvas("panel")  

@reactive.effect
@reactive.event(input.toggle)
def toggle_panel():
    ui.toggle_offcanvas("panel")  
from shiny import App, Inputs, Outputs, Session, reactive, render, ui

app_ui = ui.page_fluid(
    ui.offcanvas(
        ui.p("This panel has no trigger of its own; the server opens and closes it."),
        ui.input_action_button("hide", "Hide"),  
        title="Server controlled",
        id="panel",  
    ),
    ui.input_action_button("show", "Show"),
    ui.input_action_button("toggle", "Toggle"),
    ui.output_code("state"),
)

def server(input: Inputs, output: Outputs, session: Session):
    @reactive.effect
    @reactive.event(input.show)
    def _():
        ui.toggle_offcanvas("panel", show=True)  

    @reactive.effect
    @reactive.event(input.hide)
    def _():
        ui.hide_offcanvas("panel")  

    @reactive.effect
    @reactive.event(input.toggle)
    def _():
        ui.toggle_offcanvas("panel")  

    @render.code
    def state():
        return f"Panel is {'open' if input.panel() else 'closed'}"

app = App(app_ui, server=server)

Create a panel from the server

When the panel’s content isn’t known until the app is running, skip the UI entirely and build the panel in the server function, then insert and reveal it with ui.show_offcanvas(). It takes the object returned by ui.offcanvas() rather than an id.

#| '!! 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, Inputs, Outputs, Session, reactive, ui

app_ui = ui.page_fluid(
    ui.input_action_button("show", "Show panel"),
)

def server(input: Inputs, output: Outputs, session: Session):
    @reactive.effect
    @reactive.event(input.show)
    def _():
        ui.show_offcanvas(
            ui.offcanvas(
                ui.p("This panel was created by the server, not the UI."),
                title="Server panel",
                placement="left",
                id="server_panel",
            )
        )

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

ui.input_action_button("show", "Show panel")

@reactive.effect
@reactive.event(input.show)
def show_server_panel():
    ui.show_offcanvas(  
        ui.offcanvas(
            ui.p("This panel was created by the server, not the UI."),
            title="Server panel",
            placement="left",
            id="server_panel",
        )
    )
from shiny import App, Inputs, Outputs, Session, reactive, ui

app_ui = ui.page_fluid(
    ui.input_action_button("show", "Show panel"),
)

def server(input: Inputs, output: Outputs, session: Session):
    @reactive.effect
    @reactive.event(input.show)
    def _():
        ui.show_offcanvas(  
            ui.offcanvas(
                ui.p("This panel was created by the server, not the UI."),
                title="Server panel",
                placement="left",
                id="server_panel",
            )
        )

app = App(app_ui, server=server)
No matching items