Edit on StackBlitz
"use client";
import { useEffect, useRef, ReactNode } from "react";
import {
ScrollWatcher,
ViewWatcher,
ScrollTrigger,
ScrollData,
Hide,
Show,
Enable,
} from "lisn.js";
import {
useScrollbar,
CollapsibleComponent,
CollapsibleTriggerComponent,
PopupComponent,
PopupTriggerComponent,
ModalComponent,
OffcanvasComponent,
ScrollToTopComponent,
OffcanvasComponentRef,
ModalComponentRef,
} from "@lisn.js/react";
// import "lisn.js/collapsible.css";
// import "lisn.js/popup.css";
// import "lisn.js/modal.css";
// import "lisn.js/offcanvas.css";
// import "lisn.js/scrollbar.css";
// import "lisn.js/scroll-to-top.css";
// ^^ Or we could just import "lisn.js/lisn.css" which contains all CSS
import "lisn.js/lisn.css";
import styles from "./demo.module.css";
export default function Page() {
useScrollbar();
return (
<>
<ScrollToTopComponent />
<DemoMenu />
<DemoModal />
<div className={styles.demo}>
<DemoIntro />
{/* blank page for spacing */}
<div className={styles.section}></div>
<div
className={[styles.section, styles.top, styles.accordion].join(" ")}
>
<h1 className={styles.huge}>Why LISN?</h1>
<CollapsibleComponent config={{ peek: "75px" }}>
<DemoAccordion title="Lightweight.">
<p>Vanilla TypeScript</p>
<p>Highly optimized</p>
<DemoTooltip title="No layout thrashing">
Correct use of <code>requestAnimationFrame</code> to completely
eliminate forced re-layouts and run smoothly even on mobile
devices.
</DemoTooltip>
</DemoAccordion>
<DemoAccordion title="Interactive.">
<p>Powerful API</p>
<DemoTooltip title="Multi gesture support">
Take actions based on user gestures. Scroll, zoom or drag, using
wheel, touch or pointer device? Any or all of these.
</DemoTooltip>
<p>Mobile/touch ready</p>
</DemoAccordion>
<DemoAccordion title="Simple.">
<p>Intuitive syntax</p>
<p>Consistent API</p>
<DemoTooltip title="HTML-only mode">
The HTML-only API can do much of what the full JavaScript API
can.
</DemoTooltip>
</DemoAccordion>
<DemoAccordion title="No-nonsense.">
<p>What says on the box</p>
<DemoTooltip title="Sensible defaults">
Spend time building your site, not configuring LISN.
</DemoTooltip>
<DemoTooltip title="Highly customizable">
But if you want to tweak... go wild.
</DemoTooltip>
</DemoAccordion>
</CollapsibleComponent>
{/* trigger will be wrapped and we set the class on the wrapper */}
<CollapsibleTriggerComponent config={{ className: styles.trigger }}>
<h5 className={styles.more}>~~ View more ~~</h5>
<h5 className={styles.less}>~~ View less ~~</h5>
</CollapsibleTriggerComponent>
</div>
</div>
</>
);
}
const DemoMenu = () => {
const menuWidgetRef = useRef<OffcanvasComponentRef>(null);
useEffect(() => {
// open/close menu on scroll up/down
const watcher = ScrollWatcher.reuse();
const handler = (t: EventTarget, scrollData: ScrollData) => {
const widget = menuWidgetRef.current?.getWidget();
if (widget) {
if (scrollData.direction === "up") {
widget.open();
} else if (scrollData.direction === "down") {
widget.close();
}
}
};
watcher.onScroll(handler);
return () => {
// cleanup
watcher.offScroll(handler);
};
}, []);
{
/* Remember, openable components, if not explicitly passed a map/array of
* triggers, will try to automatically find triggers in the parent element.
* So either:
* - wrap it in it own element,
* - explicitly pass triggers (in this case we want an empty array since it
* will be opened by the view watcher) or
* - explicitly pass contentId
*/
}
return (
<OffcanvasComponent
className={styles.menu}
config={{ position: "top", closeButton: false, triggers: [] }}
widgetRef={menuWidgetRef}
>
<a href="/demos" target="_blank">
More demos
</a>
<a href="/#get-started" target="_blank">
Getting started
</a>
<a href="https://github.com/lisnjs/lisn.js" target="_blank">
Source code
</a>
</OffcanvasComponent>
);
};
const DemoModal = () => {
const modalWidgetRef = useRef<ModalComponentRef>(null);
useEffect(() => {
// show modal when scrolled to the middle of the page
const watcher = ViewWatcher.create({ rootMargin: "-48% 0px" });
const target = "top: 50%";
const handler = () => {
const widget = modalWidgetRef.current?.getWidget();
widget?.open();
watcher.offView(target, handler); // just once
};
watcher.onView(target, handler, { views: "at" });
return () => {
// cleanup
watcher.offView(target, handler);
};
}, []);
{
/* Remember, openable components, if not explicitly passed a map/array of
* triggers, will try to automatically find triggers in the parent element.
* So either:
* - wrap it in it own element,
* - explicitly pass triggers (in this case we want an empty array since it
* will be opened by the view watcher) or
* - explicitly pass contentId
*/
}
return (
<ModalComponent
className={styles.modal}
widgetRef={modalWidgetRef}
config={{ triggers: [] }}
>
You've reached the middle of the page. Are you liking LISN already?
</ModalComponent>
);
};
const DemoIntro = () => {
const msg1Ref = useRef(null);
const msg2Ref = useRef(null);
useEffect(() => {
// Show/hide scroll instruction messages
const msg1 = msg1Ref.current;
const msg2 = msg2Ref.current;
const triggers: ScrollTrigger[] = [];
if (msg1 && msg2) {
// On scroll up, hide the "Scroll up" message and shortly after enable
// the trigger on the other message
triggers.push(
new ScrollTrigger(msg1, [new Hide(msg1)], {
directions: "up",
once: true,
}),
);
triggers.push(
new ScrollTrigger(
msg2,
[new Show(msg2), new Enable(msg2, "hide on down")],
{
directions: "up",
once: true,
delay: 300,
},
),
);
// Logically, we would use Hide on direction down, but this will set the
// initial state of the element to be shown, so we reverse the logic.
triggers.push(
new ScrollTrigger(msg2, [new Show(msg2)], {
directions: "up",
once: true,
id: "hide on down",
}),
);
}
return () => {
// cleanup
for (const trigger of triggers) {
trigger.destroy();
}
};
}, []);
return (
<>
<div className={styles.section}>
<p className="text-center">
This demo includes all four types of openables with several types of
triggers for opening them.
</p>
<h2>Scroll down.</h2>
</div>
<div className={styles.section}>
<h2 ref={msg2Ref} className="lisn-hide">
Now keep going down.
</h2>
<div className={styles.spacer}></div>
<h2 ref={msg1Ref} className="lisn-hide">
Then scroll up a bit.
</h2>
</div>
</>
);
};
const DemoAccordion = ({
title,
children,
}: {
title: string;
children: ReactNode;
}) => {
return (
<div>
<CollapsibleTriggerComponent
as="h4"
className={styles.trigger}
config={{
icon: "left",
iconClosed: "plus",
iconOpen: "minus",
}}
>
{title}
</CollapsibleTriggerComponent>
<CollapsibleComponent
className={styles.content}
config={{ autoClose: true }}
>
{children}
</CollapsibleComponent>
</div>
);
};
const DemoTooltip = ({
title,
children,
}: {
title: string;
children: ReactNode;
}) => {
return (
<div>
<PopupTriggerComponent
as="p"
className={styles.tooltip}
config={{ hover: true }}
>
{title}
</PopupTriggerComponent>
<PopupComponent className={styles.popup} config={{ position: "top" }}>
{children}
</PopupComponent>
</div>
);
};
.demo {
padding: 1em;
}
.huge {
font-size: 2.5em;
}
/* Layout */
.section {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.section.top {
justify-content: flex-start;
}
.spacer {
margin: 50px 0;
}
/* Collapsibles */
.trigger[data-lisn-is-open="false"] .less {
display: none;
}
.trigger[data-lisn-is-open="true"] .more {
display: none;
}
.trigger :is(h1, h2, h3, h4, h5, h6) {
margin: 5px 0;
}
.demo p {
margin: 3px 0;
}
.accordion .content {
padding: 1em 0 1em 1.3em;
}
/* Popups */
.demo {
--lisn-popup--v-padding: 15px;
--lisn-popup--h-padding: 15px;
--lisn-popup--width: 350px;
--lisn-popup--max-width: 98vw;
}
.popup {
font-size: 16px;
}
.tooltip::after {
content: "?";
display: inline-block;
border: solid 1px currentColor;
border-radius: 50%;
width: 11px;
height: 11px;
font-size: 9px;
line-height: 12px;
text-align: center;
margin-left: 5px;
transform: translateY(-9px);
}
/* Offcanvas menu */
.menu {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
gap: 1.5em;
font-weight: bold;
font-size: 22px;
}
/* Modal */
.modal {
text-align: center;
}
Edit on CodePen
document.addEventListener("DOMContentLoaded", () => {
const main = document.getElementById("demo");
// Setup menu, modal and the their scroll and view triggers.
// In what follows we've used the triggers JavaScript API but we could
// have equally used the respective watchers.
const menu = document.getElementById("demo-menu");
new LISN.widgets.Offcanvas(menu, {
position: "top",
closeButton: false,
});
new LISN.triggers.ScrollTrigger(menu, [new LISN.actions.Open(menu)], {
directions: "up",
});
const modal = document.getElementById("demo-modal");
new LISN.widgets.Modal(modal);
new LISN.triggers.ViewTrigger(modal, [new LISN.actions.Open(modal)], {
target: "top: 50%",
rootMargin: "-48%, 0px",
once: true,
});
// Setup the triggers for the messages.
const msg1 = document.getElementById("msg1");
new LISN.triggers.ScrollTrigger(msg1, [new LISN.actions.Hide(msg1)], {
directions: "up",
once: true,
});
const msg2 = document.getElementById("msg2");
new LISN.triggers.ScrollTrigger(
msg2,
[
new LISN.actions.Show(msg2),
new LISN.actions.Enable(msg2, "hide-on-down"),
],
{
directions: "up",
once: true,
delay: 300,
},
);
// We could use Hide on direction down, but this will set the initial
// state of the element to be shown, so we reverse the logic.
new LISN.triggers.ScrollTrigger(msg2, [new LISN.actions.Show(msg2)], {
directions: "up",
once: true,
id: "hide-on-down",
});
// Setup the collapsibles.
const peekable = main.querySelector(".peekable");
const peekableTrigger = main.querySelector(".peekable-trigger");
new LISN.widgets.Collapsible(peekable, {
triggers: new Map([
// transfer the class also to the trigger wrapper so we can target it with
// .peekable-trigger[data-lisn-is-open="false"]
[peekableTrigger, { className: "peekable-trigger" }],
]),
peek: "75px",
});
for (const item of main.querySelectorAll(".accordion-item")) {
const trigger = item.querySelector(".accordion-item-trigger");
const content = item.querySelector(".accordion-item-content");
new LISN.widgets.Collapsible(content, {
triggers: [trigger],
autoClose: true,
// We can set the trigger properties either here or on a per-trigger
// basis.
icon: "left",
iconClosed: "plus",
iconOpen: "minus",
});
}
// Setup the popups.
for (const item of main.querySelectorAll(".popup")) {
const trigger = item.querySelector(".popup-trigger");
const content = item.querySelector(".popup-content");
new LISN.widgets.Popup(content, {
triggers: new Map([[trigger, { hover: true }]]),
position: "top",
});
}
});
<div id="demo">
<div id="demo-menu">
<a>More demos</a>
<a>Getting started</a>
<a>Source code</a>
</div>
<div id="demo-modal">
You've reached the middle of the page. Are you liking LISN already?
</div>
<div class="section">
<p class="text-center">
This demo includes all four types of openables with several types of
triggers for opening them.
</p>
<h2>Scroll down.</h2>
</div>
<div class="section">
<h2 id="msg2">Now keep going down.</h2>
<div class="spacer"></div>
<h2 id="msg1">Then scroll up a bit.</h2>
</div>
<!-- blank page for spacing -->
<div class="section"></div>
<div class="section top accordions">
<h1 class="huge">Why LISN?</h1>
<div class="peekable">
<div class="accordion-item">
<h4 class="accordion-item-trigger">Lightweight.</h4>
<div class="accordion-item-content">
<div>
<p>Vanilla TypeScript</p>
</div>
<div>
<p>Highly optimized</p>
</div>
<div class="popup">
<p class="popup-trigger">No layout thrashing</p>
<div class="popup-content">
Correct use of <code>requestAnimationFrame</code> to
completely eliminate forced re-layouts and run smoothly even
on mobile devices.
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h4 class="accordion-item-trigger">Interactive.</h4>
<div class="accordion-item-content">
<div>
<p>Powerful API</p>
</div>
<div class="popup">
<p class="popup-trigger">Multi gesture support</p>
<div class="popup-content">
Take actions based on user gestures. Scroll, zoom or drag,
using wheel, touch or pointer device? Any or all of these.
</div>
</div>
<div>
<p>Mobile/touch ready</p>
</div>
</div>
</div>
<div class="accordion-item">
<h4 class="accordion-item-trigger">Simple.</h4>
<div class="accordion-item-content">
<div>
<p>Intuitive syntax</p>
</div>
<div>
<p>Consistent API</p>
</div>
<div class="popup">
<p class="popup-trigger">HTML-only mode</p>
<div class="popup-content">
The HTML-only API can do much of what the full JavaScript API
can.
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h4 class="accordion-item-trigger">No-nonsense.</h4>
<div class="accordion-item-content">
<div>
<p>What says on the box</p>
</div>
<div class="popup">
<p class="popup-trigger">Sensible defaults</p>
<div class="popup-content">
Spend time building your site, not configuring LISN.
</div>
</div>
<div class="popup">
<p class="popup-trigger">Highly customizable</p>
<div class="popup-content">
But if you want to tweak... go wild.
</div>
</div>
</div>
</div>
</div>
<div class="peekable-trigger">
<h5 class="more">~~ View more ~~</h5>
<h5 class="less">~~ View less ~~</h5>
</div>
</div>
</div>
#demo .huge {
font-size: 2.5em;
}
/* Layout */
#demo .section {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
#demo .section.top {
justify-content: flex-start;
}
#demo .spacer {
margin: 50px 0;
}
/* Collapsibles */
#demo .peekable-trigger[data-lisn-is-open="false"] .less {
display: none;
}
#demo .peekable-trigger[data-lisn-is-open="true"] .more {
display: none;
}
#demo .accordion-item-trigger :is(h1, h2, h3, h4, h5, h6) {
margin: 5px 0;
}
#demo p {
margin: 3px 0;
}
#demo .accordion-item-content {
padding: 1em 0 1em 1.3em;
}
/* Popups */
#demo {
--lisn-popup--v-padding: 15px;
--lisn-popup--h-padding: 15px;
--lisn-popup--width: 350px;
--lisn-popup--max-width: 98vw;
}
#demo .popup-content {
font-size: 16px;
}
#demo .popup-trigger::after {
content: "?";
display: inline-block;
border: solid 1px currentColor;
border-radius: 50%;
width: 11px;
height: 11px;
font-size: 9px;
line-height: 12px;
text-align: center;
margin-left: 5px;
transform: translateY(-9px);
}
/* Offcanvas menu */
#demo-menu {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
gap: 1.5em;
font-weight: bold;
font-size: 22px;
}
/* Modal */
#demo-modal {
text-align: center;
}
Edit on CodePen
<div data-lisn-scroll-to-top></div>
<div id="demo">
<div
id="demo-menu"
data-lisn-offcanvas="position=top | close-button=false"
data-lisn-on-scroll="up @open"
>
<a>More demos</a>
<a>Getting started</a>
<a>Source code</a>
</div>
<div
id="demo-modal"
data-lisn-modal
data-lisn-on-view="@open +target=top: 50%
+rootMargin=-48%,0px +once"
>
You've reached the middle of the page. Are you liking LISN already?
</div>
<div class="section">
<p class="text-center">
This demo includes all four types of openables with several types of
triggers for opening them.
</p>
<h2>Scroll down.</h2>
</div>
<div class="section">
<!-- use the @show action for both triggers so that the initial state is hidden -->
<h2
data-lisn-on-scroll="up @show +once +id=hide-on-down ;
up @show @enable:hide-on-down +once +delay=300"
>
Now keep going down.
</h2>
<div class="spacer"></div>
<h2 data-lisn-on-scroll="up @hide +once">Then scroll up a bit.</h2>
</div>
<!-- blank page for spacing -->
<div class="section"></div>
<div class="section top accordions">
<h1 class="huge">Why LISN?</h1>
<div data-lisn-collapsible="peek=75px">
<div>
<h4
data-lisn-collapsible-trigger="icon=left
| icon-closed=plus
| icon-open=minus"
>
Lightweight.
</h4>
<div data-lisn-collapsible="auto-close">
<div>
<p>Vanilla TypeScript</p>
</div>
<div>
<p>Highly optimized</p>
</div>
<div>
<p data-lisn-popup-trigger="hover">No layout thrashing</p>
<div data-lisn-popup="position=top">
Correct use of <code>requestAnimationFrame</code> to
completely eliminate forced re-layouts and run smoothly even
on mobile devices.
</div>
</div>
</div>
</div>
<div>
<h4
data-lisn-collapsible-trigger="icon=left
| icon-closed=plus
| icon-open=minus"
>
Interactive.
</h4>
<div data-lisn-collapsible="auto-close">
<div>
<p>Powerful API</p>
</div>
<div>
<p data-lisn-popup-trigger="hover">Multi gesture support</p>
<div data-lisn-popup="position=top">
Take actions based on user gestures. Scroll, zoom or drag,
using wheel, touch or pointer device? Any or all of these.
</div>
</div>
<div>
<p>Mobile/touch ready</p>
</div>
</div>
</div>
<div>
<h4
data-lisn-collapsible-trigger="icon=left
| icon-closed=plus
| icon-open=minus"
>
Simple.
</h4>
<div data-lisn-collapsible="auto-close">
<div>
<p>Intuitive syntax</p>
</div>
<div>
<p>Consistent API</p>
</div>
<div>
<p data-lisn-popup-trigger="hover">HTML-only mode</p>
<div data-lisn-popup="position=top">
The HTML-only API can do much of what the full JavaScript API
can.
</div>
</div>
</div>
</div>
<div>
<h4
data-lisn-collapsible-trigger="icon=left
| icon-closed=plus
| icon-open=minus"
>
No-nonsense.
</h4>
<div data-lisn-collapsible="auto-close">
<div>
<p>What says on the box</p>
</div>
<div>
<p data-lisn-popup-trigger="hover">Sensible defaults</p>
<div data-lisn-popup="position=top">
Spend time building your site, not configuring LISN.
</div>
</div>
<div>
<p data-lisn-popup-trigger="hover">Highly customizable</p>
<div data-lisn-popup="position=top">
But if you want to tweak... go wild.
</div>
</div>
</div>
</div>
</div>
<div data-lisn-collapsible-trigger="class-name=peekable-trigger">
<h5 class="more">~~ View more ~~</h5>
<h5 class="less">~~ View less ~~</h5>
</div>
</div>
</div>
#demo .huge {
font-size: 2.5em;
}
/* Layout */
#demo .section {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
#demo .section.top {
justify-content: flex-start;
}
#demo .spacer {
margin: 50px 0;
}
/* Collapsibles */
#demo .peekable-trigger[data-lisn-is-open="false"] .less {
display: none;
}
#demo .peekable-trigger[data-lisn-is-open="true"] .more {
display: none;
}
#demo .accordion-item-trigger :is(h1, h2, h3, h4, h5, h6) {
margin: 5px 0;
}
#demo p {
margin: 3px 0;
}
#demo .accordion-item-content {
padding: 1em 0 1em 1.3em;
}
/* Popups */
#demo {
--lisn-popup--v-padding: 15px;
--lisn-popup--h-padding: 15px;
--lisn-popup--width: 350px;
--lisn-popup--max-width: 98vw;
}
#demo [data-lisn-popup] {
font-size: 16px;
}
#demo [data-lisn-popup-trigger]::after {
content: "?";
display: inline-block;
border: solid 1px currentColor;
border-radius: 50%;
width: 11px;
height: 11px;
font-size: 9px;
line-height: 12px;
text-align: center;
margin-left: 5px;
transform: translateY(-9px);
}
/* Offcanvas menu */
#demo-menu {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
gap: 1.5em;
font-weight: bold;
font-size: 22px;
}
/* Modal */
#demo-modal {
text-align: center;
}
This demo includes all four types of openables with several types of triggers for opening them.
Vanilla TypeScript
Highly optimized
No layout thrashing
requestAnimationFrame
to completely eliminate forced re-layouts and run smoothly even on mobile devices.Powerful API
Multi gesture support
Mobile/touch ready
Intuitive syntax
Consistent API
HTML-only mode
What says on the box
Sensible defaults
Highly customizable