Edit on StackBlitz
"use client";
import { useEffect, useRef, MouseEventHandler } from "react";
import {
ViewWatcher,
ScrollWatcher,
LoadTrigger,
Show,
AutoHide,
ViewData,
} from "lisn.js";
import { ScrollbarComponent, ScrollbarComponentRef } from "@lisn.js/react";
import "lisn.js/scrollbar.css";
import styles from "./demo.module.css";
export default function Page() {
const msgRef = useRef<HTMLParagraphElement>(null);
const scrollableRef = useRef<ScrollbarComponentRef>(null);
const triggerRefs = useRef<Element[]>([]);
const tabRefs = useRef<Element[]>([]);
const addTriggerRef = (trigger: Element | null) => {
if (trigger) {
triggerRefs.current.push(trigger);
}
};
const addTabRef = (tab: Element | null) => {
if (tab) {
tabRefs.current.push(tab);
}
};
const getTabForTrigger = (trigger: Element) =>
tabRefs.current[triggerRefs.current.indexOf(trigger)];
const getTriggerForTab = (tab: Element) =>
triggerRefs.current[tabRefs.current.indexOf(tab)];
const onTabSelect: MouseEventHandler<HTMLDivElement> = (event) => {
const tab = event.target;
const scrollable = scrollableRef.current?.getWidget()?.getScrollable();
if (scrollable && tab instanceof Element) {
const trigger = getTriggerForTab(tab);
if (trigger) {
ScrollWatcher.reuse().scrollTo(trigger, {
scrollable: scrollable,
offset: { top: -5 },
});
}
}
};
useEffect(() => {
let viewWatcher: ViewWatcher;
const scrollable = scrollableRef.current?.getWidget()?.getScrollable();
const triggers = [...triggerRefs.current];
const onViewHandler = (trigger: Element, viewData: ViewData) => {
const tab = getTabForTrigger(trigger);
if (tab) {
if (viewData.views[0] === "at") {
tab.classList.add(styles.inview);
} else {
tab.classList.remove(styles.inview);
}
}
};
if (scrollable) {
viewWatcher = ViewWatcher.create({
root: scrollable,
rootMargin: "0px",
});
for (const trigger of triggers) {
viewWatcher.onView(trigger, onViewHandler);
}
}
return () => {
// cleanup
for (const trigger of triggers) {
viewWatcher?.offView(trigger, onViewHandler);
}
};
}, []);
useEffect(() => {
const msg = msgRef.current;
let widget: AutoHide;
if (msg) {
new LoadTrigger(msg, [new Show(msg)], {
delay: 1000,
});
widget = new AutoHide(msg, { delay: 2500 });
}
return () => {
// cleanup
widget?.destroy();
};
}, []);
return (
<>
<div className={styles.wrapper}>
<p ref={msgRef} className={[styles.msg, "lisn-hide"].join(" ")}>
Scroll the box
</p>
<div className={styles.demo}>
<div className={styles.tabs}>
<div ref={addTabRef} onClick={onTabSelect} className={styles.tab}>
L
</div>
<div ref={addTabRef} onClick={onTabSelect} className={styles.tab}>
I
</div>
<div ref={addTabRef} onClick={onTabSelect} className={styles.tab}>
S
</div>
<div ref={addTabRef} onClick={onTabSelect} className={styles.tab}>
N
</div>
</div>
<ScrollbarComponent
widgetRef={scrollableRef}
className={styles.scrollable}
>
<div ref={addTriggerRef} className={styles.trigger}></div>
<div className={styles.section}>
<h1>L</h1>
<h4>Lightweight.</h4>
<ul>
<li>Vanilla TypeScript</li>
<li>Highly optimized</li>
<li>No layout thrashing</li>
</ul>
</div>
<div ref={addTriggerRef} className={styles.trigger}></div>
<div className={styles.section}>
<h1>I</h1>
<h4>Interactive.</h4>
<ul>
<li>Powerful API</li>
<li>Multi gesture support</li>
<li>Mobile/touch ready</li>
</ul>
</div>
<div ref={addTriggerRef} className={styles.trigger}></div>
<div className={styles.section}>
<h1>S</h1>
<h4>Simple.</h4>
<ul>
<li>Intuitive syntax</li>
<li>Consistent API</li>
<li>HTML-only mode</li>
</ul>
</div>
<div ref={addTriggerRef} className={styles.trigger}></div>
<div className={styles.section}>
<h1>N</h1>
<h4>No-nonsense.</h4>
<ul>
<li>What says on the box</li>
<li>Sensible defaults</li>
<li>Highly customizable</li>
</ul>
</div>
</ScrollbarComponent>
</div>
</div>
</>
);
}
/* Scrollable */
.wrapper {
position: fixed;
top: 0;
left: 0;
width: 100vw;
width: 100dvw;
height: 100vh;
height: 100dvh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
}
.demo {
width: 100%;
}
.scrollable {
height: 400px;
width: 800px;
max-width: 100%;
background: var(--bg-color-lighter);
box-shadow: var(--lisn-shadow);
color: var(--text-color);
overflow: auto;
margin: 0 auto;
}
.trigger {
transform: translateY(5px);
}
/* Tabs */
.tabs {
display: flex;
justify-content: center;
}
.tab {
font-size: 22px;
color: var(--text-color);
padding: 10px;
transform: scale(0.9);
transition-property: transform;
transition-duration: 0.3s;
cursor: pointer;
}
.tab::after {
content: "";
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 2px;
background: var(--text-color);
transform: scaleX(0);
transition-property: transform;
transition-duration: 0.3s;
}
.tab.inview {
color: var(--text-color-lighter);
font-weight: bold;
transform: scale(1);
}
.tab.inview::after {
transform: scaleX(1);
}
/* Content */
.section {
height: 400px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
/* Misc styles */
.demo h1 {
margin: 0 auto;
font-size: 75px;
background-image: linear-gradient(
45deg,
var(--text-color) 42%,
var(--text-color-lighter) 50%,
var(--text-color) 58%
);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
}
.demo h4 {
font-size: clamp(20px, calc(15px + 2vw), 32px);
}
.demo ul {
padding: 0;
list-style-type: none;
}
.demo ul li {
margin: 8px 0;
}
.demo ul li::before {
content: "\2726";
display: inline-block;
font-size: 0.4em;
transform: translateY(-0.4em);
margin: 0 0.6em 0 0;
}
Edit on CodePen
document.addEventListener("DOMContentLoaded", () => {
const main = document.getElementById("demo");
const scrollable = main.querySelector(".scrollable");
const tabs = main.querySelectorAll(".tab");
const triggerLines = scrollable.querySelectorAll(".trigger-line");
new LISN.widgets.Scrollbar(scrollable);
const watcher = LISN.watchers.ViewWatcher.create({
root: scrollable,
rootMargin: "0px",
});
for (let i = 0; i < triggerLines.length; i++) {
const tab = tabs[i];
if (tab) {
watcher.onView(triggerLines[i], (e, viewData) => {
if (viewData.views[0] === "at") {
tab.classList.add("inview");
} else {
tab.classList.remove("inview");
}
});
tab.addEventListener("click", () =>
LISN.watchers.ScrollWatcher.reuse().scrollTo(triggerLines[i], {
scrollable: scrollable,
offset: { top: -5 },
}),
);
}
}
const msg = document.getElementById("msg");
new LISN.triggers.LoadTrigger(msg, [new LISN.actions.Show(msg)], {
delay: 1000,
});
new LISN.widgets.AutoHide(msg, { delay: 1500 });
});
<p id="msg" class="lisn-hide">Scroll the box</p>
<div id="demo">
<div class="tabs">
<div class="tab">L</div>
<div class="tab">I</div>
<div class="tab">S</div>
<div class="tab">N</div>
</div>
<div class="scrollable">
<div class="trigger-line"></div>
<div class="section">
<h1>L</h1>
<h4>Lightweight.</h4>
<ul>
<li>Vanilla TypeScript</li>
<li>Highly optimized</li>
<li>No layout thrashing</li>
</ul>
</div>
<div class="trigger-line"></div>
<div class="section">
<h1>I</h1>
<h4>Interactive.</h4>
<ul>
<li>Powerful API</li>
<li>Multi gesture support</li>
<li>Mobile/touch ready</li>
</ul>
</div>
<div class="trigger-line"></div>
<div class="section">
<h1>S</h1>
<h4>Simple.</h4>
<ul>
<li>Intuitive syntax</li>
<li>Consistent API</li>
<li>HTML-only mode</li>
</ul>
</div>
<div class="trigger-line"></div>
<div class="section">
<h1>N</h1>
<h4>No-nonsense.</h4>
<ul>
<li>What says on the box</li>
<li>Sensible defaults</li>
<li>Highly customizable</li>
</ul>
</div>
</div>
</div>
/* Scrollable */
#demo {
width: 100%;
}
#demo .scrollable {
height: 400px;
width: 800px;
max-width: 100%;
background: #28283f;
box-shadow: var(--lisn-shadow);
color: var(--text-color);
overflow: auto;
margin: 0 auto;
}
#demo .trigger-line {
transform: translateY(5px);
}
/* Tabs */
#demo .tabs {
display: flex;
justify-content: center;
}
#demo .tab {
font-size: 22px;
color: var(--text-color);
padding: 10px;
transform: scale(0.9);
transition-property: transform;
transition-duration: 0.3s;
cursor: pointer;
}
#demo .tab::after {
content: "";
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 2px;
background: var(--text-color);
transform: scaleX(0);
transition-property: transform;
transition-duration: 0.3s;
}
#demo .tab.inview {
color: var(--text-color-lighter);
font-weight: bold;
transform: scale(1);
}
#demo .tab.inview::after {
transform: scaleX(1);
}
/* Content */
#demo .section {
height: 400px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
#demo h1 {
margin: 0 auto;
font-size: 75px;
background-image: linear-gradient(
45deg,
var(--text-color) 42%,
var(--text-color-lighter) 50%,
var(--text-color) 58%
);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
}
#demo h4 {
font-size: clamp(20px, calc(15px + 2vw), 32px);
}
#demo ul {
padding: 0;
list-style-type: none;
}
#demo ul li {
margin: 8px 0;
}
#demo ul li::before {
content: "\2726";
display: inline-block;
font-size: 0.4em;
transform: translateY(-0.4em);
margin: 0 0.6em 0 0;
}
Edit on CodePen
<p
class="lisn-hide"
data-lisn-on-load="@show +delay=1000"
data-lisn-auto-hide="1500"
>
Scroll the box
</p>
<div id="demo">
<div class="tabs">
<div
class="tab"
data-lisn-ref="tab-L"
data-lisn-on-view="at @add-class:inview
+root=next.scrollable
+rootMargin=0px
+target=next-section-L"
>
L
</div>
<div
class="tab"
data-lisn-ref="tab-I"
data-lisn-on-view="at @add-class:inview
+root=next.scrollable
+rootMargin=0px
+target=next-section-I"
>
I
</div>
<div
class="tab"
data-lisn-ref="tab-S"
data-lisn-on-view="at @add-class:inview
+root=next.scrollable
+rootMargin=0px
+target=next-section-S"
>
S
</div>
<div
class="tab"
data-lisn-ref="tab-N"
data-lisn-on-view="at @add-class:inview
+root=next.scrollable
+rootMargin=0px
+target=next-section-N"
>
N
</div>
</div>
<div class="scrollable" data-lisn-scrollbar>
<div
class="trigger-line"
data-lisn-ref="section-L"
data-lisn-on-click="@scroll-to: offsetY=-5, scrollable=this.scrollable
+target=prev-tab-L +oneWay"
></div>
<div class="section">
<h1>L</h1>
<h4>Lightweight.</h4>
<ul>
<li>Vanilla TypeScript</li>
<li>Highly optimized</li>
<li>No layout thrashing</li>
</ul>
</div>
<div
class="trigger-line"
data-lisn-ref="section-I"
data-lisn-on-click="@scroll-to: offsetY=-5, scrollable=this.scrollable
+target=prev-tab-I +oneWay"
></div>
<div class="section">
<h1>I</h1>
<h4>Interactive.</h4>
<ul>
<li>Powerful API</li>
<li>Multi gesture support</li>
<li>Mobile/touch ready</li>
</ul>
</div>
<div
class="trigger-line"
data-lisn-ref="section-S"
data-lisn-on-click="@scroll-to: offsetY=-5, scrollable=this.scrollable
+target=prev-tab-S +oneWay"
></div>
<div class="section">
<h1>S</h1>
<h4>Simple.</h4>
<ul>
<li>Intuitive syntax</li>
<li>Consistent API</li>
<li>HTML-only mode</li>
</ul>
</div>
<div
class="trigger-line"
data-lisn-ref="section-N"
data-lisn-on-click="@scroll-to: offsetY=-5, scrollable=this.scrollable
+target=prev-tab-N +oneWay"
></div>
<div class="section">
<h1>N</h1>
<h4>No-nonsense.</h4>
<ul>
<li>What says on the box</li>
<li>Sensible defaults</li>
<li>Highly customizable</li>
</ul>
</div>
</div>
</div>
/* Scrollable */
#demo {
width: 100%;
}
#demo .scrollable {
height: 400px;
width: 800px;
max-width: 100%;
background: #28283f;
box-shadow: var(--lisn-shadow);
color: var(--text-color);
overflow: auto;
margin: 0 auto;
}
#demo .trigger-line {
transform: translateY(5px);
}
/* Tabs */
#demo .tabs {
display: flex;
justify-content: center;
}
#demo .tab {
font-size: 22px;
color: var(--text-color);
padding: 10px;
transform: scale(0.9);
transition-property: transform;
transition-duration: 0.3s;
cursor: pointer;
}
#demo .tab::after {
content: "";
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 2px;
background: var(--text-color);
transform: scaleX(0);
transition-property: transform;
transition-duration: 0.3s;
}
#demo .tab.inview {
color: var(--text-color-lighter);
font-weight: bold;
transform: scale(1);
}
#demo .tab.inview::after {
transform: scaleX(1);
}
/* Content */
#demo .section {
height: 400px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
#demo h1 {
margin: 0 auto;
font-size: 75px;
background-image: linear-gradient(
45deg,
var(--text-color) 42%,
var(--text-color-lighter) 50%,
var(--text-color) 58%
);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
}
#demo h4 {
font-size: clamp(20px, calc(15px + 2vw), 32px);
}
#demo ul {
padding: 0;
list-style-type: none;
}
#demo ul li {
margin: 8px 0;
}
#demo ul li::before {
content: "\2726";
display: inline-block;
font-size: 0.4em;
transform: translateY(-0.4em);
margin: 0 0.6em 0 0;
}
Scroll the box