Skip to content

add TableOfContents#513

Merged
AnnMarieW merged 22 commits intosnehilvj:masterfrom
deadkex:tableofcontents
Jan 6, 2026
Merged

add TableOfContents#513
AnnMarieW merged 22 commits intosnehilvj:masterfrom
deadkex:tableofcontents

Conversation

@deadkex
Copy link
Copy Markdown
Contributor

@deadkex deadkex commented Feb 15, 2025

grafik

import random

import dash
from dash import Dash

import dash_mantine_components as dmc
from dash_mantine_components import AppShellAside, ScrollArea, Stack, TableOfContents, Box, Space, Title

dash._dash_renderer._set_react_version('18.2.0')  # noqa
app = Dash(external_stylesheets=dmc.styles.ALL)


def get_layout():
    items = []
    for i in range(20):
        items.append(Title(children=f'Title {i}', id=str(i), order=random.randint(1, 6)))
        items.append(Space(h=300))

    return [
        AppShellAside(
            children=ScrollArea(
                children=Stack(
                    children=[
                        dmc.Space(h=50),
                        dmc.Text("Table of contents", fw=500),
                        TableOfContents(variant="none", size="md")
                    ]),
                type='never'),
            px="5%",
            withBorder=False),
        Box(
            px="20%",
            children=[
                *items,
                Space(h=1000),
            ]
        )
    ]


app.layout = dmc.MantineProvider(
    forceColorScheme="dark",
    children=dmc.AppShell([dmc.AppShellMain(children=get_layout())])
)

if __name__ == "__main__":
    app.run(debug=True)
.mantine-TableOfContents-root {
    border-color: var(--mantine-color-dark-4) !important;
    border-left: calc(.0625rem * var(--mantine-scale)) solid;
}

.mantine-TableOfContents-control {
    border-radius: 0 var(--mantine-radius-sm) var(--mantine-radius-sm) 0;
    border-left: calc(.0625rem * var(--mantine-scale)) solid transparent;
}

.mantine-TableOfContents-control:hover {
    background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
}

.mantine-TableOfContents-control[data-active="true"] {
    background-color: rgba(24, 100, 171, .45);
    color: var(--mantine-color-blue-1);
    border-color: var(--mantine-color-blue-5);
}

.mantine-TableOfContents-control[data-active="true"]:hover {
    background-color: var(--mantine-color-blue-light) !important;
}

@AnnMarieW
Copy link
Copy Markdown
Collaborator

Hi @deadkex
This is a cool component - thanks for adding it!
I see it's marked as a draft. Are you still working on it? Just let me know when you are ready for a review 🙂

@deadkex
Copy link
Copy Markdown
Contributor Author

deadkex commented Feb 20, 2025

@AnnMarieW it is a draft since i am looking for ways to implement it like in the Mantine docs (for example turn the TableOfContents elements into Anchors so the path updates)

@deadkex
Copy link
Copy Markdown
Contributor Author

deadkex commented Feb 21, 2025

@AnnMarieW i'm open for ideas

@AnnMarieW
Copy link
Copy Markdown
Collaborator

Try this:

           getControlProps={({ data }) => ({
                onClick: () => data.getNode().scrollIntoView(),
                children: data.value,
                component: 'a',
                href: `#${data.id}`,
            })}

@AnnMarieW
Copy link
Copy Markdown
Collaborator

Here's an example with a multi-page app. The table of content doesn't update when you change pages - unless the page is refreshed. Probably need to set initial data? https://mantine.dev/core/table-of-contents/#initial-data

import dash
from dash import Dash, _dash_renderer, html
import dash_mantine_components as dmc
_dash_renderer._set_react_version("18.2.0")  # noqa

app = Dash(external_stylesheets=dmc.styles.ALL, use_pages=True, pages_folder="")

def make_section(i, page):
    return dmc.Box(
        [dmc.Title(children=f"{page} Title {i}", id=str(i), order=2), dmc.Space(h=300)],
        p="lg",
    )


aside = dmc.AppShellAside(
    children=dmc.ScrollArea(
        children=dmc.Stack(
            children=[
                dmc.Space(h=50),
                dmc.Text("Table of contents", fw=500),
                dmc.TableOfContents(
                    variant="filled",
                    color="blue",
                    size="sm",
                    radius="sm",
                ),
            ]
        ),
        type="never",
    ),
    px="lg",
)


dash.register_page(
    "home",
    path="/",
    layout=html.Div([make_section(i, "home") for i in range(10)] + [aside]),
)
dash.register_page(
    "page1",
    path="/page-1",
    layout=html.Div([make_section(i, "page1") for i in range(10)] + [aside]),
)


