CSS Scroll Snap Points with navigation (next, previous) buttons - javascript

I am building a carousel, very minimalist, using CSS snap points. It is important for me to have CSS only options, but I'm fine with enhancing a bit with javascript (no framework).
I am trying to add previous and next buttons to scroll programmatically to the next or previous element. If javascript is disabled, buttons will be hidden and carousel still functionnal.
My issue is about how to trigger the scroll to the next snap point ?
All items have different size, and most solution I found require pixel value (like scrollBy used in the exemple). A scrollBy 40px works for page 2, but not for others since they are too big (size based on viewport).
function goPrecious() {
document.getElementById('container').scrollBy({
top: -40,
behavior: 'smooth'
});
}
function goNext() {
document.getElementById('container').scrollBy({
top: 40,
behavior: 'smooth'
});
}
#container {
scroll-snap-type: y mandatory;
overflow-y: scroll;
border: 2px solid var(--gs0);
border-radius: 8px;
height: 60vh;
}
#container div {
scroll-snap-align: start;
display: flex;
justify-content: center;
align-items: center;
font-size: 4rem;
}
#container div:nth-child(1) {
background: hotpink;
color: white;
height: 50vh;
}
#container div:nth-child(2) {
background: azure;
height: 40vh;
}
#container div:nth-child(3) {
background: blanchedalmond;
height: 60vh;
}
#container div:nth-child(4) {
background: lightcoral;
color: white;
height: 40vh;
}
<div id="container">
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
</div>
<button onClick="goPrecious()">previous</button>
<button onClick="goNext()">next</button>

Nice question! I took this as a challenge.
So, I increased JavaScript for it to work dynamically. Follow my detailed solution (in the end the complete code):
First, add position: relative to the .container, because it need to be reference for scroll and height checkings inside .container.
Then, let's create 3 global auxiliary variables:
1) One to get items scroll positions (top and bottom) as arrays into an array. Example: [[0, 125], [125, 280], [280, 360]] (3 items in this case).
3) One that stores half of .container height (it will be useful later).
2) Another one to store the item index for scroll position
var carouselPositions;
var halfContainer;
var currentItem;
Now, a function called getCarouselPositions that creates the array with items positions (stored in carouselPositions) and calculates the half of .container (stored in halfContainer):
function getCarouselPositions() {
carouselPositions = [];
document.querySelectorAll('#container div').forEach(function(div) {
carouselPositions.push([div.offsetTop, div.offsetTop + div.offsetHeight]); // add to array the positions information
})
halfContainer = document.querySelector('#container').offsetHeight/2;
}
getCarouselPositions(); // call it once
Let's replace the functions on buttons. Now, when you click on them, the same function will be called, but with "next" or "previous" argument:
<button onClick="goCarousel('previous')">previous</button>
<button onClick="goCarousel('next')">next</button>
Here is about the goCarousel function itself:
First, it creates 2 variables that store top scroll position and bottom scroll position of carousel.
Then, there are 2 conditionals to see if the current carousel position is on most top or most bottom.
If it's on top and clicked "next" button, it will go to the second item position. If it's on bottom and clicked "previous" button, it will go the previous one before the last item.
If both conditionals failed, it means the current item is not the first or the last one. So, it checks to see what is the current position, calculating using the half of the container in a loop with the array of positions to see what item is showing. Then, it combines with "previous" or "next" checking to set the correct next position for currentItem variable.
Finally, it goes to the correct position through scrollTo using currentItem new value.
Below, the complete code:
var carouselPositions;
var halfContainer;
var currentItem;
function getCarouselPositions() {
carouselPositions = [];
document.querySelectorAll('#container div').forEach(function(div) {
carouselPositions.push([div.offsetTop, div.offsetTop + div.offsetHeight]); // add to array the positions information
})
halfContainer = document.querySelector('#container').offsetHeight/2;
}
getCarouselPositions(); // call it once
function goCarousel(direction) {
var currentScrollTop = document.querySelector('#container').scrollTop;
var currentScrollBottom = currentScrollTop + document.querySelector('#container').offsetHeight;
if (currentScrollTop === 0 && direction === 'next') {
currentItem = 1;
} else if (currentScrollBottom === document.querySelector('#container').scrollHeight && direction === 'previous') {
console.log('here')
currentItem = carouselPositions.length - 2;
} else {
var currentMiddlePosition = currentScrollTop + halfContainer;
for (var i = 0; i < carouselPositions.length; i++) {
if (currentMiddlePosition > carouselPositions[i][0] && currentMiddlePosition < carouselPositions[i][1]) {
currentItem = i;
if (direction === 'next') {
currentItem++;
} else if (direction === 'previous') {
currentItem--
}
}
}
}
document.getElementById('container').scrollTo({
top: carouselPositions[currentItem][0],
behavior: 'smooth'
});
}
window.addEventListener('resize', getCarouselPositions);
#container {
scroll-snap-type: y mandatory;
overflow-y: scroll;
border: 2px solid var(--gs0);
border-radius: 8px;
height: 60vh;
position: relative;
}
#container div {
scroll-snap-align: start;
display: flex;
justify-content: center;
align-items: center;
font-size: 4rem;
}
#container div:nth-child(1) {
background: hotpink;
color: white;
height: 50vh;
}
#container div:nth-child(2) {
background: azure;
height: 40vh;
}
#container div:nth-child(3) {
background: blanchedalmond;
height: 60vh;
}
#container div:nth-child(4) {
background: lightcoral;
color: white;
height: 40vh;
}
<div id="container">
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
</div>
<button onClick="goCarousel('previous')">previous</button>
<button onClick="goCarousel('next')">next</button>
Another good detail to add is to call getCarouselPositions function again if the window resizes:
window.addEventListener('resize', getCarouselPositions);
That's it.
That was cool to do. I hope it can help somehow.

