Table of contents widget

Hey there!

I’m looking for a table of contents to include in my report. I looked in the streamlit documentation and in the forum and couldn’t find any reference to it. Does streamlit have this kind of widget?

Thanks

1 Like

Hi @Avi_Aminov -

There is not currently a table of contents widget. Is your app sufficiently large where this needs to be auto-generated?

Best,
Randy

Hello @Avi_Aminov,

There’s no widget to do that as it would require to catch every other widget rendering markdown text and generate a TOC from that. And to keep things clean and simple, widgets does not communicate this way with each other.

In my opinion, it’d be more appropriate to implement a function (or use a library) that generates a TOC from a markdown input, and display it like you’d display any other markdown text.

1 Like

While I do think the TOC generation is a bit much, it would be nice to be able to scroll to a given header through the URL, as more and more websites provide this ability (like here is the section on install in Streamlit). For a report, I would be able to send an url pointing to the results section directly instead. You would also need this scroll if you provide a clickable TOC like in @Synode’s idea :slight_smile:

@Avi_Aminov do you have other ideas for navigating through your large report ? How large / structured is it ?

There seems to be a very old issue for this if we want to bump it…also with all of the lazy loading and Python background computation going around, it may be a bit tricky to implement

1 Like

In my opinion, it’d be more appropriate to implement a function (or use a library) that generates a TOC from a markdown input, and display it like you’d display any other markdown text.

Thanks for the reply. I understand how it makes sense to keep the elements separated. I can implement such a function, but how do I get the markdown input generated by streamlit to do so?

You could think about this in the reverse, creating a dictionary or list holding the values you want to have for both the TOC and headers. For the TOC, iterate over all the values and make a list, and for each header, pick the key that’s appropriate to display.

Makes the code a bit more abstracted I suppose, but by having the data in a single data object, they stay in sync.

I see two problems with this:

  1. I need the TOC to appear at the beginning, but I need it to actually render at the end (after all the data / html has been populated). Is that possible?
  2. (related to @andfanilo’s reply) How can I link to the elements themselves? I see streamlit doesn’t generate them with an anchor tag, so I can’t reference to it. I guess I can inject anchors to the elements, or get their position and use something like window.scrollTo to get to it. A bit cumbersome but should work.

Alright, here’s an example. You can place your TOC in the sidebar or on the main page, anywhere you want. The only requirement is to use toc’s title(), header() and subheader() methods to create your titles. Call toc.generate() once you have displayed every title, at the very end of your app.

And just in case, make sure you don’t use user inputs to create your titles. I’m using unsafe_allow_html, so if the end user can change titles somehow by anything he wants, he’d be able to execute code on your page.

Source code here
import streamlit as st


class Toc:

    def __init__(self):
        self._items = []
        self._placeholder = None
    
    def title(self, text):
        self._markdown(text, "h1")

    def header(self, text):
        self._markdown(text, "h2", " " * 2)

    def subheader(self, text):
        self._markdown(text, "h3", " " * 4)

    def placeholder(self, sidebar=False):
        self._placeholder = st.sidebar.empty() if sidebar else st.empty()

    def generate(self):
        if self._placeholder:
            self._placeholder.markdown("\n".join(self._items), unsafe_allow_html=True)
    
    def _markdown(self, text, level, space=""):
        key = "".join(filter(str.isalnum, text)).lower()

        st.markdown(f"<{level} id='{key}'>{text}</{level}>", unsafe_allow_html=True)
        self._items.append(f"{space}* <a href='#{key}'>{text}</a>")


toc = Toc()

st.title("Table of contents")
toc.placeholder()

toc.title("Title")

for a in range(10):
    st.write("Blabla...")

toc.header("Header 1")

for a in range(10):
    st.write("Blabla...")

toc.header("Header 2")

for a in range(10):
    st.write("Blabla...")

toc.subheader("Subheader 1")

for a in range(10):
    st.write("Blabla...")

toc.subheader("Subheader 2")

for a in range(10):
    st.write("Blabla...")

toc.generate()

streamlit-toc-2020-06-16-19-06-03.webm

4 Likes

Wow! This is super cool, thanks @synode !

1 Like

Thanks for the code! I adapted your version of Toc() with an extra Header class here:

1 Like