app.layout = dmc.MantineProvider(
    forceColorScheme="light",
    children=dmc.AppShell(
        [
            dmc.AppShellNavbar(
                [
                    dmc.NavLink(label="Home", href="/"),
                    dmc.NavLink(label="Page 1", href="/page-1"),
                ]
            ),
            dmc.AppShellMain(dash.page_container),
        ],
        navbar={"width": 200},
    ),
)

if __name__ == "__main__":
    app.run(debug=True)

@deadkex
Copy link
Copy Markdown
Contributor Author

deadkex commented Mar 10, 2025

I can't work on this currently- if someone wants to pick it up feel free to give it a go

@AnnMarieW AnnMarieW added the help wanted Extra attention is needed label Sep 15, 2025
@deadkex deadkex marked this pull request as ready for review December 22, 2025 20:44
@deadkex
Copy link
Copy Markdown
Contributor Author

deadkex commented Dec 22, 2025

@AnnMarieW i fixed the problems, can you please confirm that it works as intended?

@AnnMarieW
Copy link
Copy Markdown
Collaborator

Looks like it now works to click on a toc item and it scrolls into view. However, it doesn't work with multi-page apps. Probably need to implement the reinitialize https://mantine.dev/core/table-of-contents/#reinitialize

@deadkex
Copy link
Copy Markdown
Contributor Author

deadkex commented Dec 23, 2025

@AnnMarieW Can you give me a MRE please?
It works for me with pages - but callbacks seem to be the problem since they load the content later and the TOC doesnt check for changes.

@deadkex
Copy link
Copy Markdown
Contributor Author

deadkex commented Dec 24, 2025

@AnnMarieW What do you think about a refresh prop?

main.py

import random
import dash
from dash import Dash, Output, State, Input, dcc, clientside_callback
import dash_mantine_components as dmc

app = Dash(external_stylesheets=dmc.styles.ALL, use_pages=True)

app.layout = dmc.MantineProvider(
    forceColorScheme="dark",
    children=dmc.AppShell([
        dcc.Location(id="url"),
        dmc.AppShellMain(
            children=dmc.Stack([
                dmc.Stack([dmc.Anchor(x, href=x) for x in ["/", "/a", "/b"]]),
                dash.page_container
            ])
        )
    ])
)

if __name__ == "__main__":
    app.run(debug=True)

pages/a.py
normal page

import random
import dash
import dash_mantine_components as dmc

dash.register_page(__name__)

def layout():
    items = [
        dmc.Title(children="Content", id="content", order=1),
        dmc.Space(h=300)
    ]

    for i in range(20):
        items.append(dmc.Title(children=f'Title {i}', id=str(i), order=random.randint(1, 6)))
        items.append(dmc.Space(h=300))

    return [
        dmc.AppShellAside(
            children=dmc.ScrollArea(
                children=dmc.Stack(
                    children=[
                        dmc.Space(h=50),
                        dmc.Text("Table of contents", fw=500),
                        dmc.TableOfContents(
                            variant="filled",
                            size="md",
                            scrollIntoViewOptions={ "behavior": "smooth" }
                        )
                    ]),
                type='never'),
            px="5%",
            withBorder=False),
        dmc.Box(
            px="20%",
            children=[
                *items,
                dmc.Space(h=1000),
            ]
        )
    ]

pages/b.py
Fill content via callback and refresh

import random
import dash
from dash import callback, Output, Input
import dash_mantine_components as dmc

dash.register_page(__name__)

def layout():
    return [
        dmc.AppShellAside(
            children=dmc.ScrollArea(
                children=dmc.Stack(
                    children=[
                        dmc.Space(h=50),
                        dmc.Text("Table of contents", fw=500),
                        dmc.TableOfContents(
                            id="toc",
                            variant="none",
                            size="md",
                            scrollIntoViewOptions={"behavior": "smooth"}
                        )
                    ]),
                type='never'),
            px="5%",
            withBorder=False),
        dmc.Button("test", id="button"),
        dmc.Box(
            px="20%",
            id="test"
        )
    ]


@callback(
    Output("test", "children"),
    Output("toc", "refresh"),
    Input("url", "pathname"),
    Input("button", "n_clicks")
)
def create_content(_, n_clicks):
    items = [
        dmc.Title(children="Content B", id="content", order=1),
        dmc.Space(h=300)
    ]

    for i in range(20):
        items.append(dmc.Title(children=f'Title {i}', id=str(i), order=random.randint(1, 6)))
        items.append(dmc.Space(h=300))
    return [
        *items,
        dmc.Space(h=1000),
    ], n_clicks

