2025, Nov 24 01:00
Importing a nav_panel into a Shiny for Python navset_tab without empty tabs: use expressify
Learn why a nav_panel moved to another file renders empty in a Shiny for Python navset_tab and how to fix it with expressify. Clear steps, examples, and code.
When a Shiny for Python app grows, pulling pieces of UI into separate modules is the natural next step. A common case is a nav panel that you want to define in another file and include inside the main app’s navset_tab. If you move the code as-is, you’ll often end up with an empty tab. Here is why that happens and how to do it correctly.
Reproducing the issue
Consider a main app with a navset_tab and a separate file that tries to provide an extra nav panel via a regular function. The function is called inside the navset, but the panel shows up empty.
app.py
import page as sections
from shiny.express import input, ui
with ui.navset_tab(id="navset_active_tab"):
with ui.nav_panel("Welcome", value="home_view"):
with ui.card():
ui.input_selectize(
"dummy_select", "Filler",
["list", "of", "items", "here"],
multiple=False
)
sections.extra_tab()
page.py
from shiny import render
from shiny.express import ui
def extra_tab():
with ui.nav_panel("Page I want to put in the main app", value="aux_view"):
@render.express
def text_stub():
"Filler text"
What actually happens
Wrapping UI-building statements in a regular function and calling it from inside your navset_tab returns only the function’s return value. Since this function doesn’t return anything, the effective value is None, and the additional nav panel ends up empty. In other words, the UI you “built” line by line inside the function isn’t what’s returned; only the function’s final value is, which isn’t what you want.
The fix
Use express.expressify to make the function behave like Shiny Express code that emits UI as it executes. This ensures that calling the function produces the UI defined by its lines, not just its return value.
page.py
from shiny import render
from shiny.express import ui, expressify
@expressify
def extra_tab():
with ui.nav_panel("Page I want to put in the main app", value="aux_view"):
@render.express
def text_stub():
"Filler text"
app.py
import page as sections
from shiny.express import input, ui
with ui.navset_tab(id="navset_active_tab"):
with ui.nav_panel("Welcome", value="home_view"):
with ui.card():
ui.input_selectize(
"dummy_select", "Filler",
["list", "of", "items", "here"],
multiple=False
)
sections.extra_tab()
Why this matters
Splitting UI across files keeps large applications maintainable, but Shiny Express code has a specific execution model. Without expressify, only a function’s return value is used, not the incremental UI you expect to “flow” from each line. expressify aligns function calls with Express’s “display as you go” behavior, so modular pieces render correctly when composed into a larger layout.
Takeaways
If you need to import a nav_panel (or any Express UI) from another file into a navset_tab, wrap the UI in a function and decorate it with express.expressify. Call that function where the UI should appear. This small change prevents empty tabs and preserves the intended incremental rendering of your Express UI.