scrollTop or scrollLeft on horizontal website? - javascript

I have a website I am working on (here is a basic example), yesterday I got some help to implement active states on the radio-style button navigation, and I am now trying to link this up so that it also changes on page scroll/when in view as currently it's only onClick.
I roughly know how to achieve this as I've done something similar before, but then it occurred to me that because the page and scrollbar are rotated to accommodate the horizontal effect, I don't know if it would now be scrollTop or scrollLeft. I've never used scrollLeft before so I am unsure how to use it correctly. I am wondering if anyone has implemented something similar before and what the correct way would be? I've tried both and nothing seems to be working. This is what I'm roughly trying to achieve (but only one class active at a time).
I thought maybe using Waypoints could be another option, but again it's hard to find anything online which explains how this works when a site is rotated multiple times.
My JS knowledge is shaky (still learning!), I'm only trying to implement what I think would work so this is probably not even correct, any help understanding what I'm doing wrong would be appreciated!
Heres the latest thing I've tried.
// --- change span classes on click
const setIconState = (icon, state) => icon.className = state
? icon.className.replace('button-off', 'button-on')
: icon.className.replace('button-on', 'button-off')
const toggleIcon = element => {
const className = element.className;
element.className = className.indexOf('button-on') > -1
? setIconState(element, false)
: setIconState(element, true);
}
const setIconActiveState = (icon, state) => icon.className = state
? icon.className = `${icon.className} active`
: icon.className = icon.className.replace('active', '')
document.querySelectorAll('.bottomnav span.icon')
.forEach(icon => {
icon.onclick = (e) => {
const {
target: clickedSpan
} = e;
const siblings = [...clickedSpan.parentElement.parentElement.querySelectorAll('span.icon')]
.filter(sibling => sibling != clickedSpan);
siblings.forEach(icon => {
setIconState(icon, false);
setIconActiveState(icon, false);
});
setIconState(clickedSpan, true);
setIconActiveState(clickedSpan, true);
};
});
// --- change span classes on scroll test
function onScroll(event){
var scrollPos = $(document).scrollTop();
$('.bottomnav a').each(function () {
var currLink = $(this);
var refElement = $(currLink.attr("href"));
if (refElement.position().top <= scrollPos && refElement.position().top + refElement.height() > scrollPos) {
$('.bottomnav a span').removeClass("active");
currLink.addClass("active");
}
else{
currLink.removeClass("active");
}
});
}
* {
margin: 0;
padding: 0;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
html,
body {
color: #000;
font-family: 'IBM Plex Sans', sans-serif;
font-weight: 100;
font-size: 7px;
text-rendering: optimizeLegibility;
overflow-x: hidden;
scroll-behavior: smooth;
}
.bottomnav {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
overflow: hidden;
position: fixed;
bottom: 0px;
width: 100%;
z-index: 2;
}
.bottomnav span {
float: left;
display: block;
color: #888;
text-align: center;
padding: 14px 16px;
text-decoration: none;
font-size: 26px;
}
.bottomnav span:hover {
color: #fac123;
}
.bottomnav span.active {
color: #fac123;
}
#container {
overflow-y: scroll;
overflow-x: hidden;
transform: rotate(270deg) translateX(-100vh);
transform-origin: top left;
position: absolute;
width: 100vh;
height: 100vw;
white-space: nowrap;
scroll-snap-type: y mandatory;
}
#container .card {
width: 100vw;
height: 100vh;
display: inline-flex;
position: relative;
scroll-snap-align: start;
}
#player {
transform: rotate(90deg) translateY(-100vh);
transform-origin: top left;
font-size: 0;
width: 100vh;
height: 100vh;
display: flex;
/* position: absolute;*/
}
#player section > object {
width: 100vw;
overflow-x: hidden;
}
section object > div {
white-space: normal;
}
.container::-webkit-scrollbar {
display: none;
}
section {
padding: 5%;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
position: relative;
transition: .5s ease;
}
.cardwhite {
color: white;
background-color: black;
}
.cardblack {
color: black;
background-color: white;
}
h2 {
font-size: 40px;
font-weight: 700;
font-family: 'IBM Plex Serif', sans-serif;
}
p {
font-size: 10px;
margin-bottom: 15px;
font-weight: 100;
font-family: 'IBM Plex Sans', sans-serif;
}
<link href="https://unpkg.com/ionicons#4.5.5/dist/css/ionicons.min.css" rel="stylesheet">
<div class="bottomnav" id="bottomnav">
<span class="icon ion-ios-radio-button-on active"></span>
<span class="icon ion-ios-radio-button-off"></span>
<span class="icon ion-ios-radio-button-off"></span>
</div>
<div class="container" id="container">
<div id="player">
<section class="card cardwhite" id="1">
<object>
<h2>Section 1</h2>
<p>Description</p>
</object>
</section>
<section class="card cardblack" id="2">
<object>
<h2>Section 2</h2>
<p>Description</p>
</object>
</section>
<section class="card cardwhite" id="3">
<object>
<h2>Section 3</h2>
<p>Description</p>
</object>
</section>
</div>
</div>

