Create an interactive menu component that can be customized for any type of website.
In our example, we’ll look at different types of pasta. This Know Your Pasta component can easily be a part of a food website or blog.
The main objective is to have a list of various pasta kinds that, when a user hovers over each, a background picture of that pasta type will display. As the mouse moves to another pasta name, a seamless transition will bring up its corresponding picture as background.
You can give this a try on your own or follow along as this post will guide you in building the following:
See the Pen Interactive Menu – Know Your Pasta 🙂 by Klea Merkuri (@thehelpfultipper) on CodePen.
The end result will be a responsive component that enables a user to explore the different kinds of pasta in an interesting way.
Create the main container
Let’s start by structuring our HTML. We need a main container with two children – one containing the heading, the other containing the pasta varieties.
Give the main container a background color and decide what fonts you’d like to use. We opted for two Google fonts – Jost and Inter – so we imported the Google fonts styles into the HTML document.
For steps to import Google fonts in an HTML document, see the example right here.
<div id="main-container">
<div id="heading">
<h4>PASTA VARIETIES</h4>
</div>
<div id="pasta-main"></div>
</div>
#main-container {
width: 90%;
height: 100vh;
margin: 0 auto;
background-color: #1B1D1F;
color: white;
}
>> TIP <<
Be careful when working with Google fonts and font weights. For example, our font-family choice of “Jost” comes at two different font weights which we import via the link tag.
“family=Jost:wght@300;400”
We, then, are able to use two font-weight types for Jost – 300 and 400 – specified by the font-weight property. But we cannot use something like 100 or 800 because we haven’t imported those properties (if they even exist).
To include different properties for each Google font type you use, don’t forget to import all associated properties you want to use!
Style the heading container
Center the heading, change its font, and give it the correct placement in the window. To move it vertically further from the top relative to the parent container, add padding at the top.
Spread each character of the heading apart using the letter-spacing property.
#heading {
text-align: center;
font-family: Jost, sans-serif;
padding-top: 100px;
}
h4 {
letter-spacing: 0.2em;
font-weight: 300;
transform: scale(0.9);
}
Adding the pasta varieties
Next, decide on the number of pasta varieties to display and add them to the pasta-main container. We went for 11 different pasta options here, each pasta type being an h3 contained within a <div>
.
Fair warning: your browser might be trying to translate from Italian and it can get annoying.
<div id="pasta-main">
<div><h3>Farfalle</h3></div>
<div><h3>Rotini</h3></div>
<div><h3>Paccheri</h3></div>
<div><h3>Tagliatelle</h3></div>
<div><h3>Pici</h3></div>
<div><h3>Spaghetti</h3></div>
<div><h3>Cavatelli</h3></div>
<div><h3>Rigatoni</h3></div>
<div><h3>Macaroni</h3></div>
<div><h3>Mostaccioli</h3></div>
<div><h3>Fettuccine</h3></div>
</div>
Styling the different pasta types
Give #pasta-main a width slightly smaller than its parent, center it using auto left and right margins, and center its contents.
#pasta-main {
width: 90%;
margin: 0 auto;
text-align: center;
}
Continue by making each div holding an h3 occupy the width of its content rather than the entire line. We want to have pasta types next to each other instead of vertically stacked. To do this, change the display from the default block level of a div element to inline-block.
Click to read more on block vs inline level elements.
#pasta-main div {
display: inline-block;
}
Back on #main-container, get rid of the height:100vh
to allow the container to default to the height of its contents. We set a height in the beginning to show the area we’re working in; now it’s unnecessary as there is content in the container.
You should have something like this:
Finish styling the pasta types (for now) by increasing the size of the h3s and assigning paddings and margins to improve their display.
h3 {
padding: 5px 30px;
font-size: 80px;
font-family: Jost, sans-serif;
font-weight: 600;
letter-spacing: 0.05em;
}
What you’ll get is an organized, far more appealing presentation of the different pasta varieties – with a hiccup.
There is excessive spacing between the lines of pasta types which is strange considering we haven’t assigned any margins to either the div or the h3. This spacing is due to the default styles of an h3 and, in particular, to the margins it comes with.
To override the default spacing of an h3 add margin:0!important
to the above code.
Thinking on intention, a user should have the option to discover more pasta varieties once they explore the ones listed. So let’s include a dummy button that, were this a real component on an active food blog, for instance, would direct a user to a page devoted to the clicked pasta type.
Place the button inside #main-container but outside #pasta-main.
<button id="discover-btn" type="button">Discover Pastas</button>
The result is extremely anti-climatic, but don’t let that dampen your spirits. We’ll remedy the situation by giving the button padding, a border, and a transparent background.
Move the button in the center by assigning text-align:center
to its parent, #main-container. In doing this, we need to make a small adjustment to #heading and #pasta-main by removing the text-align property assigned to each container as it’s now redundant.
The text-align criteria, when applied to the parent, applies to all its children so no need to declare it for each child.
Note: Remove the extra “text-align”s from the direct children of #main-container!
#discover-btn {
background: transparent;
border: 1px solid white;
padding: 17px 40px;
color: white;
font-family: Jost, sans-serif;
font-weight: 300;
letter-spacing: 00.02em;
transform: scale(1.2);
margin-bottom: 120px;
margin-top: 80px;
border-radius: 2px;
}
Hovering over the button will do four things:
- change the background of the button to white
- change the font color to that of #main-container
- involve a transition so the hover changes are less jarring to the eye
- turn the cursor into a pointer
Let’s tackle one, two, and four first by changing colors, font-weight, and cursor type.
#discover-btn:hover {
background-color: white;
color: #303133;
cursor: pointer;
font-weight: 400;
}
Then go to the button and add transition:background-color 0.15s ease-in-out.
This transition causes the background color to slowly change over a period of 0.15 seconds.
Get more practice using transitions with How To Make An Animated Button With CSS.
How to get a floating background image on hover
Time to add the piece de resistance of this component – the floating background image that follows the cursor’s move while hovering over a pasta type.
A few things need to happen once a user hovers over a pasta type:
- the cursor changes to a pointer
- for the expanse of the div holding the h3 pasta name, a background image of that pasta will show
- the background pasta image is dynamic and moves along with the user’s cursor
- as the user hovers over other pasta names, there’s a seamless transition between background images
As we said, quite a few things happen here. We’ll tackle them one by one using CSS and some JavaScript.
Choosing background images
All images are from Unsplash. We made sure to specify landscape orientation and searched by the keyword “pasta” – it was an extremely pleasant experience.
Then we sized them down to a width of 1350px which isn’t ideal if you’re aiming for responsive images but will work for this example.
Disclaimer: Thanks to my lovely QA team I now know I’ve made un grosso errore. The picture for mostaccioli is that of penne. But mostaccioli isn’t penne and penne isn’t mostaccioli. Mi dispiace.
Challenge: Make the images responsive across different screen sizes using srcset and sizes. This way they don’t just scale, but fully cater to various screen sizes.
Adding responsive images
First, let’s tackle showing a background image of each pasta when hovering over the pasta name.
Include the image in a div
with a class of #pasta-img and give it a width that is the size we want the image to be, 725px.
<div class="pasta-img"><img src="./img/Farfalle.jpg"></div>
Click to read more about file paths when linking external files and assets.
To have the image take on the width of the #pasta-img wrapper give img
a width of 100% and a height of auto. Otherwise, the img
will spill out of its container leading to overflow as it’s by default larger than its 725px parent.
.pasta-img {
width: 725px;
}
.pasta-img img {
width: 100%;
height: auto;
}
Take #pasta-img out of the regular flow of the page by giving it an absolute position and move it upwards to the left corner of #main-container.
position: absolute
top: 0
left: 0
Looks odd, right? Don’t panic, keep following along. There’s one more thing we need to fix here and that’s the stack order of components. We want the image to lie underneath the pasta names – currently, it lies on top.
Layering elements on the page
Use z-index to layer the pasta names on top of the image. I admit to getting bogged down when trying to figure out the whole layering process, but it’s all rather simple when taking time to consider element nesting.
All we need to do is assign a relative position with a z-index of 5 to the h3
s (our pasta titles). Position is one of the elements determining stacking order so make sure to declare it to avoid running into the annoyance of having your z-index “not work”.
Since #pasta-img already has a declared position of absolute (and a default z-index of 0), it will naturally lie on top until specifying a higher z-index value for h3
with a declared position. The value can be anything bigger than 0 like 5 or 55 or 999 – your pick.
Add the following to your h3
styles:
position: relative;
z-index: 5;
The last step is to hide all images using visibility:hidden
.
Interactive hover effects
While hovering over #pasta-img three things should occur:
- all pasta names other than the one hovered over become somewhat transparent (spotlight effect)
- the corresponding image displays in the background
- the image eases in from the direction of the mouse and continues following the mouse
In this step, we’ll focus on points 1 and 2 – we’ll look at 3 later on.
How to create a spotlight effect
Use opacity to give all the h3
s other than the one the mouse hovers over a “transparent” look and get a spotlight effect.
This took some trial and error to get right and I credit joshnh‘s answer for understanding the parent-child hover relationship to make it all possible.
#pasta-main:hover h3 {
opacity: 0.25;
}
#pasta-main h3:hover {
opacity: 1;
cursor: pointer;
}
First, lower the opacity of the h3 children when hovering over the parent #pasta-main container. This is when all pasta names become “transparent”.
Be careful! We don’t want the pasta names to be transparent all the time such when not hovering over #pasta-main or on page load.
Second, make the h3 you’re hovering over fully opaque while the rest of the h3s remain “transparent” by setting opacity:1
on h3:hover. This action allows the rest of the elements to remain less opaque while affecting the opacity of the hovered-over element, putting it in the “spotlight”.
Display an image on hover
Display the background image when hovering over an h3 by setting the image’s visibility to visible.
h3:hover ~ .pasta-img img {
visibility: visible;
}
Note: We’re using a sibling selector to display the pasta image. Read Common CSS Selectors To Know And How To Use Them for more CSS selectors.
Make the image follow the mouse
To create the interactive background image we’ll practice one of the most important development skills you should hone – looking things up on Google. Knowing how to ask questions will become your biggest asset.
An initial search of how to create a moving background when moving the mouse points us in the right direction by introducing the mousemove event and two position properties, offsetLeft and offsetTop.
It’s okay if you are feeling a tad confused – we are too. But let’s continue searching!
We need to track the position of the mouse as it moves across the screen. A quick search brings up the MouseEvent clientX property along clientY for the equivalent of (x, y) coordinates. These two properties are called on the event object (resulting from our ‘mousemove’ event) so we know this is another factor to consider.
Let’s put together what we have so far. First, grab #pasta-main and all pasta names and images.
const mainCont = document.querySelector('#pasta-main');
const pastaName = document.querySelectorAll('h3');
const pastaImg = document.querySelectorAll('.pasta-img');
Click to learn more about how to access HTML elements with JavaScript.
Each image will change position as the mouse moves over the respective pasta name within the #pasta-main container. So listen for the mousemove event on mainCont (the variable that we stored #pasta-main ).
Get the coordinates of the mouse using the client properties discussed above. Then loop through the pasta images list, changing the position of each item based on the mouse coordinates.
We used translate() instead of setting top and left to get a smoother animation and avoid triggering a re-draw of layout.
Tip: Use a template literal to embed the mouse position variables in the style declaration to avoid the classical way of concatenation (the sort that happens with the “+” (addition) sign).
for (let i = 0; i < pastaName.length; i++) {
mainCont.addEventListener( 'mousemove', event => {
// mouse positions
let mouseX = event.clientX;
let mouseY = event.clientY;
for (let i = 0; i < pastaImg.length; i++) {
pastaImg[i].style.transform = `translate(${mouseX}px, ${mouseY}px)`;
}
});
}
Notice we manage to make the background image follow the movement of the mouse while drifting over the pasta names.
Fix the position of the moving image
Though we get the desired movement, the background images aren’t positioned very well on the screen.
The mouse is heavily focused on the left side of each image and, as we scroll, the image seems to move up out of view. For the last few pasta names, we can only view the bottom 1/4 of the image which isn’t the behavior we were going for.
Each image should have a location determined by the mouse’s movement relative to the borders of its parent, #pasta-main.
Searching for getting the mouse position relative to parent we come upon the getBoundingClientRect() method we can call on #pasta-main to get the coordinates of its bounds.
Don’t forget to account for #pasta-main’s position relative to the viewport, especially the left and top offsets!
// get top and left bounds of image (difference between offset relative to viewport and that relative to parent)
let boundX = mainCont.getBoundingClientRect().left - mainCont.offsetLeft;
let boundY = mainCont.getBoundingClientRect().top - mainCont.offsetTop;
Change the mouse coordinates to reflect the distance between the mouse’s location on the screen and the borders of the parent reference, #pasta-main.
pastaImg[i].style.transform = `translate(${mouseX - boundX}px, ${mouseY - boundY}px)
`
What does accounting for the parent’s bounds help us achieve? See how we are now moving the image with the mouse in the same location – at the very top left edge of the image.
The image’s top left corner is what moves with the mouse regardless of scrolling action. No longer are the images of the last few pasta names placed out of our view.
Lastly, make it so the center of the image follows the mouse instead of the top left corner. This corner is currently the focal point of where the mouse grabs the image.
To center the mouse we need to move the image to the left about halfway and to the top another halfway relative to this corner.
Add transform:translate(-50%, -50%)
to the img element.
>> TIP <<
Play around with the coordinate positions to actually see what’s going on. To DM’s amusement, I went so far as to draw a rough diagram of what I was looking at because this entire experience demanded extensive hair-pulling.
We suggest trying out different positions on the screen and console logging the coordinates to get a better idea of mouse movement, offsets, and bounds.
Animations and a smooth experience
We’re almost done, there are 3 things left to do:
- hide the parts of the image that go outside of #main-container
- position the image on top of the button (it’s currently underneath)
- follow mouse with ease
- create a fade-out effect when moving the mouse away from a pasta name
As always, we’ll tackle them one step at a time!
1. How to hide the overflow from the image
For the image overflow, hit Google once more to figure out just how to go about it.
A simple way to hide the parts of the image that fall outside of #main-container is to set position:relative
and overflow:hidden
to the parent element.
Note: Overflow and Position
Adding position and overflow won’t do anything if you set position:fixed
(instead of absolute) on #pasta-img.
Why? Because of the slight, albeit easily confused, difference between fixed and absolute position.
It’s not that the overflow isn’t hidden if we’d set a fixed position on #pasta-img, but that the parent container is the body itself rather than #main-container. Elements with position set to fixed are relative to the initial containing block.
What we want instead is an absolute position in order to place #pasta-img relative to #main-container. Setting the position to absolute successfully hides the parts of the image that go beyond #main-container.
Who’s looking forward to some more layering? This is a relatively simple case – we need to give the button a z-index lower than the image but not -1. A negative z-index will place the button under #main-container.
The layering order is:
z-index:5
for the pasta title. It can be any positive number so long as it’s the highest of the three.z-index:1
for the button. It cannot be a negative value as it’ll be pushed beneath its container.z-index:2
for the image container. It can be any number between 1 and 5 as it lies beneath the image but above the button.
3. Follow the mouse with ease
Currently, the image moves with the mouse because our logic positions it based on the mouse coordinates relative to the mouse’s location in the main container.
Instead of moving with the mouse, we want the image to move slightly slower so that the image follows the mouse with ease to get a smooth follow.
We can achieve this using transition:transform 0.2s ease-out
on #pasta-img. All we’re doing is slowing down the speed the image transforms and this is the transformation that translates the image so it moves along with the cursor.
By slowing down the image, we add a little drag and ease into the new location making the image follow the mouse instead of being the mouse.
4. Fade-out when mousing out of images
When the img is hidden, give it an opacity of 0. Then when a user hovers over an h3 and the image becomes visible, the opacity should change to 1 (otherwise we can’t see anything . . . duh).
Let’s add a fade-out effect when the mouse stops hovering over a pasta name. Do this using opacity and transition with a slight delay in the visibility change.
But setting opacity isn’t enough to make the image fade out. We need to add an animation element to opacity, mainly a time frame, so we ease into the opacity of 1 rather than jump to it. Though, in this case, we’d like to ease-out when we go from opacity of 1 to 0, easing into the initial zero opacity instead. Do this by setting transition:opacity 0.3s ease-out
to the img element.
Doesn’t appear to work, huh?
You’re wrong – it does work. You can’t see it working since the image’s visibility is hidden while the opacity is transitioning. It’s all about timing here and ours is off.
There’s one more property we need to transition and that’s visibility itself.
It might be confusing at first when I say transitioning visibility since you either have it or not, there are no intermediary values to gradually change. We don’t want to increment visibility though – we just want to delay it so the image doesn’t hide before our opacity transitions.
Delay the change in visibility by adding visibility 0s linear 0.2s
to the transition above so, the final product looks like this:
transition:opacity 0.3s ease-out, visibility 0s linear 0.2s
The 0s illustrates the lack of intermediary steps between visibility “hidden” and “visible” while 0.2s describes the delay.
This transition identifies the change of opacity over a 0.3-second time period and simultaneous change in visibility that occurs after 0.2 seconds.
Responsive using media query
Ok, basically done here. Only one media query to assure responsiveness for smaller screen sizes and the interactive menu component is complete!
What we have prior to taking into account responsiveness:
We need to do three things on smaller screens:
- make the divs block-level elements so they take up the entire width of a line
- adjust the font size of the h3s
- decrease the size of the images
Luckily we can do all three using a single media query for screen sizes 752px or less.
@media (max-width: 752px) {
#pasta-main div {
display: block;
}
#pasta-main h3 {
font-size: 50px;
line-height: 1.6em;
}
.pasta-img {
width: 338px;
}
}
And that’s a wrap! Thanks for sticking out for this one until the end, it was quite a challenge. Not only did you create some awesome effects and transitions, but you also learned the most important Italian of your life!
Use an interactive menu component like this on your own website to make it bellissimo.
For the full source code, visit the Github pasta repo. We’ll see ya on the next project, addio!