Reputation: 17
I'm building a html page with a navigation bar at the top, in the center and horizontally aligned.
The idea is to have a 'sliding' selection bar, rather than a border that only appears on a selected or hovered-over item in the navigation bar. I have that working for the development page, for example if I scroll my mouse over to 'the cabaret', the border around the 'development' element correctly 'slides' to the right of the page, eventually wrapping around 'the cabaret'.
Above you can see me screenshotting midway after I've hovered my mouse over 'the cabaret'.
The problem I'm having comes with the other pages, which for a reason that has been baffling me for hours, are not working like the development page. Here is an example:
As you can see, when I choose to navigate to this page, the slider does not initialise correctly, instead loading in as vertical line in the middle of the navigation bar, and displaying no further behaviour, if I try to hover over another element in the navigation bar for example.
But why! I have used console.log in my JS to ensure the right active link is being passed when an option is chosen, and I can see it is when checking the console during runtime.
Here is what I believe to be the relevant HTML, CSS and JS:
// Add event listeners for option clicks
const options = document.querySelectorAll('.option');
let selectedOption = null;
options.forEach((option) => {
option.addEventListener('click', () => {
// Deselect previous option
if (selectedOption) {
selectedOption.classList.remove('selected');
}
// Select the clicked option
option.classList.add('selected');
selectedOption = option;
// Get page ID from data attribute in HTML
const pageId = option.getAttribute('data-page');
const activeLinkId = option.getAttribute('data-active-link');
const activeLink = document.getElementById(activeLinkId);
navigateToPage(pageId, activeLink);
});
});
// PAGE NAVIGATION
function navigateToPage(pageId, activeLink) {
const portfolioPageContent = document.getElementById('portfolio-page').children; // children so not background, goal to swipe text elements up
const selectedPage = document.getElementById(pageId);
if (!selectedPage) {
console.error(`Page with ID "${pageId}" not found.`);
return;
}
// Update navbar links for active state
const links = document.querySelectorAll('.navbar a');
links.forEach(link => link.classList.remove('active')); // Remove 'active' from all links
if (activeLink) {
activeLink.classList.add('active'); // Add 'active' to the selected link
moveSlider(activeLink); // Move slider to active link
}
// Swipe the portfolio page content up, not the background
gsap.to(portfolioPageContent, {
y: '-200%',
opacity: 0,
duration: 0.8,
ease: 'power2.inOut',
onComplete: () => {
if (selectedPage) {
selectedPage.style.display = 'block';
gsap.to(selectedPage, { opacity: 1, duration: 0});
} else {
console.error(`Page with ID "${pageId}" not found.`);
}
// Fade in new page first, then hide old page
gsap.to(selectedPage, { opacity: 1, duration: 1 }, "fade-in")
.then(() => {
document.getElementById('portfolio-page').style.visibility = 'hidden';
});
console.log(activeLink);
if (activeLink) {
setTimeout(() => {
moveSlider(activeLink);
}, 100);
}
// Add hover-over effect to move the slider
links.forEach(link => {
link.addEventListener('mouseenter', () => moveSlider(link));
});
// Keep slider on the active link when not hovering
navbar.addEventListener('mouseleave', () => {
if (activeLink) moveSlider(activeLink);
});
},
});
}
const navbar = document.querySelector('.navbar');
const slider = document.querySelector('.slider');
const links = document.querySelectorAll('.navbar a');
console.log(links);
// Function to move the slider
function moveSlider(link) {
const rect = link.getBoundingClientRect(); // Get the link's position
const navbarRect = navbar.getBoundingClientRect(); // Get navbar's position
const sliderWidthAdjustment = 1.5; // Shrink the width slightly
const sliderLeftAdjustment = -1.5; // Offset to the left slightly
slider.style.width = `${rect.width - sliderWidthAdjustment}px`; // Adjust width
slider.style.left = `${rect.left - navbarRect.left + sliderLeftAdjustment}px`; // Adjust position
}
#page-development,
#page-messagetomountains,
#page-cabaret {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
color: #202020;
text-align: center;
z-index: 10;
opacity: 0;
transition: opacity 1s ease-in-out;
/* text-shadow: 0.001rem 0 0.1rem #c8fcfc; */
text-shadow: none;
font-family: "Lucida Console", "Courier New", monospace;
}
#page-development {
background: radial-gradient(circle, #f4d4b4 0%, #e9ceb3 100%);
}
#page-messagetomountains {
/* background: radial-gradient(circle, #F5C7C7 0%, #e0bdbd 100%); */
background: radial-gradient(circle, #94d0e8 0%, #82c1db 100%);
color: white ;
}
#page-cabaret {
background: radial-gradient(circle, #8E98F9 0%, #868ed6 100%);
}
.navbar {
position: relative;
display: flex;
justify-content: center;
/* align-items: center;
flex-wrap: wrap; */
background-color: #82c1db;
padding: 10px 20px;
}
.navbar a {
position: relative;
color: white;
text-decoration: none;
padding: 8px 20px;
margin: 0 150px;
border: 2px solid transparent;
border-radius: 20px;
z-index: 1; /* links above slider */
transition: color 0.3s, transform 0.3s;
}
.navbar a:hover {
color: #eeffff;
}
.navbar a.active {
color: #ffffff;
}
/* moving border */
.slider {
position: absolute;
height: calc(100% - 23px); /* try match link padding */
border: 2px solid #ffffff;
border-radius: 20px;
background: none;
transition: all 0.4s ease;
z-index: 0; /* sits behind the links */
}
<div id="page-development" style="display: none;">
<nav class="navbar">
<div class ="slider"></div>
<a href="#development" class="active" id = "development-link">development</a>
<a href="#messagetomountains">messagetomountains</a>
<a href="#cabaret">the cabaret</a>
</nav>
<h2>dev page</h2>
<p>this is the development page.</p>
</div>
<div id="page-messagetomountains" style="display: none;">
<nav class="navbar">
<div class ="slider"></div>
<a href="#development">development</a>
<a href="#messagetomountains" class="active" id = "messagetomountains-link">messagetomountains</a>
<a href="#cabaret">the cabaret</a>
</nav>
<h2>messagetomountains page</h2>
<p>this is the messagetomountains page.</p>
</div>
<div id="page-cabaret" style="display: none;">
<nav class="navbar">
<div class ="slider"></div>
<a href="#development">development</a>
<a href="#messagetomountains">messagetomountains</a>
<a href="#cabaret" class="active" id = "cabaret-link">the cabaret</a>
</nav>
<h2>cabaret page</h2>
<p>welcome to the cabaret.</p>
</div>
To give context, there is a central portfolio page that houses 3 options. When an option is clicked, it fades out the portfolio page by swiping the text up on the portfolio page, and then fades in the content from one of the options (page-development, page-cabaret, etc.). Only though when the 'page-development' is displayed does the slider behave correctly, and in the other two cases it does not.
What have I tried? I've tried rewriting the moveSlider function, and have had success in changing the way the slider works and yet it still will only work for the development page. No matter what behaviour I try to execute. I've tried reworking the navigateToPage function, such that I try only to initialise the navbar inside there and that has not worked either. My code during this process has become a bit of a mess to which is unfortunate, due to me trying to incorporate this 'activeLink' parameter which was not part of the code before.
This has been my first time using GSAP and also having a page 'fade in' and 'fade out' other pages on top of it, rather than just navigating to a specific option page when an option is clicked e.g. /development. I think in trying to do it this way I have overcomplicated things and made the transitioning between pages quite awkward.
I would really appreciate any guidance on how to correct the slider such that it behaves as intended and as it currently does on the development page, across all other pages where it is present. To reiterate, that would be the selected page has a border, and dragging the mouse over another option would 'slide' that border across to that option. If not clicked, and the mouse is dragged away, the border would automatically slide back to its initial position, resting around the currently selected page's option.
If nothing else, I'm really curious as to why this is only working on the development page. It is bugging me so much! I suppose it's something to do with it being the first page listed in my HTML .. but why? Thanks so much for any help :D
Upvotes: 0
Views: 97
Reputation: 13145
For the "slider" you don't need an extra element (<div class ="slider"></div>
). You can use the :before
or :after
pseudo element on the parent element (here the <menu>
element). If you position the pseudo element absolute, it can be moved around using the left
property. For setting the left
property you can use a combination of :target
and :has()
to style the menu:after
element with the right left
value. I set this part of the stylesheet dynamically when the document has loaded, and then add the styles to <style title="dynamic"></style>
.
document.addEventListener('DOMContentLoaded', e => {
// get the anchor with the current hash
let target = document.querySelector(`menu a[href="${location.hash}"]`);
// if there is no target element (hash empty or non exsistent) "navigate" to
// the target of the first menu anchor.
if(!target){
location.hash = document.querySelector(`menu a`).getAttribute('href');
}
// calculate width and left properties for each menu:after seletor
document.querySelector('style[title="dynamic"]').innerHTML =
[...document.querySelectorAll('menu li')].map(elm => {
let a = elm.querySelector('a');
let width = a.getBoundingClientRect().width;
let left = a.getBoundingClientRect().left;
return `body:has(div#${elm.dataset.id}:target) menu:has(li[data-id="${elm.dataset.id}"]):after {
left: ${left - 6}px;
width: ${width + 8}px;
}`;
}).join('\n');
});
body,
html {
margin: 0;
font-family: sans-serif;
}
header {
background-color: #82c1db;
height: 2.5em;
display: flex;
align-items: center;
}
menu {
margin: 0;
padding: 0;
width: 100%;
}
menu ul {
position: relative;
display: flex;
list-style: none;
padding: 0;
}
menu li {
flex-basis: calc(100% / 3);
text-align: center;
}
menu a {
text-decoration: none;
color: white;
}
menu:after {
display: block;
position: absolute;
content: "";
width: 0;
left: 0;
top: calc(2em - 1.5em - 2px);
height: 1.5em;
border: solid white 2px;
border-radius: calc(1.5em / 2);
transition: left .4s;
z-index: 0;
}
main {
display: grid;
}
main div {
grid-column: 1;
grid-row: 1;
opacity: 0;
transition: opacity .4s;
}
div:target {
opacity: 1;
}
<style title="dynamic"></style>
<header>
<menu>
<ul>
<li data-id="development"><a href="#development">development</a></li>
<li data-id="messagetomountains"><a href="#messagetomountains">messagetomountains</a></li>
<li data-id="cabaret"><a href="#cabaret">the cabaret</a></li>
</ul>
</menu>
</header>
<main>
<div id="development">development</div>
<div id="messagetomountains">messagetomountains</div>
<div id="cabaret">the cabaret</div>
</main>
Upvotes: 2
Reputation: 29511
Creating a slider for a horizontal navigation bar that 'slides' between options
You can use a CSS transition
to create the sliding effect.
In this case, whichever item in the horizontal menu contains the class .selected
will determine the position of the slider.
Working Example:
// GET LIST ITEMS
const listItems = document.querySelectorAll('li');
// SELECT CLICKED LIST ITEM
const selectListItem = (e) => {
document.querySelector('.selected').classList.remove('selected');
e.currentTarget.classList.add('selected');
}
// ADD EVENT LISTENER TO EACH LIST ITEM
[...listItems].forEach((listItem) => listItem.addEventListener('click', selectListItem));
:root {
--optionWidth: 200px;
--optionHeight: 36px;
}
header {
position: relative;
}
ul {
list-style-type: none;
padding-left: 0;
}
li {
float: left;
display: inline-block;
width: var(--optionWidth);
height: var(--optionHeight);
line-height: calc(var(--optionHeight) - 4px);
text-align: center;
border: 2px solid rgba(0, 0, 0, 0);
border-radius: 6px;
box-sizing: border-box;
cursor: pointer;
}
li:hover {
background-color: rgb(239, 239, 255);
border: 2px solid rgb(239, 239, 255);
}
.selection-border {
position: absolute;
top: 0;
left: 0;
width: var(--optionWidth);
height: var(--optionHeight);
border: 2px solid rgb(0, 0, 127);
border-radius: 6px;
box-sizing: border-box;
transition: transform 0.3s ease-out;
}
header:has(li:nth-of-type(2).selected) .selection-border {
transform: translateX(calc(var(--optionWidth) * 1));
}
header:has(li:nth-of-type(3).selected) .selection-border {
transform: translateX(calc(var(--optionWidth) * 2));
}
<header>
<ul>
<li class="selected">Option A</li>
<li>Option B</li>
<li>Option C</li>
</ul>
<div class="selection-border"></div>
</header>
Upvotes: 0