Streamlit-scroll-navigation - New Streamlit component for seamless scroll-based navigation

Introducing streamlit-scroll-navigation: flexible scroll-based navigation for Streamlit

Hey everyone, I’m excited to introduce streamlit-scroll-navigation, a customizable scroll-based navigation component for Streamlit apps.

I love the aesthetic of scrollable single-page applications (SPA). They elegantly present multiple sections of content without requiring users to navigate across different pages. With streamlit-scroll-navigation, you can easily create SPA pages in Streamlit. This component enables smooth scroll-based navigation for portfolios, data stories, or other apps that present multiple sections on the same page.

Demo:

Features

  • Smooth Animations: Scrolling to anchors on the page feels fluid and seamless.
  • Anchor tracking: As the user scrolls, the active anchor automatically updates to the nearest visible anchor.
  • Configurable Icons: Customize Bootstrap icons for each navigation option and the menu title to give your app a personal touch.
  • Configurable Styles: Edit CSS attributes with the override_styles parameter for additional customization.
  • Styled with Bootstrap: The component comes styled with Bootstrap for a sleek and responsive design.

Installation

To install, simply run:

pip install streamlit-scroll-navigation

Parameters

scroll_navbar() accepts the following parameters:

  • anchor_ids (required): The list of anchor IDs representing the sections or points to navigate.
  • menu_title (optional, default='Menu'): The title of the scroll navigation menu.
  • icons (optional, default=[]): A list of Bootstrap icon names corresponding to each navigation option.
  • default_index (optional, default=0): The index of the initially selected item.
  • orientation (optional, default='vertical'): The orientation of the navigation (either 'vertical' or 'horizontal').

The function returns the currently selected anchor’s ID.

Example

This examples contains 2 scroll navigation bars with different orientations. Explore example.py for more use cases.

# Create a dummy streamlit page 
import streamlit as st
from streamlit_scroll_navigation import scroll_navbar

# Anchor IDs and icons
anchor_ids = ["About", "Features", "Settings", "Pricing", "Contact"]
anchor_icons = ["info-circle", "lightbulb", "gear", "tag", "envelope"]

# 1. as sidebar menu
with st.sidebar:
    st.subheader("Example 1")
    scroll_navbar(
        anchor_ids,
        anchor_labels=None, # Use anchor_ids as labels
        anchor_icons=anchor_icons)

# 2. horizontal menu
st.subheader("Example 2")
scroll_navbar(
        anchor_ids,
        key = "navbar2",
        anchor_icons=anchor_icons,
        orientation="horizontal")

# Dummy page setup
for anchor_id in anchor_ids:
    st.subheader(anchor_id,anchor=anchor_id)
    st.write("content " * 100)

Give it a try and let me know your feedback, questions, or if you want to contribute!

12 Likes

That is very cool! I really love how smooth it is, and all the different options for presenting the navigation!

(By the way, looks like your number 5 “force_body” example has a bug, and presumably should be “Contact”, or one of the other existing anchors).

Thanks for catching that! I just pushed a fix to the demo to make the force_anchor example work.

1 Like

This is awesome! Love how dynamic it makes the page feel. :star_struck:

Awesome! I love using Streamlit for single-page apps, so I had to give this component some love.

1 Like

This is an awesome component!

Is there a current method to be able to return to a specific section of the page? Like clicking the title of the anchor point for instance like you would in markdown? Thinking about how this scales when you have a large number of sections on the page and you can’t easily scroll to the top of the page instantly!

Thanks for your feedback. I added the disable_scroll parameter to the scroll_nav() function. When disable_scroll=True, the navigation buttons instantly snap to their respective anchors. Let me know if this doesn’t work for your use case.

*Pushed to version 1.1.3

Is there a way to utilize this as a navigation bar and instead of clicking down to a different section on the same page, it redirects you to an entirely new page? mainly love the design and fluidity of it.

Thank for the feedback!

Page navigation is outside the scope of this project, but Streamlit has built-in page navigation.

this is fantastic. love example 1 in particular. thanks for the share!

1 Like

This looks really nice! But I have one question/concern: Do I see it correct that each time I click on a button in the navbar, the whole app is run again? Is there a way to omit this?

Because the initial loading of my app takes a few seconds, and if this done again for each click, it counteracts the nice direct response that your scroll-navigation provides.

Great find! Most people definitely don’t want their entire script to rerun after navigating to an anchor.

