Making Navigation Dots (using intersection observer)

I’m trying to make side navigation dots such as the following image:


I’ve managed to do it using HTML, CSS, and JS

html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="/assets/style.css">
</head>
<body>
    <div class="section" id="home" data-label="Home">Home</div>
    <div class="section" id="about" data-label="About Us">About Us</div>
    <div class="section" id="contact" data-label="Get In Touch">Get In Touch</div>

<nav class="nav">
    <div class="nav-item">
        <a href="#home" class="nav-link"></a>
        <span class="nav-label">Home</span>
    </div>

    <div class="nav-item">
        <a href="#about" class="nav-link"></a>
        <span class="nav-label">About us</span>
    </div>

    <div class="nav-item">
        <a href="#contact" class="nav-link"></a>
        <span class="nav-label">Get In Touch</span>
    </div>
</nav>

<script>
          function activateNavigation() {
            const sections = document.querySelectorAll(".section");
            const navLinks = document.querySelectorAll(".nav-link");

            const observer = new IntersectionObserver(
                (entries) => {
                    navLinks.forEach((navLink) => {
                        navLink.classList.remove("nav-link-selected");
                    });

                    const visibleSection = entries.find((entry) => entry.isIntersecting);

                    if (visibleSection) {
                        const visibleSectionId = visibleSection.target.getAttribute("id");
                        const correspondingNavLink = document.querySelector(`.nav-link[href="#${visibleSectionId}"]`);
                        if (correspondingNavLink) {
                            correspondingNavLink.classList.add("nav-link-selected");
                        }
                    }
                },
                { threshold: 0.5 }
            );

            sections.forEach((section) => observer.observe(section));
        }

        // Run the function when the DOM is fully loaded
        document.addEventListener("DOMContentLoaded", activateNavigation);

</script>
</body>
</html>

and this is the css:

html {
scroll-behavior: smooth;
}

body {
font-family: "Quicksand", sans-serif;
margin: 0;
color:#000;
overflow: hidden;
}

/* uncomment to hide the scrollbar but keep the scroll action*/
/* 
body::-webkit-scrollbar{
  display: none;
} */

.section {
height: 100vh;
}

#home {
background: #aff8db;
}

#about {
background: #ffabab;
}

#contact {
background: #fff5ba;
}

#section4 {
  background: #2496b3;
}

#section5 {
  background: #002B36;
}

#section6 {
  background: #41eda8;
}

#section7 {
  background: #df3838;
}

#section8 {
  background: #c3f0fb;
}
.nav {
  --nav-gap: 10px;
  padding: var(--nav-gap);
  position: fixed;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  display: inline-block;
}

.nav-link:hover ~ .nav-label {
  opacity: 1;

}

.nav-label{
  color: #000;
  font-weight: bold;
  opacity: 0;
  transition: opacity 0.1s;
}

.nav-item {
  align-items: center;
  display: flex;
  margin-bottom: var(--nav-gap);
  flex-direction: row-reverse;
}

.nav-link  {
  height: 25px;
  width: 25px;
  background-color:rgba(0, 0, 0, 0.3);
  border-radius: 50%;
  display: inline-block;
  padding: 10px;
  margin: 5px;
  transition: transform 0.1s;
}

.nav-link-selected{
  background: #000000;
  transform: scale(1.2);
}

I have managed to convert the above code to Dash except for the js part of it which observes which section takes more than 50% of the page and then automatically makes the dot bigger such as the following image:

Hello @mo_taweel,

You shouldnt need to make observers, html.Div has the n_clicks property that you can use as your navigation trigger. :slight_smile:

You could also probably use a link instead, and have the children be a div.

Sorry, I didn’t understand how can I use n_clicks to detect the scroll and which section has the larger size of the page

Ah, my bad, was trying to decipher what you meant by “Navigation”, typically it means browser navigation.

You should just be able to have this here in a JS file in your assets folder:

function activateNavigation() {
            const sections = document.querySelectorAll(".section");
            const navLinks = document.querySelectorAll(".nav-link");

            const observer = new IntersectionObserver(
                (entries) => {
                    navLinks.forEach((navLink) => {
                        navLink.classList.remove("nav-link-selected");
                    });

                    const visibleSection = entries.find((entry) => entry.isIntersecting);

                    if (visibleSection) {
                        const visibleSectionId = visibleSection.target.getAttribute("id");
                        const correspondingNavLink = document.querySelector(`.nav-link[href="#${visibleSectionId}"]`);
                        if (correspondingNavLink) {
                            correspondingNavLink.classList.add("nav-link-selected");
                        }
                    }
                },
                { threshold: 0.5 }
            );

            sections.forEach((section) => observer.observe(section));
        }

Then use a clientside callback to activate:

app.clientside_callback(
"""function (id) {
    activateNavigation();
    return window.dash_clientside.no_update
}""", 
Output('yournav', 'id'), Input('yournav', 'id')
1 Like

Thanks jinnyzor
It worked well for me, the only change I did was replacing ‘id’ with ‘children’.
Here is the full Code if you have any comments:

import dash
from dash import html, Output, Input, dcc, clientside_callback
import dash_bootstrap_components as dbc
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.SOLAR])

app.layout = html.Div([
    dcc.Location(id="url", refresh='callback-nav'),
    html.Div("Home", className="section", id="home", **{"data-label": "Home"}),
    html.Div("About Us", className="section", id="about", **{"data-label": "About Us"}),
    html.Div("Get In Touch", className="section", id="contact", **{"data-label": "Get In Touch"}),
    html.Nav(
        id="my-navigation",
        className="nav",
        children=[
            html.Div(
                className="nav-item",
                children=[
                    html.A(href="#home", className="nav-link", id="link1"),
                    html.Span("Home", className="nav-label")
                ]
            ),
            html.Div(
                className="nav-item",
                children=[
                    html.A(href="#about", className="nav-link", id="link2"),
                    html.Span("About Us", className="nav-label")
                ]
            ),
            html.Div(
                className="nav-item",
                children=[
                    html.A(href="#contact", className="nav-link", id="link3"),
                    html.Span("Get In Touch", className="nav-label")
                ]
            ),
        ])
])

app.clientside_callback(
"""function (id) {
    activateNavigation();
    return window.dash_clientside.no_update
}""", 
Output('my-navigation', 'children'), Input('my-navigation', 'children'))
if __name__ == '__main__':
    app.run_server(debug=True, port=8089)
3 Likes

No problem.

Looks pretty straightforward to me. Glad it is working. :slight_smile:

2 Likes