For horizontal scrolling I would go the following route, simplifying your HTML and whaty you listen for. Since touch devices can easily just swipe to scroll, all you need to do is make it accessible for people with scroll wheels. You could also add an animation, but it makes this snippet too long.
const main = document.querySelector( 'main' );
const nav = document.querySelector( 'nav' );
let scrollend;
function onwheel(){
/* When using the scrollwheel, translate Y direction scrolls to X direction. This way scrollwheel users get the benefit of scrolling down to go right, while touch and other users get default behaviour. */
event.preventDefault();
event.stopImmediatePropagation();
main.scrollLeft += event.wheelDeltaY;
}
function onscroll(){
/* When scrolling, find the nearest element to the center of the screen. Then find the link in the nav that links to it and activate it while deactivating all others. */
const current = Array.from( main.children ).find(child => {
return child.offsetLeft >= main.scrollLeft - innerWidth / 2;
});
const link = Array.from( nav.children ).reduce((find, child) => {
child.classList.remove( 'selected' );
return find || (child.href.indexOf( current.id ) >= 0 ? child : find);
}, false);
if( link ) link.classList.add( 'selected' );
clearTimeout( scrollend );
scrollend = setTimeout( onscrollend, 100 );
}
function onscrollend(){
/* After scrolling ends, snap the appropriate element. This could be done with an animation. */
clearTimeout( scrollend );
const current = Array.from( main.children ).find(child => {
return child.offsetLeft >= main.scrollLeft - innerWidth / 2;
});
main.scrollLeft = current.offsetLeft;
}
/* Bind and initial call */
main.addEventListener( 'wheel', onwheel );
main.addEventListener( 'scroll', onscroll );
onscroll();
html,
body,
main {
height: 100%;
}
body {
padding: 0;
margin: 0;
}
main {
display: flex;
overflow: auto;
width: 100%;
height: 100%;
scroll-snap-type: x proximity;
}
main section {
width: 100%;
height: 100%;
flex: 0 0 100%;
}
nav {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
nav a {
width: 1em;
height: 1em;
margin: 1em;
display: block;
overflow: hidden;
color: transparent;
border: 1px solid black;
border-radius: 50%;
}
nav a.selected {
background: black;
}
.bland { background: gray; }
.dark { background: darkgray; color: white; }
.bright { background: yellow; }
<nav>
Section 1
Section 2
Section 3
</nav>
<main>
<section class="bright" id="section-1">
<h2>Section 1</h2>
</section>
<section class="dark" id="section-2">
<h2>Section 2</h2>
</section>
<section class="bland" id="section-3">
<h2>Section 3</h2>
</section>
</main>

As mentioned, I would also prefer a design that does not flip the X and Y axis.
Doing so might bite us in the future, when we try to include non-trivial content on our pages.
Also if we don't do that axis flip, we have no need at all to do positional calculations.
So both the HTML structure and the CSS will be simpler.
AFAIK, it's not possible to do the scrolling purely in non-hacky CSS.
/**
* Change icon state on click.
*/
const icons = Array.from( document.querySelectorAll( '.icon' ));
const toggleIcon = icon => {
icon.classList.toggle( 'ion-ios-radio-button-on' );
icon.classList.toggle( 'ion-ios-radio-button-off' );
};
const clickIcon = event => {
// toggle previous active state
toggleIcon( document.querySelector( 'i.ion-ios-radio-button-on' ));
// toggle own state
toggleIcon( event.target );
};
icons.forEach( icon => icon.addEventListener( 'click', clickIcon ));
/**
* Scroll horizontally on scroll wheel.
* The combination of "scroll-behavior: smooth;" and the "<a href=#>" anchor links,
* can be reused to do and endless snapping cycle on wheel event.
*/
let scroll_state = 0;
window.addEventListener( 'wheel', event => {
window.requestAnimationFrame(() => {
// cast to -1 or +1
const offset = event.deltaY / Math.abs( event.deltaY );
scroll_state += offset;
// Arrays are zero-based.
// So if the length matches our state, restart over from the first page.
if ( scroll_state === icons.length ) scroll_state = 0;
else if ( scroll_state < 0 ) scroll_state = icons.length - 1;
// scrolll_state will now always contain the next icon to click.
icons[ scroll_state ].click();
});
});
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
overflow-x: scroll;
width: 100%;
}
main {
display: block;
height: 90vh;
width: 300vw;
}
nav {
background-color: orange;
display: block;
height: 10vh;
position: fixed;
width: 100%;
}
a {
text-decoration: none;
}
.page {
display: inline-block;
float: left;
height: 100%;
padding: 50px;
width: 100vw;
}
<link href="https://unpkg.com/ionicons#4.5.5/dist/css/ionicons.min.css" rel="stylesheet">
<body>
<main>
<section class="page" id="myapp_first">
<h1>First</h1>
<p>Lorem Ipsum</p>
</section>
<section class="page" id="myapp_second">
<h1>Second</h1>
<p>Lorem Ipsum</p>
</section>
<section class="page" id="myapp_third">
<h1>Third</h1>
<p>Lorem Ipsum</p>
</section>
</main>
<nav id="myapp_navigation">
<a href="#myapp_first">
<i class="icon ion-ios-radio-button-on active"></i>
</a>
<a href="#myapp_second">
<i class="icon ion-ios-radio-button-off"></i>
</a>
<a href="#myapp_third">
<i class="icon ion-ios-radio-button-off"></i>
</a>
</nav>
</body>
By leveraging the click event of the icons, we get the icons changing class and the transition for free. Adding more pages now just becomes adding the correct HTML and updating the width of the <main> element.
A last thing I would personally add, is a debounce function around the wheel event, so we don't try to scroll faster than we can render.
Without debouncing, we might want to merge the functions so we can include the class changing inside the animationFrame for hopefully less yanky visuals, but that would complicate the click events again, so i'd prefer debouncing the wheel handler.
/**
* Change icon state on click.
*/
const icons = Array.from( document.querySelectorAll( '.icon' ));
const toggleIcon = icon => {
icon.classList.toggle( 'ion-ios-radio-button-on' );
icon.classList.toggle( 'ion-ios-radio-button-off' );
icon.classList.toggle( 'active' );
};
const clickIcon = event => {
// toggle previous active state
toggleIcon( document.querySelector( '.ion-ios-radio-button-on' ));// toggle own state
toggleIcon( event.target );
};
icons.forEach( icon => icon.addEventListener( 'click', clickIcon ));
/**
* Scroll horizontally on scroll wheel.
* The combination of "scroll-behavior: smooth;" and the "<a href=#>" anchor links,
* can be reused to do and endless snapping cycle on wheel event.
*/
let scroll_state = 0;
window.addEventListener( 'wheel', event => {
// ANimation frame to smooth out the transition.
window.requestAnimationFrame(() => {
// cast to -1 or +1
const offset = event.deltaY / Math.abs( event.deltaY );
scroll_state += offset;
// Arrays are zero-based.
// So if the length matches our state, restart over from the first page.
if ( scroll_state === icons.length ) scroll_state = 0;
else if ( scroll_state < 0 ) scroll_state = icons.length - 1;
// scrolll_state will now always contain the next icon to click.
icons[ scroll_state ].click();
});
});
* {
margin: 0;
padding: 0;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
html,
body {
color: #000;
font-family: 'IBM Plex Sans', sans-serif;
font-weight: 100; /* EDIT: font-weight: 100 basically equals no font weight at all */
font-size: 7px; /* EDIT: Why so small ? */
text-rendering: optimizeLegibility;
overflow-x: scroll;
overflow-y: hidden;
scroll-behavior: smooth;
height: 100vh; /* EDIT: add height, so we can scale off this */
width: 100vw; /* EDIT: add width, so we can scale off this */
}
.bottomnav {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
/*
overflow: hidden;
*/
position: fixed;
/*bottom: 0px; EDIT: not needed after we place the nav at the bottom */
height: 15vh; /* EDIT: .bottomnav height + #container height = 100vh */
width: 100%;
z-index: 2;
background-color: black;
}
.bottomnav span {
/*float: left; /* why float when flex lets us position exactly? */
display: block;
color: #888;
text-align: center;
padding: 14px 16px;
text-decoration: none;
font-size: 26px;
}
.bottomnav span:hover {
color: #fac123;
}
.bottomnav span.active {
color: #fac123;
}
#container {
/*
overflow-y: scroll;
overflow-x: hidden;
transform: rotate(270deg) translateX(-100vh);
transform-origin: top left;
position: absolute;
*/
width: 300vw; /* EDIT: 300vw, 100 per page of 100vw */
height: 85vh; /* EDIT: .bottomnav height + #container height = 100vh */
/*scroll-snap-type: y mandatory; EDIT: only needed if we use snappoints */
}
/* EDIT: .card and section are the same elements, merged rule "container" here */
.card {
width: 100vw; /* EDIT: 100vw for each page of 100vw width */
height: 100%; /* EDIT: 100% so it scales with the container, not the screen */
display: inline-block; /* EDIT: block level, since we do not need to flex these */
float: left; /* EDIT: float left so our pages leave no space between them so 300vw = 100+100+100 . THis can be done with flexbox or grid as well, but is more complicated than needed */
/*position: relative; EDIT: not needed */
/* scroll-snap-align: start; EDIT: only needed if we use snappoints */
padding: 50px;
/* EDIT:
justify-content: center;
align-items: center;
flex-wrap: wrap;
position: relative;
*/
/* transition: .5s ease; EDIT: I would think that "scroll-behavior: smooth;" already does this */
}
/* EDIT: Since there's no use for the extra wrapper element, positioning it absolute + flex only harms us instead of helping
#player {
transform: rotate(90deg) translateY(-100vh);
transform-origin: top left;
font-size: 0;
width: 100vh;
height: 100vh;
display: flex;
position: absolute;
}
#player section > object {
width: 100vw;
overflow-x: hidden;
}
*/
/* EDIT: I don't see any <div>s inside the objects
section object > div {
white-space: normal;
}
*/
/* EDIT: ? Attempt to remove vertical scroll? Not needed
.container::-webkit-scrollbar {
display: none;
}
*/
.cardwhite {
color: white;
background-color: black;
}
.cardblack {
color: black;
background-color: white;
}
h2 {
font-size: 40px;
font-weight: 700;
font-family: 'IBM Plex Serif', sans-serif;
}
p {
font-size: 10px;
margin-bottom: 15px;
font-weight: 100;
font-family: 'IBM Plex Sans', sans-serif;
}
<link href="https://unpkg.com/ionicons#4.5.5/dist/css/ionicons.min.css" rel="stylesheet">
<div id="container">
<!-- the extra player <div> is useless since the cards fully overlap it.
so it can be removed -->
<section class="card cardwhite" id="1">
<object>
<h2>Section 1</h2>
<p>Description</p>
</object>
</section>
<section class="card cardblack" id="2">
<object>
<h2>Section 2</h2>
<p>Description</p>
</object>
</section>
<section class="card cardwhite" id="3">
<object>
<h2>Section 3</h2>
<p>Description</p>
</object>
</section>
</div>
<!-- EDIT: Put the nav at the bottom so we do not have position issues -->
<div class="bottomnav" id="bottomnav">
<a href="#1">
<span class="icon ion-ios-radio-button-on active"></span>
</a>
<a href="#2">
<span class="icon ion-ios-radio-button-off"></span>
</a>
<a href="#3">
<span class="icon ion-ios-radio-button-off"></span>
</a>
</div>

