Horizontal Scrolling div with Arrows JavaScript

Horizontal Scrolling div with Arrows JavaScript
Code Snippet:Horizontal Scrolling Menu with Arrow Keys (in Vanilla JavaScript, no jQuery)
Author: Ken A Collins
Published: April 13, 2024
Last Updated: April 16, 2024
Downloads: 152
License: MIT
Edit Code online: View on CodePen
Read More

This JavaScript code snippet helps you to create a horizontal scrolling div with left and right arrows. The arrows allow you to navigate through menu items that overflow the container. It uses animation frames for smooth scrolling. The purpose is to enable easy navigation through a menu with many items.

How to Create Horizontal Scrolling Div With Arrows Using JavaScript

1. Begin by setting up the HTML structure for the menu. Inside the <body> tag, create a <div> with an ID of “menu-wrapper” and a class of “menu-wrapper”. Inside this div, add a <ul> element with an ID of “menu” and a class of “menu”. Each menu item should be represented by an <li> element with a class of “item”. Additionally, include two <button> elements with IDs “leftArrow” and “rightArrow” for the left and right arrows, respectively.

<div id="menu-wrapper" class="menu-wrapper">
	<ul id="menu" class="menu">
		<li class="item">1</li>
    <li class="item">2</li>
    <li class="item">3</li>
    <li class="item">4</li>
    <li class="item">5</li>
    <li class="item">6</li>
    <li class="item">7</li>
    <li class="item">8</li>
	</ul>

	<div class="arrows">
		<button id="leftArrow" class="left-arrow arrow hidden">
			<
		</button>
		<button id="rightArrow" class="right-arrow arrow">
			>
		</button>
	</div>

</div>

<div class="print" id="print-wrapper-size"><b>Wrapper size:</b> <span></span></div>
<div class="print" id="print-menu-size"><b>Total menu size:</b> <span></span></div>
<div class="print" id="print-menu-invisible-size"><b>Invisible menu size:</b> <span></span></div>
<div class="print" id="print-menu-end-offset"><b>Menu end offset:</b> <span></span></div>
<div class="print" id="print-menu-position"><b>Scroll position:</b> <span>0</span></div>

2. Apply CSS styles to create the layout and appearance of the menu. Customize the styles according to your design preferences. Ensure that the menu items overflow horizontally to trigger scrolling.

body {
  margin: 3em;
  font-family: Arial, Helvetica, sans-serif;
}

* {
  padding: 0;
  margin: 0;
}

.menu-wrapper {
  position: relative; /* Required for arrow keys to be absolutely positioned child divs inside menu-wrapper parent. */
  width: 500px;
  height: 100px;  /* Intentionally shorter than menu items, to hide horizontal scroll bar. */
  margin: 1em auto;
  border: 1px solid black;
  overflow-x: hidden;
  overflow-y: hidden;
  display: flex;
  align-items: center;
  padding: 0 20px;  /* Inner space on the left and right sides of wrapper must match flexbox gap space between menu items. */
  box-sizing: border-box;
}

ul {
  list-style: none; /* Hide unordered list bullet. */
}

.menu {
  height: 120px;
  /* background: #f3f3f3; */
  box-sizing: border-box;
  white-space: nowrap;
  -webkit-overflow-scrolling: touch;
  position: relative; /* Required for animation. */
  display: flex;
  align-items: center;
  gap: 20px;  /* Flexbox space between menu items must match the left/right padding of menu wrapper. */
}

.menu .item {
  background: #f3f3f3;  /* Weird - Visible items inherit this from .menu but not hidden items# 6-8 when they slide in and can be seen. */
  width: 75px;
  height: 75px;
  outline: 1px dotted gray;
  box-sizing: border-box;
  border-radius: 5px;
  display: flex;            /* Needed to center number in middle of menu item, solution # 1 of 3. */
  align-items: center;      /* Needed to vertically center number in middle of menu item, solution # 2 of 3. */
  justify-content: center;  /* Needed to horizontally center number in middle of menu item, solution # 3 of 3. */
}

.arrow {
  position: absolute;
  top: 0;
  bottom: 0;
  /* width: 3em; Excluding width means that arrow div will only be as wide as it needs to be to contain the < or > characters. */
}

.left-arrow {
  left: 0;
}

.right-arrow {
  right: 0;
}

.hidden {
  display: none;
}

.print {
  margin: auto;
  max-width: 500px;
}

.print span {
  display: inline-block;
  width: 100px;
}

3. Now, let’s add JavaScript functionality to enable scrolling when the arrow buttons are clicked. The JavaScript code provided in the example achieves this by calculating the distance to scroll and animating the menu accordingly.

/**
 This code is based on the following developer's work found here https://codepen.io/mahish/pen/RajmQw with the following improvements:
 o Eliminates dependency on jQuery. Is rewritten in vanilla JavaScript with animation frames.
 o Scrolls to next hidden menu item and stops, does not jump to the far right which would be bad if a lot of menu items in beginning of list became hidden.
 o Has a more realistic design with menu items that are spaced apart and smaller than their container.
 o Uses flexbox for positioning.
 o Increases count of menu items from 4 to 8 to simulate a real need for scrolling.
 o Hides right arrow if all menu items fit in the container eliminating the need for scrolling.
 o Simplifies the code by eliminating functions that are only called once.
 o Has meaningful variable names and lots of comments to explain how it works.
 */

// DOM elements to track.
const leftArrow = document.getElementById('leftArrow');
const rightArrow = document.getElementById('rightArrow');
const menu = document.getElementById('menu');

