st-pages declares Streamlit multipage navigation in a TOML file
An experimental version of Streamlit Multi-Page Apps
At a glance
- What is it?
- st-pages is a small Python package that lets you list a Streamlit app's pages in a TOML file rather than deriving them from filenames, with support for sections, custom URL paths, icons and per-viewer page hiding. It suits Streamlit developers who want their navigation declared rather than inferred, and since version 1.0.0 it is a thin wrapper around native Streamlit multipage support.
- Who is it for?
- Adopt st-pages if you want your Streamlit navigation declared in a file you can read and diff, or if you need sections, which native Streamlit does not offer in the same declarative form, and note that the package's own maintainer points you at the Streamlit multipage documentation as the first place to check.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 4 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What changed at 1.0.0, and why the package shrank
The most useful thing in the README is the section explaining why st-pages still exists, because the answer has changed since the package was first written.
Previously, st-pages allowed for a much more customizable and flexible declaration of pages in a Streamlit app, and was independent of the actual filenames of the python files in your project. That was the original value proposition: you could name your pages however you wanted and place them wherever you wanted, rather than having Streamlit infer both from the files in a directory.
As of 1.0.0, the README says st-pages is now a tiny wrapper that provides an easy method for defining the pages in your app in a toml file, as well as a few utility methods to let you add the current page's title to all pages, and so on.
That is a considerable reduction in scope, and the README is direct about what it means for users. You are welcome to continue to use older versions of this package, but most of the old use-cases are now easy to do with native streamlit, so the recommendation is to check out the Streamlit documentation for the page and navigation concepts.
This is a useful pattern to recognise in general. A package that predates a framework feature will either find a new purpose around the edges of that feature, or shrink to a convenience wrapper. st-pages did the second, and the honest thing to note is that the sections support and the TOML declaration are the parts that survive, while the filename independence is gone.
The package metadata reflects the current state. The pyproject.toml describes it as an experimental version of Streamlit Multi-Page Apps, version 1.0.1, licensed MIT, authored by Zachary Blackwood, and it requires Python 3.9 and Streamlit 1.36.0 or newer.
Declaring pages in pages.toml, including the URL path override
The mechanism is a TOML file that lists pages as an array of tables, and the file lives under .streamlit.
The contents of .streamlit/pages.toml are:
[[pages]]
path = "page1.py"
name = "Home"
icon = "🏠"
[[pages]]
path = "other_pages/page2.py"
name = "Page 2"
icon = ":books:"
url_path = "my_books" # You can override the default url path for a pageEach entry names the script to run, the label to show, and an icon that can be either an emoji or a shortcode such as :books:. The optional url_path overrides the default URL segment for that page, so the example page becomes my_books in the browser rather than the path derived from its file.
The key-value naming is worth noticing because it is the one thing that is not native. The path field is what makes this a declaration rather than a convention: your scripts can sit in a subdirectory such as other_pages/, and the ordering in the file is the ordering in the navigation, neither of which has to match what a filename scan would produce.
The package ships two utility methods alongside the declaration. One is for adding the current page's title to all pages, which is the kind of small thing that is tedious to do by hand in every script. The other is the navigation builder that reads the TOML and produces the structure Streamlit's navigation call expects.
The README points at two live examples, a basic one at st-pages.streamlit.app and an example with sections at st-pages-sections.streamlit.app, and the package is published on PyPI as st-pages with a Streamlit app badge.
Sections, and the indentation rule that follows from them
The sections example is the feature that native Streamlit does not give you in the same declarative form, and it is worth understanding how it works.
The example file is .streamlit/pages_sections.toml:
[[pages]]
path = "page1.py"
name = "Home"
icon = "🏠"
[[pages]]
path = "other_pages/page2.py"
name = "Page 2"
icon = ":books:"
[[pages]]
name = "My section"
is_section = true
# Pages after an `is_section = true` will be indented
[[pages]]
name = "Another page"
icon = "💪"A section is an entry with is_section = true and no path, because it does not correspond to a script. It is a heading in the navigation rather than a page you can click through to. And the rule the comment states is positional: pages after an is_section = true will be indented.
That positional rule is the whole design of the feature, and it is also its main constraint. The grouping of pages under a section is implied by the order of the file rather than declared, so you cannot interleave two sections and come back to the first. A file with a section, three pages, another section and three more pages works, because the order is the structure, but reordering the file for any other reason silently changes the navigation.
It is a reasonable trade for a TOML file, and it is the kind of thing that is worth knowing before you reorganise the file later. The toml is also the natural place for a comment, and the example uses one to explain the indentation rule.
Wiring it into the app with get_nav_from_toml and st.navigation
The Streamlit side is a handful of lines, and the shape of it is the interesting part.
import streamlit as st
from st_pages import add_page_title, get_nav_from_toml
st.set_page_config(layout="wide")
# If you want to use the no-sections version, this
# defaults to looking in .streamlit/pages.toml, so you can
# just call `get_nav_from_toml()`
nav = get_nav_from_toml(".streamlit/pages_sections.toml")
st.logo("logo.png")
pg = st.navigation(nav)
add_page_title(pg)
pg.run()Three things are worth noting. get_nav_from_toml() with no argument defaults to looking in .streamlit/pages.toml, which is why the simple case needs no path at all. st.navigation(nav) is the native Streamlit call, so the package produces the navigation structure and Streamlit does the navigation, which is where the wrapper idea comes from in practice. And add_page_title(pg) is the utility method that uses the page object to set the title, which is the small repetitive piece the package removes from every script.
The comment in the code mentions the no-sections version, which is a slightly confusing phrase for what it is. It means the version of the file without section entries, that is the pages.toml from the earlier example, not a different mode of the package.
Note also that the pages.toml file is read relative to the path you pass, and the default is the .streamlit directory of the app. That directory is where Streamlit keeps its own configuration, so the pages file lives alongside config.toml and secrets.toml rather than in your source tree.
Hiding pages for one viewer without hiding them for another
The hide_pages function is the feature with the most unusual semantics in the package, and the README is careful about it.
You can now pass a list of page names to hide_pages to hide pages from now on. This list is custom to each viewer of the app, so you can hide pages from one viewer but not from another using this method.
from st_pages import hide_pages
hide_pages(["Another page"])The subtlety is that the hiding is per viewer and not per app. That means the list cannot simply be a constant baked into the source, because a constant would hide the page for everyone. The README points at another example of hiding pages in the Streamlit documentation on dynamic navigation, which is where the pattern of deriving the list from the viewer's identity belongs.
That capability exists because Streamlit's navigation model lets the app decide what to show, and this package sits at the point where the navigation structure is assembled. The README notes you can see another example of hiding pages in the docs, so the practical answer for how to combine this with per-user logic is in the framework's own documentation rather than in the package.
It is also worth being clear about the limitation this feature carries. Hiding a page hides it from the navigation, and the README does not describe any accompanying access control on the page script itself. So this is a navigation concern, not a security boundary, and a page hidden from the menu is still a script that runs if someone opens its URL directly.
Where st-pages is the wrong choice
The package's own README points you at native Streamlit for most of what it used to do, and that is the honest answer for anyone starting fresh.
If your app has a handful of pages and you are happy with the convention of naming files page1.py, page2.py and putting them in a pages directory, native Streamlit multipage support does that without a dependency. The pyproject.toml already requires Streamlit 1.36.0 or newer, so the native features are already available in the environment the package runs in, and adding a package on top of them to get the same result is a cost with no benefit.
If you need filename independence, that capability is gone in 1.0.0. The old versions supported it, and the README invites you to keep using them if that is what you need, but a 1.0.0-era codebase on an old package version is a dependency you will eventually have to move.
The third case is where your navigation depends on the viewer's role. hide_pages takes a list of page names rather than page objects, so it hides by display name, which means two pages with the same name are indistinguishable, and renaming a page in the TOML silently changes what hide_pages matches. That is a fragile coupling for anything load-bearing, and the README points to the framework documentation for the pattern that handles this properly.
For comparison, the alternatives differ in kind. Native Streamlit multipage support infers pages from files and requires no dependency but gives you no declarative file and no sections. A hand-written navigation list in Python gives you full control and costs you the maintenance of keeping it in sync, and it also gives you back the per-viewer flexibility without a name-based matching hack. st-pages is the option in between: a file you can read and diff, with a couple of conveniences, for the case where a declared navigation file is worth more to you than the dependency.
The repository also has the usual signs of a maintained package rather than a script, with a src/ layout, tests/, an example_app/ directory, tox.ini, a poetry.lock, a pre-commit configuration and a Ruff setup with a long select list. The author is @blackary, who works on Streamlit, and the package is published to PyPI as st-pages.
Editorial conclusion
Adopt st-pages if you want your Streamlit navigation declared in a file you can read and diff, or if you need sections, which native Streamlit does not offer in the same declarative form, and note that the package's own maintainer points you at the Streamlit multipage documentation as the first place to check. Do not adopt the older versions, because the README says that as of 1.0.0 the package is a tiny wrapper and that most previous use cases are now easy to do with native Streamlit. Before you upgrade, read the Why st-pages section and check that your Streamlit version is 1.36.0 or newer, which is the stated minimum, and remember that pages.toml has to live under .streamlit. The licence is MIT, the latest release is v1.0.1 from 2024-08-08, and the last push was on 2026-10-19.
Frequently asked questions
What does st-pages do for Streamlit?
It lets you declare the pages of a Streamlit app in a TOML file instead of deriving them from filenames, and adds a few utility methods such as adding the current page's title to all pages. Since version 1.0.0 the package describes itself as a tiny wrapper around native Streamlit multipage support.
How do I install st-pages?
With pip install st-pages. The package is published on PyPI as st-pages and requires Python 3.9 and Streamlit 1.36.0 or newer, according to the pyproject.toml.
Where does the pages TOML file go and what does it contain?
It goes in the .streamlit directory of your app, as pages.toml by default. It contains a series of [[pages]] entries, each with a path to the script, a name, an icon, and optionally a url_path to override the default URL segment.
How do I create sections in a st-pages navigation?
Add a [[pages]] entry with is_section = true and a name but no path. Pages listed after it in the file are indented under that section, so the grouping depends on the order of the entries in the TOML file.
Does hide_pages hide a page from everyone?
No. The README says the list is custom to each viewer of the app, so you can hide pages from one viewer but not from another. The function takes a list of page names, and the README points at the Streamlit dynamic navigation documentation for another example of the pattern.
Is st-pages still needed now that Streamlit has native multipage support?
The README says that as of 1.0.0 st-pages is a tiny wrapper and that most of the old use cases are now easy to do with native Streamlit, recommending the Streamlit documentation on pages and navigation. Older versions remain usable if you rely on the filename independence they offered.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/blackary-st-pages)Was this useful?