📖 [Launched!] Multi-page apps: Improved API and new navigation UI features

Hi folks! :wave:

I wanted to preview a new way to define multi-page Streamlit apps and some new features coming to the side navigation. We’re aiming to release these updates within the next 2 months.

:warning::point_right: NOTE: this will be additive, the current MPA approach with pages/ will still work just fine :slightly_smiling_face:

This is cross-posted on GitHub issue #8388.

Update: Check out the :point_right: demo app :point_left: & whl file!

Summary

Using the new API, when you do streamlit run streamlit_app.py, the contents of streamlit_app.py will automatically run before every page instead of defining the home page. Any common code can go here.

  • We’re building a new API called st.navigation() with st.Page() to programmatically define the available pages of your app.
  • This means the available and displayed pages can change in a given session / rerun! (see code example below)
  • Besides defining your pages / nav, you can also add any common session setup code, authorization check, page_config, styles, etc only once in this file.
  • We also plan to add native support for an app logo at the top left, text headings between page groups in the native navigation, and support for Material icons in addition to emojis

Here’s how a native side navigation might look using all the new features:

image

Here’s the simplest example of how this would look in your app code:

# streamlit_app.py

# Define all the available pages, and return the current page
current_page = st.navigation([
    st.Page("hello.py", title="Hello World", icon=":material:Plane:"),
    st.Page("north_star.py", title="North Star", icon=":material:Star:"),
    # ...
])

# call run() to execute the current page
current_page.run()

Pages can be defined by path to a python file, or passing in a function.

def Page(
    page: str | Callable,
    *,
    title: str = None,
    icon: str = None, # emojis and material icons
    default: bool = False, # Explicitly set a default page ("/") for the app
    key: str = None, # set an identifier and /url-path for the page, otherwise inferred
)

New navigation UI

Here’s a fuller example with a logo and section headers.

st.logo("my_logo.png")

# Define all the available pages, and return the current page
current_page = st.navigation({
    "Overview": [
        st.Page("hello.py", title="Hello World", icon=":material:Plane:"),
        st.Page("north_star.py", title="North Star", icon=":material:Star:"),
    ],
    "Metrics": [
        st.Page("core_metrics.py", title="Core Metrics", icon=":material:Hourglass:"),
        # ...
    ],
})

# current_page is also a Page object you can .run()
current_page.run()

Logos

Calling st.logo(image) adds an app logo in the top left of your app, floating above the navigation / sidebar.

Material icons

You’ll be able to use a wide range of Material icons for page navigation and other elements that support icon= today. Our current plan is to support the Material icons built into @emotion. You can specify these via shortcode, such as icon=":material:Archive:". The final details of this might change a bit before release.

Navigation headers

By default, st.navigation expects a list of Pages (List[st.Page]). However you can also pass Dict[str: List[st.Page]]. In this case, each dictionary key becomes a section header in the navigation with the listed pages below. E.g.

current_page = st.navigation({
    "Overview": [ # "Overview" becomes a section header
        st.Page("hello.py"),
        st.Page("north_star.py"),
    ],
    "Metrics": [ # "Metrics becomes a section header
        st.Page("core_metrics.py"),
        # ...
    ],
})

Dynamic navigation

The available pages are re-assessed on each rerun. So, for example, if you want to add some pages only if the user is authenticated, you can just append them to the list passed to st.navigation based on some check. E.g.:

import streamlit as st

pages = [st.Page("home.py", title="Home", icon="🏠", default=True)]

if is_authenticated():
    pages.append(st.Page("step_1.py", title="Step 1", icon="1️⃣"))
    pages.append(st.Page("step_2.py", title="Step 2", icon="2️⃣"))

page = st.navigation(pages)

page.run()

You can also set position="hidden" on st.navigation if you want to use the new API while defining your own navigation UI (such as via st.page_link and st.switch_page).

# streamlit_app.py
import streamlit as st

pages = [
    st.Page("page1.py", title="Page 1", icon="📊"),
    st.Page("page2.py", title="Page 2", icon="🌀"),
    st.Page("page3.py", title="Page 3", icon="🧩"),
]

# Makes pages available, but position="hidden" means it doesn't draw the nav
# This is equivalent to setting config.toml: client.showSidebarNavigation = false
page = st.navigation([pages], position="hidden")

page.run()

# page1.py
st.write("Welcome to my app. Explore the sections below.")
col1, col2, col3 = st.columns(3)
col1.page_link("page1.py")
col2.page_link("page2.py")
col3.page_link("page3.py")

# page2.py
st.markdown(long_about_text)
if st.button("Back"):
    st.switch_page("page1.py")