// Establish unchanging constants and initialize variables.
const menuWrapperSize = document.getElementById('menu-wrapper').offsetWidth; // Unchanging area of the screen where the menu is always visible.
const menuSize = document.getElementById('menu').offsetWidth;	// Includes itemsCount * itemSize but also factors in space between items added by flexbox.
const menuInvisibleSize = Math.max(menuSize - menuWrapperSize, 0);	// Fixed portion of scrollable menu that is hidden at all times, or zero if menu fits within container.
const arrowSize = rightArrow.offsetWidth;	// Width of each arrow div. In current design, this equates to 12px. Still computes value even if right arrow is hidden, which it is at time this line is executed.
const menuEndOffset = Math.max(menuInvisibleSize - arrowSize, 0);	// Fixed portion of scrollable menu that is not obscured by an overlapping arrow key, or zero if no arrow keys are needed.
const itemsCount = document.getElementsByClassName('item').length; // Number of menu items.
const itemSize = document.getElementsByClassName('item')[0].offsetWidth; // offsetWidth includes borders and padding but not margins of a menu item (since all the same, choose first one in array). FYI, clientWidth includes padding but NOT borders and margins.
const itemsSpaceBetween = (menuSize - (itemsCount * itemSize)) / (itemsCount - 1);	// Space between menu items is deliberately set to equal menu wrapper padding left/right. In this design it is 20 pixels.
const distanceInPixels = itemSize + itemsSpaceBetween;	// Distance to scroll per arrow button click equals width of a menu item plus the space to its right or left. In this design, it is 75 + 20 = 95.
const durationInMilliseconds = 500;
let starttime = null;

// Iniitially, on page load menu items are left aligned and left arrow is hidden. Let's hide right arrow also if there is no need for it (as when all menu items fit within visible container).
if (menuInvisibleSize === 0) {
	rightArrow.classList.add("hidden");
}

// Get current left position of menu in pixels.
const getMenuPosition = () => {
	return parseFloat(menu.style.left) || 0;	// First time, left property is not set so initialize to 0.
};

// Get current distance (in pixels) that we have scrolled.
const getScrolledDistance = () => {
	return -1 * getMenuPosition();	// Negate value because this is the only way it will work.
};

// After an arrow key is clicked and menu is animating, check to see where we are and determine which arrow key(s) to show, always resulting in at least one arrow key visible. Also, update data at bottom.
// Notes: o This function is only applicable when all menu items cannot be seen in container at one time and an arrow key is clicked to animate menu. 
//        o If all menu items fit in visible container, UI will be initially rendered without any arrow keys and this function will never be called.
const checkPosition = () => {
	// Calculate where we are right now.
	const menuPosition = getScrolledDistance();

	// Determine which arrow key(s) to display based on position.
	if (menuPosition <= arrowSize) {			// SHOW RIGHT ARROW if we are scrolling from far left.
		leftArrow.classList.add("hidden");		// FYI, this will NOT create duplicate hidden class if leftArrow already contains it.	
		rightArrow.classList.remove("hidden");
	} else if (menuPosition < menuEndOffset) {	// SHOW BOTH ARROWS when in the middle of the menu.
		leftArrow.classList.remove("hidden");
		rightArrow.classList.remove("hidden");
	} else if (menuPosition >= menuEndOffset) {	// SHOW LEFT ARROW if we are scrolling as far right as we can go.
		leftArrow.classList.remove("hidden");
		rightArrow.classList.add("hidden");
    }

	// Print changing scroll position under the menu for informational purposes.
	document.querySelector("#print-menu-position span").textContent = menuPosition + 'px';
};

const animateMenu = (timestamp, startingPoint, distance) => {
    const runtime = timestamp - starttime;
    let progress = runtime / durationInMilliseconds;
    progress = Math.min(progress, 1);
	let newValue = (startingPoint + (distance * progress)).toFixed(2) + 'px';
	menu.style.left = newValue;

	if (runtime < durationInMilliseconds) {	// If we still have time remaining...
        requestAnimationFrame(function(timestamp) {	// Request another animation frame and recursively call THIS function.
            animateMenu(timestamp, startingPoint, distance);
        })
    }
	checkPosition();
};
 
const animationFramesSetup = (timestamp, travelDistanceInPixels) => {
	timestamp = timestamp || new Date().getTime();	// if browser doesn't support requestAnimationFrame, generate our own timestamp using Date.
	starttime = timestamp;
	const startingPoint = getMenuPosition();		// This cannot be defined up top in constants. Need to read current value only during initial setup of arrow button click.
	animateMenu(timestamp, startingPoint, travelDistanceInPixels);
};

rightArrow.addEventListener('click', () => requestAnimationFrame(
	timestamp => animationFramesSetup(timestamp, -1 * distanceInPixels)
));
	
leftArrow.addEventListener('click', () => requestAnimationFrame(
	timestamp => animationFramesSetup(timestamp, distanceInPixels)
));

// Print unchanging values under the menu for informational purposes.
document.querySelector("#print-wrapper-size span").textContent = menuWrapperSize + 'px';
document.querySelector("#print-menu-size span").textContent = menuSize + 'px';
document.querySelector("#print-menu-invisible-size span").textContent = menuInvisibleSize + 'px';
document.querySelector("#print-menu-end-offset span").textContent = menuEndOffset + 'px';

That’s all! hopefully, you have successfully created a Horizontal scrolling div with arrows using JavaScript. If you have any questions or suggestions, feel free to comment below.

Leave a Comment