Comment thread src/ts/components/core/TableOfContents.tsx Outdated
@BSd3v
Copy link
Copy Markdown
Contributor

BSd3v commented Jan 2, 2026

@deadkex

I have a PR on your fork against this branch. Check it out and merge if everything is good.

adjustments and utilization of functions similar to `dcc.Loading` for refreshing upon necessary updates
@AnnMarieW
Copy link
Copy Markdown
Collaborator

@deadkex Thanks for coming back to this PR and working on this component again -- things are looking good!

@BSd3v Thanks for adding the feature to reinitialize the TOC automatically by using the dash loading status. It's nice to be able to do that without using a callback.

I'm going to work on adding tests and docs. That's usually a good way to make sure all the features are working correctly.

So far, I have one additional comment: The refresh is a good prop name, but I think it would be better to call it reinitialize so it more closely matches the Mantine API

@AnnMarieW
Copy link
Copy Markdown
Collaborator

AnnMarieW commented Jan 3, 2026

@BSd3v Your feature to to support the loading state is only compatible with dash>=3. Is it possible to make it work with dash 2 as well?

If not, how about adding a check for the dash version and an error message to use the reinitialize rather than targetComponentId when using dash < 3 ?

@AnnMarieW
Copy link
Copy Markdown
Collaborator

What do you think about changing the prop targetComponentId to target_id. This would be consistent with other components like the CopyButton

@AnnMarieW
Copy link
Copy Markdown
Collaborator

@BSd3v - would you like to check out the changes we discussed for handling the target id and the loading completed function for dash 3?

@AnnMarieW AnnMarieW self-requested a review January 5, 2026 20:27
Comment thread src/ts/components/core/TableOfContents.tsx Outdated
Comment thread src/ts/components/core/TableOfContents.tsx Outdated
Comment on lines +31 to +32
/** Data used to render content until actual values are retrieved from the DOM, empty array by default */
initialData?: InitialTableOfContentsData[];
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we should include the initialData prop? It refers to server-side rendering, which is not applicable in Dash

TableOfContents retrieves data on mount. If you want to render headings before TableOfContents component is mounted (for example during server-side rendering), you can pass initialData prop with array of headings data. initialData is replaced with actual data on mount.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah unless we can demonstrate a good use for it, sounds like we should drop it. I could imagine this content showing up while the rest of the page is loading, but then it would either be wrong or it would not respond properly to clicking on the headings until the page finishes loading and the real content is present.

Comment thread src/ts/components/core/TableOfContents.tsx Outdated
Comment thread src/ts/components/core/TableOfContents.tsx Outdated
Comment thread src/ts/components/core/TableOfContents.tsx Outdated
deadkex and others added 6 commits January 6, 2026 12:25
Co-authored-by: Ann Marie Ward <72614349+AnnMarieW@users.noreply.github.com>
Co-authored-by: Ann Marie Ward <72614349+AnnMarieW@users.noreply.github.com>
Co-authored-by: Ann Marie Ward <72614349+AnnMarieW@users.noreply.github.com>
Co-authored-by: Ann Marie Ward <72614349+AnnMarieW@users.noreply.github.com>
Removed commented-out code for scrollSpyOptions.
@AnnMarieW AnnMarieW removed the help wanted Extra attention is needed label Jan 6, 2026
@AnnMarieW
Copy link
Copy Markdown
Collaborator

@alexcjohnson Do you have time to take a look at this PR? This component wasn't a simple one to add and your feedback is always appreciated.

Comment thread src/ts/utils/dash3.ts Outdated
@alexcjohnson
Copy link
Copy Markdown
Collaborator

Cool component! Looks good, @AnnMarieW I agree about initialData but otherwise the API looks great, only one small comment on the implementation.

Co-authored-by: Alex Johnson <alex@plot.ly>
@AnnMarieW
Copy link
Copy Markdown
Collaborator

Thanks so much for your quick review! 🙏
I'll make the changes and remove the initialData prop.

@AnnMarieW
Copy link
Copy Markdown
Collaborator

Thanks @deadkex for seeing this over the finish line!

Thanks for your feedback and reviews @BSd3v and @alexcjohnson

This will be an fantastic new feature for the next release 🎉

💃

@AnnMarieW AnnMarieW merged commit c5d1d9c into snehilvj:master Jan 6, 2026
1 check passed
@deadkex deadkex deleted the tableofcontents branch January 9, 2026 09:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants