|
| 1 | +--- |
| 2 | +title: Create custom navigation menus |
| 3 | +slug: /library/advanced-features/multipage-apps/custom-navigation |
| 4 | +description: Streamlit makes it easy to build a custom navigation menu in your multipage app. |
| 5 | +--- |
| 6 | + |
| 7 | +# Create custom navigation menus |
| 8 | + |
| 9 | +Streamlit lets you build custom navigation menus and elements with `st.page_link`. Introduced in Streamlit version 1.31.0, `st.page_link` can link to other pages in your multipage app or to external sites. When linked to another page in your app, `st.page_link` will show a highlight effect to indicate the current page. When combined with the [`client.showSidebarNavigation`](/library/advanced-features/configuration#client) configuration option, you can build sleek, dynamic navigation in your app. |
| 10 | + |
| 11 | +## Prerequisites |
| 12 | + |
| 13 | +Create a new working directory in your development environment. We'll call this directory `your-repository`. |
| 14 | + |
| 15 | +## Summary |
| 16 | + |
| 17 | +In this example, we'll build a dynamic navigation menu for a multipage app that depends on the current user's role. We've abstracted away the use of username and creditials to simplify the example. Instead, we'll use a selectbox on the main page of the app to switch between roles. Session State will carry this selection between pages. The app will have a main page (`app.py`) which serves as the abstracted log-in page. There will be three additional pages which will be hidden or accessible, depending on the current role. The file structure will be as follows: |
| 18 | + |
| 19 | +``` |
| 20 | +your-repository/ |
| 21 | +├── .streamlit/ |
| 22 | +│ └── config.toml |
| 23 | +├── pages/ |
| 24 | +│ ├── admin.py |
| 25 | +│ ├── super-admin.py |
| 26 | +│ └── user.py |
| 27 | +├── menu.py |
| 28 | +└── app.py |
| 29 | +``` |
| 30 | + |
| 31 | +Here's a look at what we'll build: |
| 32 | + |
| 33 | +<Cloud src="https://doc-custom-navigation.streamlit.app/?embed=true" height="400" /> |
| 34 | + |
| 35 | +## Build the example |
| 36 | + |
| 37 | +### Hide the default sidebar navigation |
| 38 | + |
| 39 | +When creating a custom navigation menu, you need to hide the default sidebar navigation using `client.showSidebarNavigation`. Add the following `.streamlit/config.toml` file to your working directory: |
| 40 | + |
| 41 | +```toml |
| 42 | +[client] |
| 43 | +showSidebarNavigation = false |
| 44 | +``` |
| 45 | + |
| 46 | +### Create a menu function |
| 47 | + |
| 48 | +You can write different menu logic for different pages or you can create a single menu function to call on multiple pages. In this example, we'll use the same menu logic on all pages, including a redirect to the main page when a user isn't logged in. We'll build a few helper functions to do this. |
| 49 | + |
| 50 | +- `menu_with_redirect()` checks if a user is logged in, then either redirects them to the main page or renders the menu. |
| 51 | +- `menu()` will call the correct helper function to render the menu based on whether the user is logged in or not. |
| 52 | +- `authenticated_menu()` will display a menu based on an authenticated user's role. |
| 53 | +- `unauthenticated_menu()` will display a menu for unauthenticated users. |
| 54 | + |
| 55 | +We'll call `menu()` on the main page and call `menu_with_redirect()` on the other pages. `st.session_state.role` will store the current selected role. If this value does not exist or is set to `None`, then the user is not logged in. Otherwise, it will hold the user's role as a string: `"user"`, `"admin"`, or `"super-admin"`. |
| 56 | + |
| 57 | +Add the following `menu.py` file to your working directory. (We'll describe the functions in more detail below.) |
| 58 | + |
| 59 | +```python |
| 60 | +import streamlit as st |
| 61 | + |
| 62 | + |
| 63 | +def authenticated_menu(): |
| 64 | + # Show a navigation menu for authenticated users |
| 65 | + st.sidebar.page_link("app.py", label="Switch accounts") |
| 66 | + st.sidebar.page_link("pages/user.py", label="Your profile") |
| 67 | + if st.session_state.role in ["admin", "super-admin"]: |
| 68 | + st.sidebar.page_link("pages/admin.py", label="Manage users") |
| 69 | + st.sidebar.page_link( |
| 70 | + "pages/super-admin.py", |
| 71 | + label="Manage admin access", |
| 72 | + disabled=st.session_state.role != "super-admin", |
| 73 | + ) |
| 74 | + |
| 75 | + |
| 76 | +def unauthenticated_menu(): |
| 77 | + # Show a navigation menu for unauthenticated users |
| 78 | + st.sidebar.page_link("app.py", label="Log in") |
| 79 | + |
| 80 | + |
| 81 | +def menu(): |
| 82 | + # Determine if a user is logged in or not, then show the correct |
| 83 | + # navigation menu |
| 84 | + if "role" not in st.session_state or st.session_state.role is None: |
| 85 | + unauthenticated_menu() |
| 86 | + return |
| 87 | + authenticated_menu() |
| 88 | + |
| 89 | + |
| 90 | +def menu_with_redirect(): |
| 91 | + # Redirect users to the main page if not logged in, otherwise continue to |
| 92 | + # render the navigation menu |
| 93 | + if "role" not in st.session_state or st.session_state.role is None: |
| 94 | + st.switch_page("app.py") |
| 95 | + menu() |
| 96 | +``` |
| 97 | + |
| 98 | +Let's take a closer look at `authenticated_menu()`. When this function is called, `st.session_state.role` exists and has a value other than `None`. |
| 99 | + |
| 100 | +```python |
| 101 | +def authenticated_menu(): |
| 102 | + # Show a navigation menu for authenticated users |
| 103 | +``` |
| 104 | + |
| 105 | +The first two pages in the navigation menu are available to all users. Since we know a user is logged in when this function is called, we'll use the label "Switch accounts" for the main page. (If you don't use the `label` parameter, the page name will be derived from the file name like it is with the default sidebar navigation.) |
| 106 | + |
| 107 | +```python |
| 108 | + st.sidebar.page_link("app.py", label="Switch accounts") |
| 109 | + st.sidebar.page_link("pages/user.py", label="Your profile") |
| 110 | +``` |
| 111 | + |
| 112 | +We only want to show the next two pages to admins. Furthermore, we've chosen to disable—but not hide—the super-admin page when the admin user is not a super-admin. We do this using the `disabled` parameter. (`disabled=True` when the role is not `"super-admin"`.) |
| 113 | + |
| 114 | +``` |
| 115 | + if st.session_state.role in ["admin", "super-admin"]: |
| 116 | + st.sidebar.page_link("pages/admin.py", label="Manage users") |
| 117 | + st.sidebar.page_link( |
| 118 | + "pages/super-admin.py", |
| 119 | + label="Manage admin access", |
| 120 | + disabled=st.session_state.role != "super-admin", |
| 121 | + ) |
| 122 | +``` |
| 123 | + |
| 124 | +It's that simple! `unauthenticated_menu()` will only show a link to the main page of the app with the label "Log in." `menu()` does a simple inspection of `st.session_state.role` to switch between the two menu-rendering functions. Finally, `menu_with_redirect()` extends `menu()` to redirect users to `app.py` if they aren't logged in. |
| 125 | + |
| 126 | +<Tip> |
| 127 | + |
| 128 | +If you want to include emojis in your page labels, you can use the `icon` parameter. There's no need to include emojis in your file name or the `label` parameter. |
| 129 | + |
| 130 | +</Tip> |
| 131 | + |
| 132 | +### Create the main file of your app |
| 133 | + |
| 134 | +The main `app.py` file will serve as a pseudo-login page. The user can choose a role from the `st.selectbox` widget. A few bits of logic will save that role into Session State to preserve it while navigating between pages—even when returning to `app.py`. |
| 135 | + |
| 136 | +Add the following `app.py` file to your working directory: |
| 137 | + |
| 138 | +```python |
| 139 | +import streamlit as st |
| 140 | +from menu import menu |
| 141 | + |
| 142 | +# Initialize st.session_state.role to None |
| 143 | +if "role" not in st.session_state: |
| 144 | + st.session_state.role = None |
| 145 | + |
| 146 | +# Retrieve the role from Session State to initialize the widget |
| 147 | +st.session_state._role = st.session_state.role |
| 148 | + |
| 149 | +def set_role(): |
| 150 | + # Callback function to save the role selection to Session State |
| 151 | + st.session_state.role = st.session_state._role |
| 152 | + |
| 153 | + |
| 154 | +# Selectbox to choose role |
| 155 | +st.selectbox( |
| 156 | + "Select your role:", |
| 157 | + [None, "user", "admin", "super-admin"], |
| 158 | + key="_role", |
| 159 | + on_change=set_role, |
| 160 | +) |
| 161 | +menu() # Render the dynamic menu! |
| 162 | +``` |
| 163 | + |
| 164 | +### Add other pages to your app |
| 165 | + |
| 166 | +Add the following `pages/user.py` file: |
| 167 | + |
| 168 | +```python |
| 169 | +import streamlit as st |
| 170 | +from menu import menu_with_redirect |
| 171 | + |
| 172 | +# Redirect to app.py if not logged in, otherwise show the navigation menu |
| 173 | +menu_with_redirect() |
| 174 | + |
| 175 | +st.title("This page is available to all users") |
| 176 | +st.markdown(f"You are currently logged with the role of {st.session_state.role}.") |
| 177 | +``` |
| 178 | + |
| 179 | +Session State resets if a user manually navigates to a page by URL. Therefore, if a user tries to access an admin page in this example, Session State will be cleared, and they will be redirected to the main page as an unauthenicated user. However, it's still good practice to include a check of the role at the top of each restricted page. You can use `st.stop` to halt an app if a role is not whitelisted. |
| 180 | + |
| 181 | +`pages/admin.py`: |
| 182 | + |
| 183 | +```python |
| 184 | +import streamlit as st |
| 185 | +from menu import menu_with_redirect |
| 186 | + |
| 187 | +# Redirect to app.py if not logged in, otherwise show the navigation menu |
| 188 | +menu_with_redirect() |
| 189 | + |
| 190 | +# Verify the user's role |
| 191 | +if st.session_state.role not in ["admin", "super-admin"]: |
| 192 | + st.warning("You do not have permission to view this page.") |
| 193 | + st.stop() |
| 194 | + |
| 195 | +st.title("This page is available to all admins") |
| 196 | +st.markdown(f"You are currently logged with the role of {st.session_state.role}.") |
| 197 | +``` |
| 198 | + |
| 199 | +`pages/super-admin.py`: |
| 200 | + |
| 201 | +```python |
| 202 | +import streamlit as st |
| 203 | +from menu import menu_with_redirect |
| 204 | + |
| 205 | +# Redirect to app.py if not logged in, otherwise show the navigation menu |
| 206 | +menu_with_redirect() |
| 207 | + |
| 208 | +# Verify the user's role |
| 209 | +if st.session_state.role not in ["super-admin"]: |
| 210 | + st.warning("You do not have permission to view this page.") |
| 211 | + st.stop() |
| 212 | + |
| 213 | +st.title("This page is available to super-admins") |
| 214 | +st.markdown(f"You are currently logged with the role of {st.session_state.role}.") |
| 215 | +``` |
| 216 | + |
| 217 | +As noted above, the redirect in `menu_with_redirect()` will prevent a user from ever seeing the warning messages on the admin pages. If you want to see the warning, just add another `st.page_link("pages/admin.py")` button at the bottom of `app.py` so you can navigate to the admin page after selecting the "user" role. 😉 |
0 commit comments