Related

Convert vertical scrolling Website to a functional horizontal scrolling website

The current code is a responsive vertical webpage with a vertical navigation
I want to convert It to a responsive horizontal webpage with left-right arrow control.
Instances
When scrolling up or down with the mouse, the website should go left when scrolling up and right when scrolling down.
When right or left arrow keys are pressed the website should scroll right or left depending on the key pressed.
My HTML code
<body>
<div class="section" id="home" data-label="Home">Home</div>
<div class="section" id="about" data-label="About Me">About</div>
<div class="section" id="contact" data-label="Say Hi">Contact</div>
<script>
function activateNavigation() {
const sections = document.querySelectorAll(".section");
const navContainer = document.createElement("nav");
const navItems = Array.from(sections).map((section) => {
return `
<div class="nav-item" data-for-section="${section.id}">
<span class="nav-label">${section.dataset.label}</span>
</div>
`;
});
navContainer.classList.add("nav");
navContainer.innerHTML = navItems.join("");
const observer = new IntersectionObserver(
(entries) => {
document.querySelectorAll(".nav-link").forEach((navLink) => {
navLink.classList.remove("nav-link-selected");
});
const visibleSection = entries.filter((entry) => entry.isIntersecting)[0];
document
.querySelector(
`.nav-item[data-for-section="${visibleSection.target.id}"] .nav-link`
)
.classList.add("nav-link-selected");
},
{ threshold: 0.5 }
);
sections.forEach((section) => observer.observe(section));
document.body.appendChild(navContainer);
}
activateNavigation();
</script>
</body>
My CSS
.section{
height: 100vh;
.nav{
--nav-gap : 15px;
padding: var(--nav-gap);
position: fixed;
right: 0;
top:50%;
transform: translateY(-50%);
}
.nav-item{
align-items: center;
display: flex;
flex-direction: row-reverse;
margin-bottom: var(--nav-gap);
}
.nav-link:hover ~ .nav-label{
opacity: 1;
}
.nav-label{
color: black;
font-weight: bold;
opacity: 0;
transition: opacity 0.1s;
}
.nav-link{
background: rgba(0,0,0,0.5);
border-radius: 50%;
height: var(--nav-gap);
margin-left: var(--nav-gap);
width: var(--nav-gap);
transition: transform 0.3s;
}
.nav-link-selected{
background: #000000;
transform: scale(1.4);
}
The idea is to make a container div which will contain all the sections, and then make it display flex to handle sections as rows not columns.
Make sure to that the width and height of the container match the body's.
and then disable overflow-x on the container.
Now all we have to do is to get the current scroll Y position and transform it into a X one.
Since the browser does the job for us, we don't really need to calculate anything... just get paste the same Y position as an X one and it will work like a charm.
There is the script and explanations... All you have to do next is to challenge yourself to get the proper height of the website and use it as min-height for the body
A quick hint on your challenge: The height you'll need could be equal to the width of your container.
Good luck!
Example with your layout: jsfiddle link
const container = document.querySelector(".container")
window.addEventListener("scroll", horizontalScroll)
window.addEventListener("keydown", horizontalScroll)
function horizontalScroll(e, keyboadScrollingSpeed=30) {
let y = window.scrollY || window.pageYOffset
if( e.type == "keydown" ) {
if( (e.key == 'ArrowLeft' || e.key == 'ArrowRight') ) {
const direction = e.key == 'ArrowLeft' ? -1 : 1
y += keyboadScrollingSpeed * direction
window.scrollTo({
top: y
})
}
else e.preventDefault()
}
container.scrollTo({
left: y,
})
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
min-height: 300vh;
}
.container {
background-color: yellow;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
position: sticky;
top: 0;
left: 0;
width: 100vw;
overflow-x: hidden;
}
.section {
display: block;
flex: 1;
max-width: 100vw;
min-width: 100vw;
height: 100vh;
background-color: green;
}
<div class="container">
<div class="section">
<div class="content">
<h1>Section 1</h1>
<p>This is some text</p>
</div>
</div>
<div class="section">
<div class="content">
<h1>Section 2</h1>
<p>This is some text</p>
</div>
</div>
</div>

Horizontal scroll areas with buttons and gradients

This is my code so far:
// Show and hide gradients
$(document).ready(function() {
$(".scroll-area").each(function(index) {
if ($(this)[0].scrollWidth <= $(this)[0].clientWidth) {
$(this).closest(".container").find(".left").css("display", "none");
$(this).closest(".container").find(".right").css("display", "none");
} else {
$(this).scroll(function() {
if ($(this)[0].scrollWidth > $(this)[0].clientWidth) {
if ($(this).scrollLeft() > 0) {
$(this).closest(".container").find(".left").css("display", "block");
}
if ($(this).scrollLeft() == 0) {
$(this).closest(".container").find(".left").css("display", "none");
}
var fullWidth = $(this)[0].scrollWidth - $(this)[0].offsetWidth - 1;
if ($(this).scrollLeft() >= fullWidth) {
$(this).closest(".container").find(".right").css("display", "none");
}
if ($(this).scrollLeft() < fullWidth) {
$(this).closest(".container").find(".right").css("display", "block");
}
}
});
}
});
});
// Scroll buttons
let interval;
$('.scroll-btn').on('mousedown', ({
target
}) => {
const type = $(target).attr('id');
interval = setInterval(() => {
var x = $('#a').scrollLeft();
$('#a').scrollLeft(type === 'left-arrow' ? x - 10 : x + 10);
}, 50);
});
$('.scroll-btn').on('mouseup', () => clearInterval(interval));
* {
margin: 0;
padding: 0;
font-family: sans-serif;
font-size: 16px;
}
.container {
width: 550px;
height: 80px;
background-color: grey;
position: relative;
margin-bottom: 20px;
}
.scroll-area {
white-space: nowrap;
overflow-x: auto;
height: 100%;
}
.left,
.right {
width: 50px;
height: 100%;
position: absolute;
pointer-events: none;
top: 0;
}
.left {
background: linear-gradient(90deg, orange 0%, rgba(0, 0, 0, 0) 100%);
left: 0;
display: none;
}
.right {
background: linear-gradient(-90deg, orange 0%, rgba(0, 0, 0, 0) 100%);
right: 0;
}
.arrow {
display: block;
position: absolute;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 15px;
cursor: pointer;
}
.left-arrow {
left: 0;
}
.right-arrow {
right: 0;
}
.left-arrow div,
.right-arrow div {
font-size: 40px;
}
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<div class="container">
<div id="x" class="left"></div>
<div class="right"></div>
<div class="arrow left-arrow">
<div class="scroll-btn" id="left-arrow">
<</div>
</div>
<div class="arrow right-arrow">
<div class="scroll-btn" id="right-arrow">></div>
</div>
<div id="a" class="scroll-area">
<div class="text">Scroll to right. The gradients and arrows should appear and disappear based on the scroll position. It should work with more than one container. Lorem ipsum.</div>
</div>
</div>
The needs are:
The arrows should appear and disappear in the same way like the gradients.
If there is not enough text to cause a scrollable area, there should be no gradient and now arrow.
There should be more than one container in the end.
Can somebody help me to do that? I would be super thankful!
You can put your arrows inside the left/right gradient divs. That way they will show/hide same way as the gradients.
EDIT
I cleaned up the code a bit since the original answer was kinda messy. (or 'weird' as mstephen19 put it :)).
// Show gradient and left/right arrows only if scrollable
$(".scroll-area").each((i, el) => {
$(el).parent().find(".right")[el.scrollWidth > el.clientWidth ? "show" : "hide"]();
});
// Show/hide gradient and arrows on scroll
$('.scroll-area').scroll((e) => {
const fullWidth = $(e.target)[0].scrollWidth - $(e.target)[0].offsetWidth - 1;
const left = $(e.target).scrollLeft()
$(e.target).parent().find(".left, .left-arrow")[left > 0 ? "show" : "hide"]();
$(e.target).parent().find(".right, .right-arrow")[left < fullWidth ? "show" : "hide"]();
});
// Scroll on left/right arrow mouse down
let intervalId;
$(".left-arrow, .right-arrow").on("mousedown", (e) => {
const scroll = $(e.target).closest(".container").find(".scroll-area");
intervalId = setInterval(() => {
const left = scroll.scrollLeft();
scroll.scrollLeft(e.target.classList.contains("left-arrow") ? left - 10 : left + 10);
}, 50);
}).on("mouseup mouseleave", () => {
clearInterval(intervalId);
});
* {
margin: 0;
padding: 0;
font-family: sans-serif;
font-size: 16px;
}
.container {
width: 550px;
height: 80px;
background-color: grey;
position: relative;
margin-bottom: 20px;
margin-left: 20px;
}
.scroll-area {
white-space: nowrap;
overflow-x: auto;
height: 100%;
}
.left,
.right {
width: 50px;
height: 100%;
position: absolute;
top: 0;
}
.left {
background: linear-gradient(90deg, orange 0%, rgba(0, 0, 0, 0) 100%);
left: 0;
display: none;
}
.right {
background: linear-gradient(-90deg, orange 0%, rgba(0, 0, 0, 0) 100%);
right: 0;
text-align: right;
}
.left-arrow,
.right-arrow {
margin: 0 10px;
position: absolute;
top: 50%;
-ms-transform: translateY(-50%);
transform: translateY(-50%);
cursor: pointer;
user-select: none;
font-size: 40px
}
.left-arrow {
display: none;
left: -25px;
}
.right-arrow {
right: -25px;
}
<html>
<head>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<div class="container">
<div class="left"></div>
<div class="right"></div>
<div class="left-arrow"><</div>
<div class="right-arrow">></div>
<div class="scroll-area">
<div class="text">Scroll to right. The gradients and arrows should appear and disappear based on the scroll position. It should work with more than one container. Lorem ipsum.</div>
</div>
</div>
<div class="container">
<div class="left"><span class="left-arrow"><</span></div>
<div class="right"><span class="right-arrow">></span></div>
<div class="scroll-area">
<div class="text">No scroll.</div>
</div>
</div>
</body>
</html>
Some things about your code:
Your original code would not work with multiple containers, because you had a hardcoded #a ID in the interval code. You should really only have IDs on one element ideally, anyways (they're unique identifiers, while classes can be placed on multiple elements). The .scroll-area element should be found based on the target clicked.
You should combine your gradient and arrow elements into one element. By that, I mean making the div in which the arrow lives should be a child of the gradient div. Why manage them both separately?
Use class adding/removing/toggling instead of directly setting the CSS. Remember - when you find yourself writing the same code multiple times, it usually means there is a way to condense it down and make your code more dry and easier to understand + read.
Don't use the literal < and > symbols, as it can confuse some browsers. Use < and > instead.
Rather than toggling display to none and block, it's better to use visibility in this specific case. In my example, we use opacity for a fun fading effect.
Don't forget to listen for both mouseup mouseout events :)
Here is the working solution. I've refactored the code a bit:
let interval;
$('.arrow').on('mousedown', ({ target }) => {
const type = target.classList[1];
const scrollArea = $(target).parent().find('.scroll-area');
interval = setInterval(() => {
const prev = scrollArea.scrollLeft();
scrollArea.scrollLeft(type === 'left-arrow' ? prev - 10 : prev + 10);
}, 50);
});
$('.arrow').on('mouseup mouseout', () => clearInterval(interval));
$('.scroll-area').on('scroll', ({ target }) => {
const left = $(target).parent().find('.left-arrow');
const right = $(target).parent().find('.right-arrow');
const scroll = $(target).scrollLeft();
const fullWidth = $(target)[0].scrollWidth - $(target)[0].offsetWidth;
if (scroll === 0) left.addClass('hide');
else left.removeClass('hide');
if (scroll > fullWidth) right.addClass('hide');
else right.removeClass('hide');
});
.container {
width: 550px;
height: 80px;
background: grey;
position: relative;
}
.right-arrow,
.left-arrow {
height: 100%;
width: 50px;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
font-size: 2rem;
cursor: pointer;
transition: all 0.2s linear;
}
.scroll-area {
white-space: nowrap;
overflow-x: scroll;
height: 100%;
}
.right-arrow {
background: linear-gradient(-90deg, orange 0%, rgba(0, 0, 0, 0) 100%);
left: 500px;
}
.left-arrow {
background: linear-gradient(90deg, orange 0%, rgba(0, 0, 0, 0) 100%);
left: 0px;
}
.scroll-btn {
pointer-events: none;
}
.hide {
opacity: 0;
}
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<div class="container">
<div class="arrow left-arrow">
<div class="scroll-btn" id="left-arrow"><</div>
</div>
<div class="arrow right-arrow">
<div class="scroll-btn" id="right-arrow">></div>
</div>
<div class="scroll-area">
<div class="text">
Scroll to right. The gradients and arrows should appear and disappear based on the scroll position. It should work with more than one
container. Lorem ipsum.
</div>
</div>
</div>
PS: If you don't like the fade effect, remove the transition: all 0.2s linear; part of the CSS, and switch .hide's opacity: 0 to visibility: hidden.