We’re still putting the final touches on this feature so the final API and UI might change slightly. But we’re excited about this and wanted to share, so you know what’s coming and in case you have early feedback! Thanks!! :balloon::balloon:

24 Likes

Is it possible to use a user_logon check to control page visibility, similar to Flask? All tutorials I’ve tried don’t prevent the login page from reappearing upon refresh. This feature is crucial for user experience with that multi-page features

Yes, that should be possible and in fact pretty straightforward to do. Something like:

if not user_logon():
    pages = [st.Page("login.py", title="Login")]
else: # assuming you only want to show other pages after login
    pages = [...]

st.navigation(pages)

Does it make sense?

Understood, that’s exactly what we need for enhanced user experience with Streamlit. Thanks!

2 Likes

Is this an already existing feature?
Or, one that is to come on future updates

@Elvis not available yet but we’re targeting to release it within the next 2 months.

1 Like

Can we do deeplinks like I do now?

I’m excited for this for long time, hoping to see this soon

Hey @rcsmit, yes, you’ll still be able to deep-link using both the page path /my-page and ?query_params= with this approach, just like you can today. Also looking at other small improvements we can make in that area :slight_smile:

1 Like

It would be cool if page_link (or more generally any method of in-app navigation) also had the on_click parameter to execute a callable such as e.g. buttons do. This would allow creating links in a page where one can actually transfer some information to another page using the session state without creating a new session as it happens when one uses inks with query parameters.

Use case: I have a dropdown where I see the names of all my clients. Now I want to look at the detail data of one particular client. Obviously it doesn’t make sense to create an individual page per client, so the detail page is one generic page that is populated with that client’s data. Currently I have to create a button for each client, whose on_click callable writes the client id into the session state and then opens the detail page that reads said client_id from the session state and shows the corresponding data. The buttons work but they feel rather like a workaround than a real solution, being able to use page_link would be way more natural :wink:

1 Like

Another method is to use a popover and create an html list with a link to the client page in multi-page app. That link has a query parameter with the name of the client.

image

Once clicked, the client page will be shown carrying the client name in the query parameter. Capture that name, lookup some info with that name and display it.

1 Like

@ferdy already tried something with html links. Maybe I made a mistake but my experience is that clicking on such a hyperlink or generally working with query parameters will lead to a new streamlit session, meaning that whatever was written into the session state etc. is not available any more.

haven’t tried popovers yet as they are a feature just added in the very latest version :wink: – but they look really useful

Hi @hitbyfrozenfire

Good use case and makes sense!

Is it possible to store the current client selection of the dropdown in session state today, so it’s already available when the user clicks the page_link?

I think you are right that the method ferdy proposed will start a new session so it might not be ideal.

I think the more likely path we will take short term for your use case is allowing you to set query_params in the page_link. So you could add the customer ID in a query_param when the new page opens, and still remain within the same session. This is discussed here and something I want to add soon:

LMK what you think and whether that would meet your need!

That is correct the session is different. However you can pass the session state as query parameter. If there is an object in the session state such as a dataframe it needs to be serialized first.

@jcarroll thanks for your answer.

Is it possible to store the current client selection of the dropdown in session state today, so it’s already available when the user clicks the page_link?

I wouldn’t know how to do it. At the end the selection of a particular client is done by clicking on that client’s link, which is why I was thinking about that on_click functionality for page_link. If there’s already a way of getting the client id (or whatever one needs) into the session state and following the page_link without starting a new session let me know, as I am not aware that it is possible.

If there is an object in the session state such as a dataframe it needs to be serialized first.

When there are rather large or complex objects that take a wile to serialise this might cause quite some wait times which is why imho starting a new session might not be the optimal solution.

Makes sense. I think support query params in st.page_link when using file path · Issue #8112 · streamlit/streamlit · GitHub will be the best solution for this use case. Hoping we can get it added soon!

1 Like

Hi all, by popular request we now have a demo app and early preview whl file for you to play with! Check it out. The whl file is linked in the app right after you click on “Login”

Would love to hear how you find the feature!

Note: This is an early preview, there are some known bugs (will update the app to describe those tomorrow)

3 Likes

What happens to multi-page with pages folder?

@ferdy it will still work like today :slight_smile: And developers can choose which approach they want to use. Existing apps will still all work just like today.

Some of the new features like section headers and non-emoji icons in navigation will require using st.navigation(), but if you don’t need those you can just continue defining with files in pages/

I think you will see an error if you call st.navigation() in an app that also has pages/ defined, we might provide a config or something for that at some point - still TBD and open to feedback. But we don’t intend to fully support mixing both modes.

1 Like