I made scroll_navbar() a fragment function so that user interaction reruns don’t affect the entire page. This should fix the problem, but let me know if it persists

*update pushed to v.1.1.6 on pip

Awesome that you fixed this so fast, thanks!

Only thing I noticed is that the force_anchor example is still reloading the page (well, it’s a standard streamlit button). Besides, I can’t get this option to work for me at all on a first try. Which is not too bad; I just thought about using it as a “Go back to top of page” button.

Also, auto_update_anchor doesn’t seem to work (at least like I think it should work). It’s not really updating the navbar to reflect the currently “closest” anchor. This is true also for https://scrollnav-demo.streamlit.app I think.

Thanks again for the feedback. v1.1.6 implemented StreamLit fragments to make navbar interactions not rerun the entire page, but this broke force_anchor because the string that StreamLit passes to fragments does not get reset. v1.2.1 adds the ForceAnchor class to consume forced anchors without a page rerun. Check out the updated Example 5 that pushes force_anchor from a fragment. This should meet satisfy your use case:

st.subheader("Example 5", help="Programatically select an anchor within StreamLit fragment")
@st.fragment
def example5():
    from streamlit_scroll_navigation import ForceAnchor
    force_settings = ForceAnchor()
    if st.button("Go to Settings"):
        force_settings.push("Settings")
    scroll_navbar(
            anchor_ids,
            key="navbar5",
            anchor_icons=anchor_icons,
            orientation="horizontal",
            force_anchor=force_settings)
example5()

I believe auto_update_anchor=True is working as expected. When the active anchor leaves the screen, the closest anchor to the old active anchor is selected. I couldn’t think of a better anchor update pattern that wouldn’t have a ton of edge cases, but let me know if one exists.

Thanks again for the quick fix! Is there a way to put the st.button at another position in the app, not directly next to the navbar? I tried it with placing a container1 = st.container() at the top of the page, and at the bottom add the code of example 5, with the scroll_navbar() inside with container1: (see example below). This works in principle, but it creates a second navbar inside the container after pushing the button.

# Setup
import streamlit as st
from streamlit_scroll_navigation import scroll_navbar
st.set_page_config(page_title="Scroll Navigation Demo")

# Anchor IDs and icons
anchor_ids = ["About", "Features", "Settings", "Pricing", "Contact"]
anchor_icons = ["info-circle", "lightbulb", "gear", "tag", "envelope"]

container1 = st.container()
    
# Dummy page setup
for anchor_id in anchor_ids:
    st.subheader(anchor_id,anchor=anchor_id)
    st.write("content " * 100)

# 5. Force anchor
st.subheader("Example 5", help="Programatically select an anchor within StreamLit fragment")
@st.fragment
def example5():
    from streamlit_scroll_navigation import ForceAnchor
    force_settings = ForceAnchor()
    if st.button("Go to Settings"):
        force_settings.push("Settings")
    with container1:
        scroll_navbar(
            anchor_ids,
            key="navbar5",
            anchor_icons=anchor_icons,
            orientation="horizontal",
            force_anchor=force_settings)
example5()

Interestingly, now your example works as expected. Maybe there was just some hiccup on my computer last time. :person_shrugging:

Ah that has something to do with fragments not triggering redraws of external containers. A simple hack is to use st.empty() instead of st.container(). This makes the container only hold the most recently drawn component.

# Setup
import streamlit as st
from streamlit_scroll_navigation import scroll_navbar
st.set_page_config(page_title="Scroll Navigation Demo")

# Anchor IDs and icons
anchor_ids = ["About", "Features", "Settings", "Pricing", "Contact"]
anchor_icons = ["info-circle", "lightbulb", "gear", "tag", "envelope"]

container1 = st.empty()
    
# Dummy page setup
for anchor_id in anchor_ids:
    st.subheader(anchor_id,anchor=anchor_id)
    st.write("content " * 100)

# 5. Force anchor
st.subheader("Example 5", help="Programatically select an anchor within StreamLit fragment")
@st.fragment
def example5():
    from streamlit_scroll_navigation import ForceAnchor
    force_settings = ForceAnchor()
    if st.button("Go to Settings"):
        force_settings.push("Settings")
    with container1:
        scroll_navbar(
            anchor_ids,
            key="navbar5",
            anchor_icons=anchor_icons,
            orientation="horizontal",
            force_anchor=force_settings)
example5()

I bet it was a weird bug in v1.1.6, but it’s fixed now :partying_face:.

2 Likes

Yes, that works, thanks!

1 Like