position sticky using javascript makes div goes outside the parent

I have a menu on the left that I want to be always sticky, I'm using javascript for that for IE11 support.
The problem I'm having is that the right div goes to the left when it's sticky and doesn't keep it's position, the second issue is that the .content div width grows when the right div is sticky.
For the javascript part, I don't know how to make the right div to stop when it reaches the footer.
EDIT:
I managed to solve the second issue, the code is updated, I also tried to add a right value for the right div so it sticks in its initial vertical position, but that's not working because it changes when the screen gets resized.
How can I solve this?
Edit 2:
For the javascript issue I found this post which helped me resolve my issue:
Make sticky/fixed element stop at footer
var sticky = document.getElementsByClassName("sticky-element")[0];
var stickyAnchor = sticky.parentNode;
var state = false;
function getAnchorOffset() {
return stickyAnchor.getBoundingClientRect().top;
}
updateSticky = function (e) {
if (!state && (getAnchorOffset() < 0)) {
sticky.classList.add("is-sticky");
sticky.parentElement.classList.add("has-sticky");
state = true;
} else if (state && (getAnchorOffset() >=0 )) {
sticky.classList.remove("is-sticky");
sticky.parentElement.classList.remove("has-sticky");
state = false;
}
}
window.addEventListener('scroll', updateSticky);
window.addEventListener('resize', updateSticky);
updateSticky();
.main-wrapper {
margin: 48px 48px 0 48px;
max-width: 1366px;
}
.wrapper {
width: 100%;
display: flex;
justify-content: space-between;
position: relative;
}
.wrapper.has-sticky .content{
margin-right: calc(199px + 72px);
}
.content {
flex: 0 1 1040px;
width: calc(1040px - 72px);
min-width: 1%;
margin-right: 72px;
height: 1200px;
background-color: #e6e9f0;
}
.nav-menu {
position: static;
flex: 0 1 199px;
width: 199px;
min-width: 199px;
color: white;
height: 300px;
background-color: #04246a;
right: 10%;
}
footer {
background-color: yellow;
height: 300px;
margin-top: 50px;
}
.is-sticky {
top: 0;
position: fixed;
}
<div class="main-wrapper">
<div class="wrapper">
<div class="content">
Main content
</div>
<div class="nav-menu sticky-element">
<nav>
Side content
</nav>
</div>
</div>
<footer>
Footer content
</footer>
</div>
Are you looking for this?
The problem on your code is that whenever you set the position of your right div to fixed it then looks for its relative parent and jumps to the upper left position inside the parent. In your case, the parent div was the .wrapper, that's why it keeps on jumping to the left side and overlaps your main content div.
I added a parent container for the .nav-menu so it will still be in the same position when scrolling. With this, your .nav-menu element won't be using the .wrapper as its main parent. This will create a smooth scroll without noticing any change in position.
Happy coding!
var sticky = document.getElementsByClassName('sticky-element')[0];
var stickyAnchor = sticky.parentNode;
var state = false;
function getAnchorOffset() {
return stickyAnchor.getBoundingClientRect().top;
}
updateSticky = function (e) {
if (!state && getAnchorOffset() < 0) {
sticky.classList.add('is-sticky');
state = true;
} else if (state && getAnchorOffset() >= 0) {
sticky.classList.remove('is-sticky');
state = false;
}
};
window.addEventListener('scroll', updateSticky);
window.addEventListener('resize', updateSticky);
updateSticky();
.main-wrapper {
margin: 48px 48px 0 48px;
max-width: 80%;
}
.wrapper {
width: 100%;
display: flex;
justify-content: space-between;
position: relative;
}
.content {
flex: 0 1 80%;
width: calc(80% - 24px);
min-width: 1%;
margin-right: 24px;
height: 1200px;
background-color: #e6e9f0;
}
.nav-container {
flex-grow: 1;
width: 20%;
min-width: 200px;
position: relative;
display: block;
}
.nav-menu {
color: white;
width: 100%;
min-width: inherit;
height: 300px;
background-color: #04246a;
}
.is-sticky {
top: 0;
position: fixed;
width: calc(20% - 97px);
}
<div class="main-wrapper">
<div class="wrapper">
<div class="content">Main content</div>
<div class="nav-container">
<div class="nav-menu sticky-element">
<nav>Side content</nav>
</div>
</div>
</div>
</div>
var sticky = document.getElementsByClassName("sticky-element")[0];
var stickyAnchor = sticky.parentNode;
var state = false;
function getAnchorOffset() {
return stickyAnchor.getBoundingClientRect().top;
}
updateSticky = function (e) {
if (!state && (getAnchorOffset() < 0)) {
sticky.classList.add("is-sticky");
state = true;
} else if (state && (getAnchorOffset() >=0 )) {
sticky.classList.remove("is-sticky");
state = false;
}
}
window.addEventListener('scroll', updateSticky);
window.addEventListener('resize', updateSticky);
updateSticky();
.main-wrapper {
margin: 48px 48px 0 48px;
max-width: 80%;
}
.wrapper {
width: 100%;
display: flex;
justify-content: space-between;
position: relative;
}
.content {
flex: 0 1 80%;
width: calc(80% - 24px);
min-width: 1%;
margin-right: 24px;
height: 1200px;
background-color: #e6e9f0;
}
.nav-menu {
position: static;
flex: 0 1 20%;
width: 20%;
min-width: 20%;
color: white;
height: 300px;
background-color: #04246a;
}
.is-sticky {
top: 0;
right:5%;
position: fixed;
}
<div class="main-wrapper">
<div class="wrapper">
<div class="content">
Main content
</div>
<div class="nav-menu sticky-element">
<nav>
Side content
</nav>
</div>
</div>
</div>