I've just done something similar recently. The idea is to use IntersectionObserver to keep track of which item is in view currently and then hook up the previous/next buttons to event handler calling Element.scrollIntoView().
Anyway, Safari does not currently support scroll behavior options. So you might want to polyfill it on demand with polyfill.app service.
let activeIndex = 0;
const container = document.querySelector("#container");
const elements = [...document.querySelectorAll("#container div")];
function handleIntersect(entries){
const entry = entries.find(e => e.isIntersecting);
if (entry) {
const index = elements.findIndex(
e => e === entry.target
);
activeIndex = index;
}
}
const observer = new IntersectionObserver(handleIntersect, {
root: container,
rootMargin: "0px",
threshold: 0.75
});
elements.forEach(el => {
observer.observe(el);
});
function goPrevious() {
if(activeIndex > 0) {
elements[activeIndex - 1].scrollIntoView({
behavior: 'smooth'
})
}
}
function goNext() {
if(activeIndex < elements.length - 1) {
elements[activeIndex + 1].scrollIntoView({
behavior: 'smooth'
})
}
}
#container {
scroll-snap-type: y mandatory;
overflow-y: scroll;
border: 2px solid var(--gs0);
border-radius: 8px;
height: 60vh;
}
#container div {
scroll-snap-align: start;
display: flex;
justify-content: center;
align-items: center;
font-size: 4rem;
}
#container div:nth-child(1) {
background: hotpink;
color: white;
height: 50vh;
}
#container div:nth-child(2) {
background: azure;
height: 40vh;
}
#container div:nth-child(3) {
background: blanchedalmond;
height: 60vh;
}
#container div:nth-child(4) {
background: lightcoral;
color: white;
height: 40vh;
}
<div id="container">
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
</div>
<button onClick="goPrevious()">previous</button>
<button onClick="goNext()">next</button>

An easier approach done with react.
export const AppCarousel = props => {
const containerRef = useRef(null);
const carouselRef = useRef(null);
const [state, setState] = useState({
scroller: null,
itemWidth: 0,
isPrevHidden: true,
isNextHidden: false
})
const next = () => {
state.scroller.scrollBy({left: state.itemWidth * 3, top: 0, behavior: 'smooth'});
// Hide if is the last item
setState({...state, isNextHidden: true, isPrevHidden: false});
}
const prev = () => {
state.scroller.scrollBy({left: -state.itemWidth * 3, top: 0, behavior: 'smooth'});
setState({...state, isNextHidden: false, isPrevHidden: true});
// Hide if is the last item
// Show remaining
}
useEffect(() => {
const items = containerRef.current.childNodes;
const scroller = containerRef.current;
const itemWidth = containerRef.current.firstElementChild?.clientWidth;
setState({...state, scroller, itemWidth});
return () => {
}
},[props.items])
return (<div className="app-carousel" ref={carouselRef}>
<div className="carousel-items shop-products products-swiper" ref={containerRef}>
{props.children}
</div>
<div className="app-carousel--navigation">
<button className="btn prev" onClick={e => prev()} hidden={state.isPrevHidden}><</button>
<button className="btn next" onClick={e => next()} hidden={state.isNextHidden}>></button>
</div>
</div>)
}

I was struggling with the too while working with a react project and came up with this solution. Here's a super basic example of the code using react and styled-components.
import React, { useState, useRef } from 'react';
import styled from 'styled-components';
const App = () => {
const ref = useRef();
const [scrollX, setScrollX] = useState(0);
const scrollSideways = (px) => {
ref.current.scrollTo({
top: 0,
left: scrollX + px,
behavior: 'smooth'
});
setScrollX(scrollX + px);
};
return (
<div>
<List ref={ref}>
<ListItem color="red">Card 1</ListItem>
<ListItem color="blue">Card 2</ListItem>
<ListItem color="green">Card 3</ListItem>
<ListItem color="yellow">Card 4</ListItem>
</List>
<button onClick={() => scrollSideways(-600)}> Left </button>
<button onClick={() => scrollSideways(600)}> Right </button>
</div>
);
};
const List = styled.ul`
display: flex;
overflow-x: auto;
padding-inline-start: 40px;
scroll-snap-type: x mandatory;
list-style: none;
padding: 40px;
width: 700px;
`;
const ListItem = styled.li`
display: flex;
flex-shrink: 0;
scroll-snap-align: start;
background: ${(p) => p.color};
width: 600px;
margin-left: 15px;
height: 200px;
`;

Related

How to add a smooth animation to the progress bar

