I'm building my app in NextJS and I'd like to highlight entry in my TOC when its content is visible. I found this solution: https://css-tricks.com/table-of-contents-with-intersectionobserver/ but I don't know how to rewrite it for react. Are there any other React-ready solutions?
To use IntersectionObserver in nextJS (React), firstly you need to create a client component (so insert the "use client"
line at the very top of you file). Then, you have to import useEffect, because the IntersectionObserver API can be used only once the window object is available, so in the dependecies array you have to insert window
. Then, create the IntersectionObserver object, in this way
useEffect(() => {
const observer = new IntersectionObserver((entries) => { })
}, [window]);
Now, i think you have a container, and inside it, at right, the content, and at left a position fixed div with an ul and all the li tags inside it to represent your TOC. You have to keep track of all of your text paragraphs, and observe them. You can use an array of useRef, or other data structure if you want to represent also subTopics (like an hashmap). Because this is a minimal example i used a simple array with two entries, like this
const allToc = [useRef(null), useRef(null)];
const allElems = [useRef(null), useRef(null)];
Then you have to observe them, so in your useEffect hook insert the following code
allElems.forEach((ref) => {
observer.observe(ref.current);
})
I suppose also that every topix has an id that corresponds to the position of the topic in the array, so for example
<h1 ref={allElems[0]} id="0">Topic1</h1>
<div style={{height: "1000px"}}></div>
<p ref={allElems[1]} id="1">Topic2</p>
The last thing you have to do is to add the logic to the observer, so in your IntersectionObserver object add this code
entries.forEach(entry => {
if(entry.isIntersecting) {
// get index of entry
const index = parseInt(entry.target.id);
allToc[index].current.style.color = "red";
// all others should be black
allToc.forEach((ref, i) => {
if(i !== index) {
ref.current.style.color = "black";
}
})
}
})
In the end, your code should look like this
const allToc = [useRef(null), useRef(null)]
const allElems = [useRef(null), useRef(null)]
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if(entry.isIntersecting) {
// get index of entry
const index = parseInt(entry.target.id);
allToc[index].current.style.color = "red";
// all others should be black
allToc.forEach((ref, i) => {
if(i !== index) {
ref.current.style.color = "black";
}
})
}
})})
allElems.forEach((ref) => {
observer.observe(ref.current);
})
}, [allElems, allToc]);
And your jsx something like
<div id="container" style={{width: "80%"}}>
<div className="text">
<h1 ref={allElems[0]} id="0">Test</h1>
<div style={{height: "1000px"}}></div>
<p ref={allElems[1]} id="1">Test</p>
</div>
</div>
<div id="toc" style={{position: "fixed", left: "80%"}}>
<ul>
{tocEntries.map((entry, index) =>
<li key={index} ref={allToc[index]} id={index.toString()}>
{entry}
</li>
)
}
</ul>
</div>