Scroll Points / overflow-y breaking javascript function with window.pageYOffset

I have a page where I was using JS - specifically window.pageYOffset - and HTML data to change the inner HTML of the h1 footer, use l1 links to scroll the page to each section, and to add classes to each li when I reached the top of each section.work-page.
However, after I implemented CSS scroll points and added the div.container over the scrollable sections my javascript stopped working. Specifically when I set the overflow-y: scroll.
Basically when I made the div.container overflow-y: scroll; the doWork function stopped working and I can't figure out why.
^^^^ div.container in CSS
const doWork = function () {
const p01Tag = document.getElementById("p01")
const p02Tag = document.getElementById("p02")
const p03Tag = document.getElementById("p03")
const p04Tag = document.getElementById("p04")
const container = document.querySelector("div.container")
const sections = document.querySelectorAll("section.work-page")
const clientTag = document.querySelector("h2.about")
document.addEventListener("scroll", function () {
const pixels = window.pageYOffset
console.log(pixels)
sections.forEach(section => {
if(section.offsetTop - 400 <= pixels) {
clientTag.innerHTML = section.getAttribute("data-client")
if (section.hasAttribute("data-seen-1")) {
p01Tag.classList.add("move")
} else {
p01Tag.classList.remove("move")
}
if (section.hasAttribute("data-seen-2")) {
p02Tag.classList.add("move")
} else {
p02Tag.classList.remove("move")
}
if (section.hasAttribute("data-seen-3")) {
p03Tag.classList.add("move")
} else {
p03Tag.classList.remove("move")
}
if (section.hasAttribute("data-seen-4")) {
p04Tag.classList.add("move")
} else {
p04Tag.classList.remove("move")
}
}
})
})
// scrolling between projects ============================
function smoothScroll(target, duration) {
const targetTag = document.querySelector(target);
let targetPosition = targetTag.getBoundingClientRect().top;
const startPosition = window.pageYOffset;
let startTime = null;
function animation(currentTime) {
if(startTime === null ) startTime = currentTime;
const timeElapsed = currentTime - startTime;
const run = ease(timeElapsed, startPosition, targetPosition, duration);
window.scrollTo(0,run);
if (timeElapsed < duration) requestAnimationFrame(animation)
}
function ease(t, b, c, d) {
t /= d / 2;
if (t < 1) return c / 2 * t * t + b;
t--;
return -c / 2 * (t * (t - 2) - 1) + b;
}
requestAnimationFrame(animation)
}
p01Tag.addEventListener("click", function() {
smoothScroll('section.fn-up', 800)
})
p02Tag.addEventListener("click", function() {
smoothScroll('section.cameron', 800)
})
p03Tag.addEventListener("click", function() {
smoothScroll('section.truax', 800)
})
p04Tag.addEventListener("click", function() {
smoothScroll('section.romero', 800)
})
}
doWork()
const doInfo = function () {
const toggleTag = document.querySelector("a.contact")
const sectionTag = document.querySelector("section.info-page")
toggleTag.addEventListener("click", function () {
sectionTag.classList.toggle("open")
if (sectionTag.classList.contains("open")) {
toggleTag.innerHTML = "Close"
} else {
toggleTag.innerHTML = "Info"
}
})
}
doInfo()
html {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
*, *:before, *:after {
-webkit-box-sizing: inherit;
-moz-box-sizing: inherit;
box-sizing: inherit;
}
body {
font-family: 'IBM Plex Mono', monospace;
font-size: 14px;
background-color: #050505;
color: #ffffff;
line-height: 1.1;
}
header {
width: 100%;
z-index: 3;
position: fixed;
top: 0;
left: 0;
padding-top: 40px;
padding-left: 40px;
padding-right: 40px;
}
.contact {
float: right;
}
ul {
font-family: 'IBM Plex Mono', Arial;
font-size: 14px;
}
p {
margin-bottom: 50px;
}
/* Info page -------------------- */
section.info-page {
z-index: 2;
position: fixed;
top: -100vh;
left: 0;
display: flex;
margin-top: 100px;
margin-left: 40px;
margin-right: 40px;
width: 100vw;
min-height: 100vh;
max-width: 100vw;
transition: top 0.5s;
}
section.info-page.open {
top: 0;
}
/* Work page ------------------------*/
div.container {
top: 0;
left: 0;
max-width: 100vw;
max-height: 100vh;
/* WHEN WE ADD THIS OVERFLOW SETTING IN ORDER TO GET THE CSS SCROLL SNAP POINTS TO WORK IT BREAKS THE JAVASCRIPT */
/* overflow-y: scroll; */
scroll-snap-type: y mandatory;
position: relative;
z-index: 1;
}
div.work-info {
width: 13vw;
top: 0;
left: 0;
height: 100vh;
position: fixed;
z-index: 2;
padding-right: 80px;
display: flex;
align-items: center;
margin-left: 40px;
}
div.work-info li {
padding-bottom: 30px;
transition: transform 0.3s;
}
div.work-info li.move {
transform: translateX(15px);
}
footer {
width: 100%;
z-index: 1;
position: fixed;
bottom: 0;
left: 0;
padding-left: 40px;
padding-right: 40px;
padding-bottom: 40px;
color: #979797;
}
section.work-page {
scroll-snap-align: start;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
position: relative;
}
section.work-page img {
max-width: 60vw;
}
<body>
<!-- hidden modal that runs off of the info.js script -->
<section class="info-page">
<h1>
Hello
</h1>
</section>
<header>
<a class="contact" href="#">Info</a>
</header>
<!-- objects that get new classes with javascript on pageYOffset -->
<div class="work-info">
<ul>
<li id="p01" data-number="FN-UP Magazine">01</li>
<li id="p02" data-number="Cameron Tidball-Sciullo">02</li>
<li id="p03" data-number="Jacob Truax">03</li>
<li id="p04" data-number="Alexander Romero">04</li>
</ul>
</div>
<!-- scollable sections using the scroll points and triggering the pageYOffset -->
<div class="container">
<section class="work-page fn-up" data-client="FN-UP Magazine" data-seen-1="yes">
<div class="content">
<img src="lib/fn-up.png">
</div>
</section>
<section class="work-page cameron" data-client="Cameron Tidball-Sciullo" data-seen-2="yes">
<div class="content">
<img src="lib/alex.png">
</div>
</section>
<section class="work-page truax" data-client="Jacob Truax" data-seen-3="yes">
<div class="content">
<img src="lib/old.png">
</div>
</section>
<section class="work-page romero" data-client="Alexander Romero" data-seen-4="yes">
<div class="content">
<img src="lib/alex.png">
</div>
</section>
</div>
<footer class="footer">
<h2 class="about">FN-UP Magazine</h2>
</footer>
</body>
You have added a event listener to the page's Document object.
document.addEventListener("scroll", function () {
Then you calculate the number of pixels the document is currently scrolled along the vertical axis using window.pageYOffset.
const pixels = window.pageYOffset
When you set the CSS attribute overflow-y to scroll in the div.container element, new scrollbars appears on the window. According to MDN:
scroll
Content is clipped if necessary to fit the padding box. Browsers display scrollbars whether or not any content is actually clipped. (This prevents scrollbars from appearing or disappearing when the content changes.) Printers may still print overflowing content.
From that moment on, you are not scrolling the document, you are scrolling div.container. That won't trigger you scroll event.
You need to bound the event to the div element:
const container = document.querySelector("div.container")
container.addEventListener("scroll", function () {
And, instead of calculating how much document has scrolled, get the scrollTop property of the div.container:
const pixels = container.scrollTop
You need to make the same changes in whatever part of the code that involves the above calculations. In smoothScroll():
// const startPosition = window.pageYOffset;
const startPosition = container.scrollTop;
// window.scrollTo(0,run);
container.scrollTo(0,run);

Make element fixed on scroll

I'm attempting to make the navigation bar stick to the top, when the user scrolls down to the nav bar and then unstick when the user scrolls back up past the navbar. I understand that this can only be implemented via JavaScript. I'm a beginner to JavaScript, so the easier the better. The JSFIDDLE is here.
The HTML is as follows:
<section class="main">
<div id="wrap">
<div id="featured">
<div class="wrap">
<div class="textwidget">
<div class="cup"><img src="#""></div>
<div id="header"></div></div></div></div></div></div></div>
<div class="whiteboard">
<h1>HELLO GUYS</h1> </div>
</div>
<div class="bg1">
<h2> WE ARE AN EVENTS MANAGEMENT COMPANY BASED IN LONDON. </h2></div>
The CSS is as follows:
.main{text-align:center;}
h1{
-webkit-font-smoothing: antialiased;
display:inline-block;
font: 800 1.313em "proxima-nova",sans-serif;
padding: 10px 10px;
margin: 20px 20px;
letter-spacing: 8px;
text-transform: uppercase;
font-size:3.125em;
text-align: center;
max-width: 606px;
line-height: 1.45em;
position: scroll;
background-color:#e94f78;
text-decoration: none;
color:yellow;
background-image:url;
}
h1 a{
text-decoration: none;
color:yellow;
padding-left: 0.15em;
}
h2{
-webkit-font-smoothing: antialiased;
display:inline-block;
font: 800 1.313em "proxima-nova",sans-serif;
letter-spacing: 8px;
margin-top: 100px;
text-transform: uppercase;
font-size:3.125em;
text-align: center;
line-height: 1.45em;
position: scroll;
text-decoration: none;
color:white;
z-index: -9999;
}
h2 a{
text-decoration: none;
color:white;
padding-left: 0.15em;
}
h5{
position: absolute;
font-family:sans-serif;
font-weight:bold;
font-size:40px;
text-align: center;
float: right;
background-color:#fff;
margin-top: -80px;
margin-left: 280px;
}
h5 a{
text-decoration: none;
color:red;
}
h5 a:hover{
color:yellow;
}
#text1{
-webkit-font-smoothing: antialiased;
display:inline-block;
font: 800 1.313em "proxima-nova",sans-serif;
margin: 20px 20px;
letter-spacing: 8px;
text-transform: uppercase;
font-size:3.125em;
text-align: center;
max-width: 606px;
line-height: 1.45em;
position: scroll;
background-color:#E94F78;
}
#text1 a{
color:yellow;
text-decoration: none;
padding-left: 0.15em;
}
#text1 a:hover{
text-decoration: none;
cursor:pointer;
}
.whiteboard{
background-image:url(http://krystalrae.com/img/krystalrae-2012-fall-print-leopard-sketch.jpg);
background-position: center;
padding: ;
background-color: #fff;
z-index: 1000;
}
.bg{
height:2000px;
background-color:#ff0;
background-image:url(http://alwayscreative.net/images/stars-last.jpg);
position:relative;
z-index: -9999;
}
.bg1{
background-image:url(http://alwayscreative.net/images/stars-last.jpg);
z-index: -9999;
height:1000px;
}
/* Header */
#wrap {
margin: 0 auto;
padding: 0;
width: 100%;
}
#featured {
background: #E94F78 url(http://www.creativityfluid.com/wp-content/themes/creativityfluid/images/img-bubbles-red.png) no-repeat top;
background-size: 385px 465px;
color: #fff;
height: 535px;
overflow: hidden;
position: relative;
z-index: -2;
}
#featured .wrap {
overflow: hidden;
clear: both;
padding: 70px 0 30px;
position: fixed;
z-index: -1;
width: 100%;
}
#featured .wrap .widget {
width: 80%;
max-width: 1040px;
margin: 0 auto;
}
#featured h1,
#featured h3,
#featured p {
color: yellow;
text-shadow: none;
}
#featured h4{
color:white;
text-shadow:none;
}
#featured h4 {
margin: 0 0 30px;
}
#featured h3 {
font-family: 'proxima-nova-sc-osf', arial, serif;
font-weight: 600;
letter-spacing: 3px;
}
#featured h1 {
margin: 0;
}
.textwidget{
padding: 0;
}
.cup{
margin-top:210px;
z-index: 999999;
}
.container{font-size:14px; margin:0 auto; width:960px}
.test_content{margin:10px 0;}
.scroller_anchor{height:0px; margin:0; padding:0;background-image:url()}
.scroller{background:#FFF;
background-image:url(http://krystalrae.com/img/krystalrae-2012-fall-print-leopard-sketch.jpg);
margin:0 0 10px; z-index:100; height:50px; font-size:18px; font-weight:bold; text-align:center; width:960px;}
You can do that with some easy jQuery:
http://jsfiddle.net/jpXjH/6/
var elementPosition = $('#navigation').offset();
$(window).scroll(function(){
if($(window).scrollTop() > elementPosition.top){
$('#navigation').css('position','fixed').css('top','0');
} else {
$('#navigation').css('position','static');
}
});
I wouldn't bother with jQuery or LESS. A javascript framework is overkill in my opinion.
window.addEventListener('scroll', function (evt) {
// This value is your scroll distance from the top
var distance_from_top = document.body.scrollTop;
// The user has scrolled to the tippy top of the page. Set appropriate style.
if (distance_from_top === 0) {
}
// The user has scrolled down the page.
if(distance_from_top > 0) {
}
});
There are some problems implementing this which the original accepted answer does not answer:
The onscroll event of the window is firing very often. This
implies that you either have to use a very performant listener, or
you have to delay the listener somehow. jQuery Creator John Resig
states here how a
delayed mechanism can be implemented, and the reasons why you should
do it. In my opinion, given todays browsers and environments, a
performant listener will do as well. Here is an implementation of the pattern suggested by John Resig
The way position:fixed works in css, if you scroll down the page and move an element from position:static to position: fixed, the page will "jump" a little because the document "looses" the height of the element. You can get rid of that by adding the height to the scrollTop and replace the lost height in the document body with another object. You can also use that object to determine if the sticky item has already been moved to position: fixed and reduce the calls to the code reverting position: fixed to the original state: Look at the fiddle here
Now, the only expensive thing in terms of performance the handler is really doing is calling scrollTop on every call. Since the interval bound handler has also its drawbacks, I'll go as far as to argue here that you can reattach the event listener to the original scroll Event to make it feel snappier without many worries. You'll have to profile it though, on every browser you target / support. See it working here
Here's the code:
JS
/* Initialize sticky outside the event listener as a cached selector.
* Also, initialize any needed variables outside the listener for
* performance reasons - no variable instantiation is happening inside the listener.
*/
var sticky = $('#sticky'),
stickyClone,
stickyTop = sticky.offset().top,
scrollTop,
scrolled = false,
$window = $(window);
/* Bind the scroll Event */
$window.on('scroll', function (e) {
scrollTop = $window.scrollTop();
if (scrollTop >= stickyTop && !stickyClone) {
/* Attach a clone to replace the "missing" body height */
stickyClone = sticky.clone().prop('id', sticky.prop('id') + '-clone')
stickyClone = stickyClone.insertBefore(sticky);
sticky.addClass('fixed');
} else if (scrollTop < stickyTop && stickyClone) {
/* Since sticky is in the viewport again, we can remove the clone and the class */
stickyClone.remove();
stickyClone = null;
sticky.removeClass('fixed');
}
});
CSS
body {
margin: 0
}
.sticky {
padding: 1em;
background: black;
color: white;
width: 100%
}
.sticky.fixed {
position: fixed;
top: 0;
left: 0;
}
.content {
padding: 1em
}
HTML
<div class="container">
<div id="page-above" class="content">
<h2>Some Content above sticky</h2>
...some long text...
</div>
<div id="sticky" class="sticky">This is sticky</div>
<div id="page-content" class="content">
<h2>Some Random Page Content</h2>...some really long text...
</div>
</div>
Here you go, no frameworks, short and simple:
var el = document.getElementById('elId');
var elTop = el.getBoundingClientRect().top - document.body.getBoundingClientRect().top;
window.addEventListener('scroll', function(){
if (document.documentElement.scrollTop > elTop){
el.style.position = 'fixed';
el.style.top = '0px';
}
else
{
el.style.position = 'static';
el.style.top = 'auto';
}
});
You want to use jQuery WayPoints. It is a very simple plugin and acheives exactly what you have described.
Most straightforward implementation
$('.thing').waypoint(function(direction) {
alert('Top of thing hit top of viewport.');
});
You will need to set some custom CSS to set exactly where it does become stuck, this is normal though for most ways to do it.
This page will show you all the examples and info that you need.
For future reference a example of it stopping and starting is this website. It is a "in the wild" example.
You can go to LESS CSS website http://lesscss.org/
Their dockable menu is light and performs well. The only caveat is that the effect takes place after the scroll is complete. Just do a view source to see the js.
You can do this with css too.
just use position:fixed;
for what you want to be fixed when you scroll down.
you can have some examples here:
http://davidwalsh.name/demo/css-fixed-position.php
http://demo.tutorialzine.com/2010/06/microtut-how-css-position-works/demo.html
window.addEventListener("scroll", function(evt) {
var pos_top = document.body.scrollTop;
if(pos_top == 0){
$('#divID').css('position','fixed');
}
else if(pos_top > 0){
$('#divId').css('position','static');
}
});
Plain Javascript Solution (DEMO) :
<br/><br/><br/><br/><br/><br/><br/>
<div>
<div id="myyy_bar" style="background:red;"> Here is window </div>
</div>
<br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
<script type="text/javascript">
var myyElement = document.getElementById("myyy_bar");
var EnableConsoleLOGS = true; //to check the results in Browser's Inspector(Console), whenever you are scrolling
// ==============================================
window.addEventListener('scroll', function (evt) {
var Positionsss = GetTopLeft ();
if (EnableConsoleLOGS) { console.log(Positionsss); }
if (Positionsss.toppp > 70) { myyElement.style.position="relative"; myyElement.style.top = "0px"; myyElement.style.right = "auto"; }
else { myyElement.style.position="fixed"; myyElement.style.top = "100px"; myyElement.style.right = "0px"; }
});
function GetOffset (object, offset) {
if (!object) return;
offset.x += object.offsetLeft; offset.y += object.offsetTop;
GetOffset (object.offsetParent, offset);
}
function GetScrolled (object, scrolled) {
if (!object) return;
scrolled.x += object.scrollLeft; scrolled.y += object.scrollTop;
if (object.tagName.toLowerCase () != "html") { GetScrolled (object.parentNode, scrolled); }
}
function GetTopLeft () {
var offset = {x : 0, y : 0}; GetOffset (myyElement.parentNode, offset);
var scrolled = {x : 0, y : 0}; GetScrolled (myyElement.parentNode.parentNode, scrolled);
var posX = offset.x - scrolled.x; var posY = offset.y - scrolled.y;
return {lefttt: posX , toppp: posY };
}
// ==============================================
</script>
The solution that worked for me lately is:
.sticky {
position: fixed;
top: 0;
width: 100%;
}
var header = document.getElementById("filters-tab");
var sticky = header.offsetTop;
if (window.pageYOffset > sticky) {
header.classList.add("fixed");
} else {
header.classList.remove("fixed");
}
Javascript is no longer required for this.
Do this using the CSS position:sticky property
https://css-tricks.com/position-sticky-2/

Categories

Resources