When I click I want to smoothly add segments to the progress bar. They are added but instantly. What could be the problem?
I tried to implement a smooth animation with setInterval, but nothing comes out. Percentages are also added instantly.
let progressBar = document.querySelector(".progressbar");
let progressBarValue = document.querySelector(".progressbar__value");
const body = document.querySelector("body");
let progressBarStartValue = 0;
let progressBarEndValue = 100;
let speed = 50;
body.addEventListener("click", function(e) {
if (progressBarStartValue === progressBarEndValue) {
alert("you have completed all the tasks");
} else {
let progress = setInterval(() => {
if (progressBarStartValue != 100) {
progressBarStartValue += 10;
clearInterval(progress);
}
progressBarValue.textContent = `${progressBarStartValue}%`;
progressBar.style.background = `conic-gradient(
#FFF ${progressBarStartValue * 3.6}deg,
#262623 ${progressBarStartValue * 3.6}deg
)`;
}, speed);
}
});
.progressbar {
position: relative;
height: 150px;
width: 150px;
background-color: #262623;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.progressbar::before {
content: "";
position: absolute;
height: 80%;
width: 80%;
background-color: #0f0f0f;
border-radius: 50%;
}
.progressbar__value {
color: #fff;
z-index: 9;
font-size: 25px;
font-weight: 600;
}
<main class="main">
<section class="statistic">
<div class="container">
<div class="statistic__inner">
<div class="statistic__text">
<h2 class="statistic__title">You're almost there!</h2>
<p class="statistic__subtitle">keep up the good work</p>
</div>
<div class="progressbar"><span class="progressbar__value">0%</span></div>
</div>
</div>
</section>
</main>
This may not be exactly what you're looking for, but with the conic-gradient() implementation you're using, I'd recommend checking out a library call anime.js.
Here's an example with your implementation (same html and css):
// your.js
let progressBar = document.querySelector(".progressbar");
let progressBarValue = document.querySelector(".progressbar__value");
const body = document.querySelector("body");
// Switched to object for target in anime()
let progressBarObject = {
progressBarStartValue: 0,
progressBarEndValue: 100,
progressBarAnimationValue: 0 * 3.6 // New value needed for smoothing the progress bar, since the progress value needs to be multiplied by 3.6
}
// Not necessary, but I recommend changing the event listener to pointerup for better support
// Also not necessary, I changed function to arrow function for my own preference
body.addEventListener("pointerup", e => {
e.preventDefault()
if (progressBarObject.progressBarStartValue === progressBarObject.progressBarEndValue) {
alert("you have completed all the tasks");
} else {
let newValue = 0 // Needed so we can set the value, before it's applied in anime()
if (progressBarObject.progressBarStartValue != 100) {
// Math.ceil() allows us to round to the nearest 10 to guarantee the correct output
newValue = Math.ceil((progressBarObject.progressBarStartValue + 10) / 10) * 10;
}
// Optional: Prevents accidentally going over 100 somehow
if (newValue > 100) {
newValue = 100
}
anime({
targets: progressBarObject,
progressBarStartValue: newValue,
progressBarAnimationValue: newValue * 3.6,
easing: 'easeInOutExpo',
round: 1, // Rounds to nearest 1 so you don't have 0.3339...% displayed in progressBarValue
update: () => {
progressBar.style.backgroundImage = `conic-gradient(
#FFF ${progressBarObject.progressBarAnimationValue}deg,
#262623 ${progressBarObject.progressBarAnimationValue}deg)`;
progressBarValue.textContent = `${progressBarObject.progressBarStartValue}%`;
},
duration: 500
});
}
});
Here's a CodePen using the anime.js CDN: Circular Progress Bar Smoothing
If you don't want to use a javascript library, then I'd recommend switching from the conic-gradient() to something else. I hear using an .svg circle with stroke and stroke-dasharray can work great with CSS transition.
You shouldn't setInterval your progress variable like this. instead, put it as a global variable outside the function then use it to gradually add 1 as long as the start value is less than progress, and you still can control the speed with your speed variable.
let progressBar = document.querySelector(".progressbar");
let progressBarValue = document.querySelector(".progressbar__value");
const body = document.querySelector("body");
let progressBarStartValue = 0;
let progressBarEndValue = 100;
let speed = 50;
let progress = 0;
body.addEventListener("click", function(e) {
if (progressBarStartValue === progressBarEndValue) {
alert("you have completed all the tasks");
} else {
progress += 10;
setInterval(() => {
if (progressBarStartValue < progress) {
progressBarStartValue += 1;
clearInterval();
}
progressBarValue.textContent = `${progressBarStartValue}%`;
progressBar.style.background = `conic-gradient(
#FFF ${progressBarStartValue * 3.6}deg,
#262623 ${progressBarStartValue * 3.6}deg
)`;
}, speed);
}
});
.progressbar {
position: relative;
height: 150px;
width: 150px;
background-color: #262623;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 3px solid red;
}
.progressbar::before {
content: "";
position: absolute;
height: 80%;
width: 80%;
background-color: #0f0f0f;
border-radius: 50%;
border: 3px solid blue;
}
.progressbar__value {
color: #fff;
z-index: 9;
font-size: 25px;
font-weight: 600;
}
<main class="main">
<section class="statistic">
<div class="container">
<div class="statistic__inner">
<div class="statistic__text">
<h2 class="statistic__title">You're almost there!</h2>
<p class="statistic__subtitle">keep up the good work</p>
</div>
<div class="progressbar"><span class="progressbar__value">0%</span></div>
</div>
</div>
</section>
</main>

Scroll a web page using Javascript whose scroll triggers at the middle of page

