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

7 Likes

Wow! This is super cool, thanks @okld !

2 Likes

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

1 Like

Hi @okld, thanks for your code! :+1: I was able to jump to Title but not header or subheader and couldnā€™t figure out why. Could you please help? Thank you!

1 Like

Did you happen to figure out the solution to this? If yes, please do let me know

I also had this issue, solved it by replacing

key = "".join(filter(str.isalnum, text)).lower()

with

import re
key = re.sub('[^0-9a-zA-Z]+', '-', text).lower()
1 Like

Hi everyone,

Thanks for sharing this. Do you know if there is a way to stop the anchor tags from showing at the end of url? Does it require jQuery or javascript?

:ok_hand: Very nice! is it possible to adapt the toc code to work with headers or sub headers within an st.tab? So, for example, in the tabs below if I click on the header in the toc ā€œAn Owlā€ it would guide me towards and open the tab3. In the toc current state if you click on a header that is within a tab it does not take you anywhere. Any ideas on a workaround? Thank you.

tab1, tab2, tab3 = st.tabs(["Cat", "Dog", "Owl"])

with tab1:
   st.header("A cat")
   st.image("https://static.streamlit.io/examples/cat.jpg", width=200)

with tab2:
   st.header("A dog")
   st.image("https://static.streamlit.io/examples/dog.jpg", width=200)

with tab3:
   st.header("An owl")
   st.image("https://static.streamlit.io/examples/owl.jpg", width=200)

The real problem is that the TOC Widget does not work as stated here in the current verson.

This is caused by streamlit replacing any given ID added to a heading flag by the text of the heading. Your solution works. The original code only keeps alpha numeric chars, while your version keeps the dash, exactly like streamlit does.

The entire thing breaks as soon as you have two headings with the same name as streamlit will give them the same ID ā€“ no matter what you set as ID.

The workaround is to not use any html heading but only use <p> and style them manually, e.g.:

style = 'font-size:1.5rem;font-weight:600;color:rgb(49, 51, 63);line-height 1.2;"'
st.markdown(f"<p id='{key}' style={style}>{text}</p>", unsafe_allow_html=True)

I am not an expert, but I would guess that tabs are JavaScript. Meaning the js will on click remove the elements on one tab from the dom (or make them invisible) and adding the other tabs elements to the dom. So really, you wont get anywhere with pure html functionality. You kind of need to open the tab via a callback and only then can you jump to the right place using html.