I have encountered some websites which has footer at the bottom and scroll actually happens when I scroll to the area above footer.
To automatically scroll those pages, but the problem with my code currently is it goes at the bottom of the page, where I directly reach footer and hence the scroll trigger which is present just above the footer does not gets triggered.
Is there any way to achieve the same?
This is what I have tried currently which I am executing from the console:
(function() {
var intervalObj = null;
var retry = 0;
var clickHandler = function() {
console.log("Clicked; stopping autoscroll");
clearInterval(intervalObj);
document.body.removeEventListener("click", clickHandler);
}
function scrollDown() {
var scrollHeight = document.body.scrollHeight,
scrollTop = document.body.scrollTop,
innerHeight = window.innerHeight,
difference = (scrollHeight - scrollTop) - innerHeight
if (difference > 0) {
window.scrollBy(0, difference);
if (retry > 0) {
retry = 0;
}
console.log("scrolling down more");
} else {
if (retry >= 3) {
console.log("reached bottom of page; stopping");
clearInterval(intervalObj);
document.body.removeEventListener("click", clickHandler);
} else {
console.log("[apparenty] hit bottom of page; retrying: " + (retry + 1));
retry++;
}
}
}
document.body.addEventListener("click", clickHandler);
intervalObj = setInterval(scrollDown, 1000);
})()
There are many websites that has this feature, to test the same one of the website which you can try is
https://www.zomato.com/bangalore/indiranagar-restaurants
Note : The question similar to this does not answer how to scroll at some mid point of page instead it takes me directly to the footer, so this is not a duplicate
Logic is to retain to the middle of the Scroller unless the page is completely loaded. We can tweak the code a little to achieve the last position of scroller. Try this:
var scrollHeight = 0,
newScrollHeight;
do {
window.scrollTo(0, document.body.scrollHeight / 2);
newScrollHeight = document.body.scrollHeight / 2;
if (newScrollHeight == scrollHeight) {
break;
} else {
scrollHeight = newScrollHeight;
}
} while (true);
Although Kumar Rishabh has already answered your question, I have another solution for this situation.
Set the domain to detect if the user scrolls to the domain.
The effect just like the website you povider . https://www.zomato.com/bangalore/indiranagar-restaurants
I do some simple example for you with pure Javascript.
Fragment core code:
// Here is domain to detect if user scroll into.
if (
triggerDomain.getBoundingClientRect().top < window.innerHeight &&
triggerDomain.getBoundingClientRect().bottom > 0
) {
if (getMore === false) {
getMore = true
// Do something you want here ....
console.info('got more !!')
Full code sample, check code snippet:
const rootElement = document.getElementById("rootDiv");
const triggerDomain = document.getElementById("triggerDomain");
let getMore = false;
function detectScrollIntoDomain() {
// Here is domain to detect if user scroll into.
if (
triggerDomain.getBoundingClientRect().top < window.innerHeight &&
triggerDomain.getBoundingClientRect().bottom > 0
) {
if (getMore === false) {
getMore = true;
// Do something you want here ....
console.info("got more !!");
setTimeout(() => {
let currentScrollTop = rootElement.scrollTop;
for (let i = 0; i < 15; i++) {
let r = Math.floor(Math.random() * 255);
let g = Math.floor(Math.random() * 255);
let b = Math.floor(Math.random() * 255);
const contentElement = document.getElementById("content");
const card = document.createElement("div");
card.className = "contentCard";
card.style.backgroundColor = `rgba(${r}, ${g}, ${b})`;
contentElement.appendChild(card);
}
rootElement.scrollTo(0, currentScrollTop);
// Don't forget to set flag to `false`.
getMore = false;
}, 200);
}
}
}
rootElement.addEventListener("scroll", detectScrollIntoDomain, {
passive: true
});
html,
body {
position: relative;
font-family: sans-serif;
padding: 0;
margin: 0;
height: 100%;
width: 100%;
}
h1,
h2 {
margin: 0;
color: aliceblue;
}
#rootDiv {
position: relative;
overflow: auto;
height: 100%;
width: 100%;
}
#header {
height: 200px;
background-color: rgb(112, 112, 112);
}
#content {
display: flex;
flex-wrap: wrap;
position: relative;
height: fit-content;
background-color: rgb(136, 136, 136);
}
#content div:first-child {
height: 600px;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
#triggerDomain {
position: absolute;
bottom: 0px;
outline: 1px dashed rgb(3, 25, 119);
height: 500px;
width: 100%;
background: repeating-linear-gradient(
135deg,
rgba(46, 45, 45, 0.3) 0,
rgba(46, 45, 45, 0.3) 10px,
rgba(136, 136, 136, 0.3) 10px,
rgba(136, 136, 136, 0.3) 20px
);
}
#footer {
height: 180%;
background-color: rgb(112, 112, 112);
}
.contentCard {
width: 180px;
height: 180px;
margin: 12px;
border-radius: 8px;
background-color: aquamarine;
}
<!DOCTYPE html>
<html>
<head>
<title>Parcel Sandbox</title>
<meta charset="UTF-8" />
<link rel="stylesheet" type="text/css" href="./src/styles.css"
</head>
<body>
<div id="rootDiv">
<div id="header">
<h1>Header</h1>
</div>
<div id="content">
<div>
<h2>Content</h2>
<h2>Scroll down to get more cards.</h2>
</div>
<div id="triggerDomain">
<h2>Trigger domain</h2>
</div>
</div>
<div id="footer">
<h2>Footer</h2>
</div>
</div>
<script src="src/index.js"></script>
</body>
</html>
Hope to help you !

How do I use a single IntersectionObserver to perform unique callbacks per observed element?

I need to know out when dozens of HTMLElements are inside or outside of the viewport when scrolling down the page. So I'm using the IntersectionObserver API to create several instances of a VisibilityHelper class, each one with its own IntersectionObserver. With this helper class, I can detect when any HTMLElement is 50% visible or hidden:
Working demo:
// Create helper class
class VisibilityHelper {
constructor(htmlElem, hiddenCallback, visibleCallback) {
this.observer = new IntersectionObserver((entities) => {
const ratio = entities[0].intersectionRatio;
if (ratio <= 0.0) {
hiddenCallback();
} else if (ratio >= 0.5) {
visibleCallback();
}
}, {threshold: [0.0, 0.5]});
this.observer.observe(htmlElem);
}
}
// Get elements
const headerElem = document.getElementById("header");
const footerElem = document.getElementById("footer");
// Use helper class to know whether visible or hidden
const headerViz = new VisibilityHelper(
headerElem,
() => {console.log('header is hidden')},
() => {console.log('header is visible')},
);
const footerViz = new VisibilityHelper(
footerElem,
() => {console.log('footer is hidden')},
() => {console.log('footer is visible')},
);
#page {
width: 100%;
height: 1500px;
position: relative;
background: linear-gradient(#000, #fff);
}
#header {
position: absolute;
top: 0;
width: 100%;
height: 100px;
background: #f90;
text-align: center;
}
#footer {
position: absolute;
bottom: 0;
width: 100%;
height: 100px;
background: #09f;
text-align: center;
}
<div id="page">
<div id="header">
Header
</div>
<div id="footer">
Footer
</div>
</div>
The problem is that my demo above creates one IntersectionObserver for each HTMLElement that needs to be watched. I need to use this on 100 elements, and this question indicates that we should only use one IntersectionObserver per page for performance reasons. Secondly, the API also suggests that one observer can be used to watch several elements, since the callback will give you a list of entries.
How would you use a single IntersectionObserver to watch multiple htmlElements and trigger unique hidden/visible callbacks for each element?
You can define a callback mapping between your target elements and their visibility state. Then inside of your IntersectionObserver callback, use the IntersectionObserverEntry.target to read the id and invoke the associated callback from the map based on the visibility state of visible or hidden.
Here is a simplified approach based on your example. The gist of the approach is defining the callbacks map and reading the target from the IntersectionObserverEntry:
// Create helper class
class VisibilityHelper {
constructor(htmlElems, callbacks) {
this.callbacks = callbacks;
this.observer = new IntersectionObserver(
(entities) => {
const ratio = entities[0].intersectionRatio;
const target = entities[0].target;
if (ratio <= 0.0) {
this.callbacks[target.id].hidden();
} else if (ratio >= 0.5) {
this.callbacks[target.id].visible();
}
},
{ threshold: [0.0, 0.5] }
);
htmlElems.forEach((elem) => this.observer.observe(elem));
}
}
// Get elements
const headerElem = document.getElementById("header");
const footerElem = document.getElementById("footer");
// Use helper class to know whether visible or hidden
const helper = new VisibilityHelper([headerElem, footerElem], {
header: {
visible: () => console.log("header is visible"),
hidden: () => console.log("header is hidden"),
},
footer: {
visible: () => console.log("footer is visible"),
hidden: () => console.log("footer is hidden"),
},
});
#page {
width: 100%;
height: 1500px;
position: relative;
background: linear-gradient(#000, #fff);
}
#header {
position: absolute;
top: 0;
width: 100%;
height: 100px;
background: #f90;
text-align: center;
}
#footer {
position: absolute;
bottom: 0;
width: 100%;
height: 100px;
background: #09f;
text-align: center;
}
<div id="page">
<div id="header">
Header
</div>
<div id="footer">
Footer
</div>
</div>
You can use a similar approach if you want to loop over the entire array of entities instead of just considering the first entities[0].

Creating an active status for links on a sticky nav bar in relation to their sections

I have been trying to make the links in my sticky nav bar highlight when on the corresponding section.
I have been able to get it to partially work using JavaScript with all links being highlighted when scrolling over the corresponding sections, however, the section 1 link is highlighted even if I'm on the landing page and have not scrolled at all. How can I make it so that section 1 isn't highlighted until I scroll to the section?
I've tried to use an if statement to set all links to not active if window.scrollY === 0, but this only works for the initial render and doesn't work after I have scrolled down and back.
Update
Adding another if statement at the start of the highlightNav function to check if window.scrollY === 0 has allowed me to remove the .active class from all the links, but ideally I want it to work when window.scrollY < 450 which doesn't seem to work.
My JavaScript is as follows:
const highlightNav = () => {
const links = document.querySelectorAll('.link');
const section = document.querySelectorAll('.section');
//updated if statement
//I tried using window.scrollY < 450 in the if statement below with no success
if (window.scrollY === 0) {
links.forEach((link) => link.classList.remove('active'));
};
if (window.scrollY !== 0) {
const changeLinkState = () => {
let index = section.length;
while(--index && window.scrollY + 450 < section[index].offsetTop) {};
links.forEach((link) => link.classList.remove('active'));
links[index].classList.add('active');
};
changeLinkState();
};
};
highlightNav();
window.addEventListener('scroll', highlightNav);
The HTML follows this structure:
<header>
<div class="nav-bar">
<nav>
About
Pricing
Contact
Sign in
</nav>
</div>
<section>
<!-- Landing Screen -->
</section>
</header>
<article>
<section id="about" class="section">
<!-- section 1 -->
</section>
<section id="price" class="section">
<!-- Section two -->
</section>
<section id="contact" class="section">
<!-- Section Three -->
</section>
</article>
css:
.nav-bar {
position: fixed;
top: 0px;
width: 100%;
height: 8vh;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #297094;
text-align: center;
box-shadow: 0 3px 3px rgba(0, 0, 0, 0.3);
}
nav {
width: 25%;
height: 100%;
display: flex;
justify-content: flex-end;
align-items: center;
}
nav a {
padding: 0 10%;
border-left: 1px solid black;
height: 100%;
display: flex;
align-items: center;
font-size: 1.25rem;
color: white;
transition: background-color 600ms;
text-decoration: none;
white-space: nowrap;
}
nav a:hover {
background-color: #3B80A0;
cursor: pointer;
}
.active {
background-color: #3B80A0;
}
I'm very new to JavaScript so any help is welcome, thanks in advance.
Main issue:
Instead of mixing that 450 math into a while loop, define that amount as a constant with a descriptive name, then use it in your conditional.
A couple other notes:
The while loop in your posted code doesn't do anything, since there's nothing inside the {}
You call links.forEach((link) => link.classList.remove('active')); twice, so make that its own function (I've named it clearAllActive() here).
const highlightNav = () => {
const links = document.querySelectorAll('.link');
const sections = document.querySelectorAll('.section');
const clearAllActive = () => links.forEach(link => link.classList.remove('active'));
const highlightActive = () => {
// your highlighting code here
};
// don't start highlighting until we've scrolled at least this far
const minScrollBeforeHighlighting = 450;
if (window.scrollY < minScrollBeforeHighlighting) {
clearAllActive();
} else {
highlightActive();
};
};
highlightNav();
window.addEventListener('scroll', highlightNav);

Making a dragbar to resize divs inside CSS grids

I have 2 boxes and a vertical div line in one unique container div (code and fiddle below).
I'm using CSS grids to position my elements inside the container
What I'd like to accomplish is to use the vertical line to resize horizontally the two boxes based on the position of the vertical line.
I apologize if the question is noobish, I am new to web development, only used Python before, already tried to google and stackoverflow search but all solutions seem overly complicated and generally require additional libraries, I was looking for something simpler and JS only.
HTML:
<div class="wrapper">
<div class="box a">A</div>
<div class="handler"></div>
<div class="box b">B</div>
</div>
CSS:
body {
margin: 40px;
}
.wrapper {
display: grid;
grid-template-columns: 200px 8px 200px;
grid-gap: 10px;
background-color: #fff;
color: #444;
}
.box {
background-color: #444;
color: #fff;
border-radius: 5px;
padding: 20px;
font-size: 150%;
resize: both;
}
.handler{
width: 3px;
height: 100%;
padding: 0px 0;
top: 0;
background: red;
draggable: true;
}
https://jsfiddle.net/gv8Lwckh/6/
What you intend to do can be done using CSS flexbox—there is no need to use CSS grid. The bad news is that HTML + CSS is not so smart that declaring resize and draggable will make the layout flexible and adjustable by user interaction. For that, you will have to use JS. The good news is that this is actually not too complicated.
Here is a quick screen grab of output the code below:
However, for you to understand the code I will post below, you will have to familiarize yourself with:
Event binding using .addEventListener. In this case, we will use a combination of mousedown, mouseup and mousemove to determine whether the user is in the middle of dragging the element
CSS flexbox layout
Description of the solution
Initial layout using CSS
Firstly, you will want to layout your boxes using CSS flexbox. We simply declare display: flex on the parent, and then use flex: 1 1 auto (which translates to "let the element grow, let the element shrink, and have equal widths). This layout is only valid at the initial rendering of the page:
.wrapper {
/* Use flexbox */
display: flex;
}
.box {
/* Use box-sizing so that element's outerwidth will match width property */
box-sizing: border-box;
/* Allow box to grow and shrink, and ensure they are all equally sized */
flex: 1 1 auto;
}
Listen to drag interaction
You want to listen to mouse events that might have originated from your .handler element, and you want a global flag that remembers whether the user is dragging or not:
var handler = document.querySelector('.handler');
var isHandlerDragging = false;
Then you can use the following logic to check if the user is dragging or not:
document.addEventListener('mousedown', function(e) {
// If mousedown event is fired from .handler, toggle flag to true
if (e.target === handler) {
isHandlerDragging = true;
}
});
document.addEventListener('mousemove', function(e) {
// Don't do anything if dragging flag is false
if (!isHandlerDragging) {
return false;
}
// Set boxA width properly
// [...more logic here...]
});
document.addEventListener('mouseup', function(e) {
// Turn off dragging flag when user mouse is up
isHandlerDragging = false;
});
Computing the width of box A
All you are left with now is to compute the width of box A (to be inserted in the [...more logic here...] placeholder in the code above), so that it matches that of the movement of the mouse. Flexbox will ensure that box B will fill up the remaining space:
// Get offset
var containerOffsetLeft = wrapper.offsetLeft;
// Get x-coordinate of pointer relative to container
var pointerRelativeXpos = e.clientX - containerOffsetLeft;
// Resize box A
// * 8px is the left/right spacing between .handler and its inner pseudo-element
// * Set flex-grow to 0 to prevent it from growing
boxA.style.width = (pointerRelativeXpos - 8) + 'px';
boxA.style.flexGrow = 0;
Working example
var handler = document.querySelector('.handler');
var wrapper = handler.closest('.wrapper');
var boxA = wrapper.querySelector('.box');
var isHandlerDragging = false;
document.addEventListener('mousedown', function(e) {
// If mousedown event is fired from .handler, toggle flag to true
if (e.target === handler) {
isHandlerDragging = true;
}
});
document.addEventListener('mousemove', function(e) {
// Don't do anything if dragging flag is false
if (!isHandlerDragging) {
return false;
}
// Get offset
var containerOffsetLeft = wrapper.offsetLeft;
// Get x-coordinate of pointer relative to container
var pointerRelativeXpos = e.clientX - containerOffsetLeft;
// Arbitrary minimum width set on box A, otherwise its inner content will collapse to width of 0
var boxAminWidth = 60;
// Resize box A
// * 8px is the left/right spacing between .handler and its inner pseudo-element
// * Set flex-grow to 0 to prevent it from growing
boxA.style.width = (Math.max(boxAminWidth, pointerRelativeXpos - 8)) + 'px';
boxA.style.flexGrow = 0;
});
document.addEventListener('mouseup', function(e) {
// Turn off dragging flag when user mouse is up
isHandlerDragging = false;
});
body {
margin: 40px;
}
.wrapper {
background-color: #fff;
color: #444;
/* Use flexbox */
display: flex;
}
.box {
background-color: #444;
color: #fff;
border-radius: 5px;
padding: 20px;
font-size: 150%;
/* Use box-sizing so that element's outerwidth will match width property */
box-sizing: border-box;
/* Allow box to grow and shrink, and ensure they are all equally sized */
flex: 1 1 auto;
}
.handler {
width: 20px;
padding: 0;
cursor: ew-resize;
flex: 0 0 auto;
}
.handler::before {
content: '';
display: block;
width: 4px;
height: 100%;
background: red;
margin: 0 auto;
}
<div class="wrapper">
<div class="box">A</div>
<div class="handler"></div>
<div class="box">B</div>
</div>
Here's an example of the drag event handling, but using CSS Grids
The trick is to set the grid-template-columns (or rows) on the grid container rather than than the size of the grid items
let isLeftDragging = false;
let isRightDragging = false;
function ResetColumnSizes() {
// when page resizes return to default col sizes
let page = document.getElementById("pageFrame");
page.style.gridTemplateColumns = "2fr 6px 6fr 6px 2fr";
}
function SetCursor(cursor) {
let page = document.getElementById("page");
page.style.cursor = cursor;
}
function StartLeftDrag() {
// console.log("mouse down");
isLeftDragging = true;
SetCursor("ew-resize");
}
function StartRightDrag() {
// console.log("mouse down");
isRightDragging = true;
SetCursor("ew-resize");
}
function EndDrag() {
// console.log("mouse up");
isLeftDragging = false;
isRightDragging = false;
SetCursor("auto");
}
function OnDrag(event) {
if (isLeftDragging || isRightDragging) {
// console.log("Dragging");
//console.log(event);
let page = document.getElementById("page");
let leftcol = document.getElementById("leftcol");
let rightcol = document.getElementById("rightcol");
let leftColWidth = isLeftDragging ? event.clientX : leftcol.clientWidth;
let rightColWidth = isRightDragging ? page.clientWidth - event.clientX : rightcol.clientWidth;
let dragbarWidth = 6;
let cols = [
leftColWidth,
dragbarWidth,
page.clientWidth - (2 * dragbarWidth) - leftColWidth - rightColWidth,
dragbarWidth,
rightColWidth
];
let newColDefn = cols.map(c => c.toString() + "px").join(" ");
// console.log(newColDefn);
page.style.gridTemplateColumns = newColDefn;
event.preventDefault()
}
}
#page {
height: 100%;
background-color: pink;
display: grid;
grid-template-areas: 'header header header header header' 'leftcol leftdragbar tabs tabs tabs' 'leftcol leftdragbar tabpages rightdragbar rightcol' 'leftcol leftdragbar footer footer footer';
grid-template-rows: min-content 1fr 9fr 1fr;
grid-template-columns: 2fr 6px 6fr 6px 2fr;
}
/*****************************/
#header {
background-color: lightblue;
overflow: auto;
grid-area: header;
}
#leftcol {
background-color: #aaaaaa;
overflow: auto;
grid-area: leftcol;
}
#leftdragbar {
background-color: black;
grid-area: leftdragbar;
cursor: ew-resize;
}
#tabs {
background-color: #cccccc;
overflow: auto;
grid-area: tabs;
}
#tabpages {
background-color: #888888;
overflow: auto;
grid-area: tabpages;
}
#rightdragbar {
background-color: black;
grid-area: rightdragbar;
cursor: ew-resize;
}
#rightcol {
background-color: #aaaaaa;
overflow: auto;
grid-area: rightcol;
}
#footer {
background-color: lightblue;
overflow: auto;
grid-area: footer;
}
<body onresize="ResetColumnSizes()">
<div id="page" onmouseup="EndDrag()" onmousemove="OnDrag(event)">
<div id="header">
Header
</div>
<div id="leftcol">
Left Col
</div>
<div id="leftdragbar" onmousedown="StartLeftDrag()"></div>
<div id="tabs">
Tabs
</div>
<div id="tabpages">
Tab Pages
</div>
<div id="rightdragbar" onmousedown="StartRightDrag()"></div>
<div id="rightcol">
Rightcol
</div>
<div id="footer">
Footer
</div>
</div>
</body>
https://codepen.io/lukerazor/pen/GVBMZK
I changed, so you can add more Horizontal and Vertical slider.
test1.html:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="test1.css">
<script src= "test1.js" > </script>
</head>
<body>
<div id="page" onmouseup="EndDrag()" onmousemove="OnDrag(event)">
<div id="header">
Header asdlkj flkdfj sdflkksdjf sd;flsdjf sd;flkjsd;fljsd;flsdj;fjsd f;sdlfj;sdlfj
</div>
<div id="leftcol">
Left Col
</div>
<div id="leftdragbar" onmousedown="StartHDrag(1)"></div>
<div id="tabs">
Tabs
</div>
<div id="topdragbar" onmousedown="StartVDrag(2)"></div>
<div id="tabpages">
Tab Pages
</div>
<div id="rightdragbar" onmousedown="StartHDrag(3)"></div>
<div id="rightcol">
Rightcol
</div>
<div id="botdragbar" onmousedown="StartVDrag(4)"></div>
<div id="footer">
Footer
</div>
</div>
<div id= 'status'></div>
</body>
</html>
test1.css
body {
}
#page {
height: 100vh;
background-color: pink;
display: grid;
grid-template-areas:
'header header header header header'
'leftcol leftdragbar tabs tabs tabs'
'leftcol leftdragbar topdragbar topdragbar topdragbar'
'leftcol leftdragbar tabpages rightdragbar rightcol'
'botdragbar botdragbar botdragbar botdragbar botdragbar'
'footer footer footer footer footer';
grid-template-rows: min-content 1fr 6px 9fr 6px 1fr;
grid-template-columns: 2fr 6px 6fr 6px 2fr;
}
/*****************************/
#header {
background-color: lightblue;
overflow: auto;
grid-area: header;
}
#leftcol {
background-color: #aaaaaa;
overflow: auto;
grid-area: leftcol;
}
#leftdragbar {
background-color: black;
grid-area: leftdragbar;
cursor: ew-resize;
}
#topdragbar {
background-color: black;
grid-area: topdragbar;
cursor: ns-resize;
}
#botdragbar {
background-color: black;
grid-area: botdragbar;
cursor: ns-resize;
}
#tabs {
background-color: #cccccc;
overflow: auto;
grid-area: tabs;
}
#tabpages {
background-color: #888888;
overflow: auto;
grid-area: tabpages;
}
#rightdragbar {
background-color: black;
grid-area: rightdragbar;
cursor: ew-resize;
}
#rightcol {
background-color: #aaaaaa;
overflow: auto;
grid-area: rightcol;
}
#footer {
background-color: lightblue;
overflow: auto;
grid-area: footer;
}
test1.js
let isHDragging = false;
let isVDragging = false;
let cols = ['2fr','6px','6fr','6px','2fr']; //grid-template-columns: 2fr 6px 6fr 6px 2fr;
let colns = ['leftcol','','tabpages','','rightcol'];
let Tcols = [];
let rows = ['min-content','1fr','6px','9fr','6px','1fr']; //grid-template-rows: min-content 1fr 6px 9fr 1fr
let rowns = ['header','tabs','','tabpages','','footer'];
let Trows = []
let CLfactor ;
let CRfactor ;
let gWcol = -1;
let gWrow = -1;
function StartHDrag(pWcol) {
isHDragging = true;
SetCursor("ew-resize");
CLfactor = parseFloat(cols[pWcol-1]) / document.getElementById(colns[pWcol-1]).clientWidth;
CRfactor = parseFloat(cols[pWcol+1]) / document.getElementById(colns[pWcol+1]).clientWidth;
Tcols = cols.map(parseFloat);
gWcol = pWcol;
}
function StartVDrag(pRow) {
isVDragging = true;
SetCursor("ns-resize");
CLfactor = parseFloat(rows[pRow-1]) / document.getElementById(rowns[pRow-1]).clientHeight;
CRfactor = parseFloat(rows[pRow+1]) / document.getElementById(rowns[pRow+1]).clientHeight;
Trows = rows.map(parseFloat);
gWrow = pRow;
}
function SetCursor(cursor) {
let page = document.getElementById("page");
page.style.cursor = cursor;
}
function EndDrag() {
isHDragging = false;
isVDragging = false;
SetCursor("auto");
}
function OnDrag(event) {
if(isHDragging) {
Tcols[gWcol-1] += (CLfactor * event.movementX);
Tcols[gWcol+1] -= (CLfactor * event.movementX);
cols[gWcol-1] = Math.max(Tcols[gWcol-1],0.01) + "fr";
cols[gWcol+1] = Math.max(Tcols[gWcol+1],0.01) + "fr";
let newColDefn = cols.join(" ");
page.style.gridTemplateColumns = newColDefn;
} else if (isVDragging) {
Trows[gWrow-1] += (CLfactor * event.movementY);
Trows[gWrow+1] -= (CLfactor * event.movementY);
rows[gWrow-1] = Math.max(Trows[gWrow-1],0.01) + "fr";
rows[gWrow+1] = Math.max(Trows[gWrow+1],0.01) + "fr";
let newRowDefn = rows.join(" ");
page.style.gridTemplateRows = newRowDefn;
document.getElementById("footer").innerHTML = newRowDefn;
}
event.preventDefault()
}
To actually match the question! Making a dragbar to resize divs inside CSS grids.
Here is a possible way, the original OP layout is kept, as well as the CSS, using Grids.
The goal is to capture the original state of the Grid Template Columns, and convert it to floats.
The browser always compute in pixels, and the sum of those columns + the gap, represent the total width of the container element. That sum must always be the same, or the elements will jump!
NB: Calls to .getComputedStyle() are not very efficient, optimisation is likely possible here!
Notice, doing this way using grids and screenX avoid the common jumping bug on mouse down.
Comments are added, this will allow to apply the logic with any number of columns, or rows, good luck.
With the usage of pointer events, it does works from a touch device as well.
let target = document.querySelector("div") // Target container element
let md = false; // Will be true at mouse down
let xorigin; // Click origin X position
let gtcorigin = []; // Origin Grid Template Columns in pixels
const pointerdown = (e) => {
if (e.target.classList[0] === "handler"){ // Filter to target the wanted element
md = true; // Set mouse down
xorigin = e.screenX; // Store the origin X position
// Grid Template Columns, array of pixels as float
gtcorigin = window.getComputedStyle(target)["grid-template-columns"].split(" ").map((a) => +(a.slice(0, -2)));
document.body.style.cursor = "col-resize" // This makes things nice
document.body.style.userSelect = "none" // This makes things nice
}
}
const pointerup = (e) => {
md = false; // Reset bool at mouse up
document.body.style.cursor = "pointer"
document.body.style.userSelect = "unset"
}
const resizer = (e) => {
if (md){ // Mouse is down hover the handler element
let gtc = window.getComputedStyle(target)["grid-template-columns"].split(" ").map((a) => +(a.slice(0, -2))); // Grid Template Columns, array of pixels as float
let xdragdif = xorigin - e.screenX; // Move in pixels since the click
gtc[0] = gtcorigin[0] - xdragdif // First column, if negative, it will grow
gtc[2] = gtcorigin[2] + xdragdif // Third column
gtc = gtc.map((a) => a+"px") // Set back the values in string with "px"
document.querySelector("console").textContent = gtc.join(" ") // !!! This is only for the demo
target.style.gridTemplateColumns = gtc.join(" ") // Apply the new Grid Template Column as inline style.
}
}
// Attach all events on the largest container element. Here the body is used.
document.body.addEventListener("pointerdown", pointerdown, false)
document.body.addEventListener("pointerup", pointerup, false)
document.body.addEventListener("pointermove", resizer, false)
body {
margin: 40px;
overflow-x: hidden
}
.wrapper {
display: grid;
grid-template-columns: 200px 8px 200px;
grid-gap: 10px;
background-color: #fff;
color: #444;
}
.box {
background-color: #444;
color: #fff;
border-radius: 5px;
padding: 20px;
font-size: 150%;
}
.handler{
width: 3px;
height: 100%;
padding: 0px 0;
top: 0;
background: red;
cursor: col-resize
}
<div class="wrapper">
<div class="box">A</div>
<div class="handler"></div>
<div class="box">B</div>
</div>
<console></console>
No limits are applied here, this can be enhanced with CSS only, using min-width and other similar rules, and the float values can be retrieved to create range sliders and more, this way.

Categories