Responsive restaurant menu with filter
In another blog, I wrote about creating a restaurant menu using only HTML, and CSS. In this blog, we’ll create another restaurant menu page with filtering functionality. By filtering, we mean that, by default, all dishes on the menu are displayed, but you can click on a specific category to show only dishes that are in that category. For example, showing only breakfast items, or only lunch items, or only dinner items.
We also want the menu to be responsive. On large screens, we want our page to have a two-column layout like the following:
On smaller computer screens, the page should have a one column layout like the following:
On small screens, the item details should be below the item image like the following:
The HTML structure
We’ll create an elaborate HTML structure for this web page. We start with the following:
<!DOCTYPE html>
<html>
<head>
<title>Restaurant Menu with Filtering</title>
<link rel="stylesheet" href="assets/style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600&family=Great+Vibes:wght@400;600&family=Noto+Sans:wght@400;600&family=Playpen+Sans:wght@400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Icons" />
</head>
<body>
<nav>
<h2 class="logo">Eater's Delight</h2>
</nav>
<section class="menu">
<div class="menu-container">
<div class="menu-header">
<h2>Our menu</h2>
<p>Choose from our delightful menu items. We cater to all kinds of culinary taste. You may filter the menu items by type.</p>
</div>
<div class="menu-buttons">
<button type="button" class="menu-button active-button" id="All">All</button>
<button type="button" class="menu-button" id="breakfast">Breakfast</button>
<button type="button" class="menu-button" id="lunch">Lunch</button>
<button type="button" class="menu-button" id="dinner">Dinner</button>
</div>
<div class="menu-items">
<!-- Menu items go here -->
</div>
</div>
</section>
<script src="app.js"></script>
</body>
</html>
In the head
section, we refer to our external style sheet file. I want to use Google Fonts, and some Material Icons. That’s what the next four link
tags are for. For the Google Fonts, notice that we have specified different weights for some of the fonts.
In the body
section, we have a nav
element that houses the logo, in the form of an h2
element. Next up, we create a section
for the rest of the page. We give this section a class of menu
.
You’ll notice several HTML elements with class names, above. These are all hypothetical at the moment. We haven’t defined the style for these classes. But it helps to visualize in our heads what the distinct styling categories are.
Within the menu
section, we create a div
element with a class of menu-container
. As the class name suggests, this will serve as the container for the menu items. Inside it, we create another div
element with a class of menu-header
. This serves as the header of the menu, housing the h2
heading for the menu, and a brief message to the customers.
After this, we create another div
element to house the buttons that the user would eventually be able to click to filter the menu items. This div
element is given a class of menu-buttons
. It includes button
elements for filtering. The first one is “All”, to show all the menu items. This one is the default. The second button is to filter and show only the breakfast items. This is followed by other buttons for lunch, and dinner. Each button is given a unique id
, which will help in getting hold of the button object in JavaScript. All buttons are also given a class of menu-button
which will serve to provide a consistent styling. At any point, only one of these buttons can be in the “pressed” state. To indicate that, we’ll use a class named active-button
. By default, this class is assigned to the “All” button.
After the buttons, we have another div
with a class of menu-items
. This will house the menu items. It’ll have one div
element for each menu item. What will a menu item look like? Here’s what we’ll go with:
<div class="menu-item">
<div class="item-image">
<img src="assets/img/img1.jpeg" alt="food image">
</div>
<div class="item-details">
<h3 class="item-name">Omnivorous Delight</h3>
<h4 class="item-price">$13.99</h4>
<ul class="item-rating">
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star_outline</span></li>
</ul>
<p class="item-category">Categories: <span>Dinner</span></p>
</div>
</div>
We give the outer div
element a class of menu-item
. Inside it, we include an image of the food item. Notice that we’re using the alt
attribute, which is a good practice from an accessibility perspective. Next up, we have a div
element for the item details, with a class named item-details
. The item details include the item name, an item price, a rating, and the category to which the item belongs. For this blog, we’ll use categories for breakfast, lunch, and dinner.
For the rating, we are using an unordered list to hold the stars representing the rating. We are using star icons from the Material icon library inside span
elements. To display the stars, we need to use the class of material-icons
. To display a filled star, we need to type star
as the text inside the span
element. Similarly, we can display a hollow star using star_outline
, and a half filled star using star-half
.
Copy and paste the menu item so that you have nine menu items with a class of menu-items
inside the div
element with a class of menu-container
immediately after the div
element with a class of menu-buttons
. Give each item a different name, different image, price, description, and rating. The document structure should resemble the following:
<!DOCTYPE html>
<html>
<head>
<title>Restaurant Menu with Filtering</title>
<link rel="stylesheet" href="assets/style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Icons" />
</head>
<body>
<nav>
<h2 class="logo">Eater's Delight</h2>
</nav>
<section class="menu">
<div class="menu-container">
<div class="menu-header">
<h2>Our menu</h2>
<p>Choose from our delightful menu items. We cater to all kinds of culinary taste. You may filter the menu items by type.</p>
</div>
<div class="menu-buttons">
<button type="button" class="menu-button active-button" id="All">All</button>
<button type="button" class="menu-button" id="breakfast">Breakfast</button>
<button type="button" class="menu-button" id="lunch">Lunch</button>
<button type="button" class="menu-button" id="dinner">Dinner</button>
</div>
<div class="menu-items">
<div class="menu-item">
<!-- Details removed -->
</div>
<div class="menu-item">
<!-- Details removed -->
</div>
<div class="menu-item">
<!-- Details removed -->
</div>
<div class="menu-item">
<!-- Details removed -->
</div>
<div class="menu-item">
<!-- Details removed -->
</div>
<div class="menu-item">
<!-- Details removed -->
</div>
<div class="menu-item">
<!-- Details removed -->
</div>
<div class="menu-item">
<!-- Details removed -->
</div>
<div class="menu-item">
<!-- Details removed -->
</div>
</div>
</div>
</section>
<script src="app.js"></script>
</body>
</html>
At this point, the page looks really flat, with no styling. Let’s start to style the elements now.
Basic styling
First, let’s lay some ground work for the CSS styling:
*, ::after, ::before {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--Roboto: 'Roboto', 'Open Sans', sans-serif;
--Noto: 'Noto Sans', 'Open Sans', sans-serif;
--Monts: 'Montserrat', 'Open Sans', sans-serif;
--script: 'Playpen Sans', 'Open Sans', sans-serif;
--vibes: 'Great Vibes', 'Open Sans', sans-serif;
--bred: #AB3428;
--orange: #F49E4C;
--yellow: #F5EE9E;
--lightblue: #C4C6E7;
--darkblue: #3B8EA5;
--lightgray: #eeeeee;
--black: #000;
}
We start with a CSS reset. We select all the elements, and set the margins and padding to zero. We also set box-sizing to border-box so that borders are included in element sizes. These settings get rid of some browser-specific styling. Next, we define a few CSS variables for some Google fonts and color shades. Defining CSS variables is a useful approach in that if you want to change the color scheme or fonts used on your page later, all you need to do is to update the values in one place (the variable definitions).
Next up, some basic styling for the body
element:
body {
font-family: var(--Monts);
font-size: 16px;
line-height: 1.3;
background-color: var(--bred);
color: var(--lightgray);
}
We declare that our default font is Montserrat, 16 pixels. We set line-height
to 1.3 for comfortable reading (increased spacing between lines). We set the background and font color.
Styling the logo
Let’s give the logo a hand-writing touch:
.logo {
font-family: var(--vibes);
font-size: 2.5rem;
color: var(--yellow);
}
We select the Great Vibes font with a font size of 2.5 rem. We use a yellowish color for the logo, which contrasts well over the background color. But, the logo is a bit too close to the top and left edges. Let’s fix that:
nav {
padding: 1.5rem;
}
We apply a padding of 1.5 rem on all sides of the nav
section, which contains the logo.
Here’s what the top of the page looks like at this point:
Styling the menu header
Let’s style the menu header section:
.menu {
margin: 1.5rem 0;
}
.menu-item img {
width: 100%;
}
.menu-container {
margin: 0 auto;
max-width: 1300px;
padding: 0 1rem;
}
.menu-header {
text-align: center;
}
.menu-header h2 {
font-family: var(--script);
}
.menu-header p {
padding: 1.5rem 0;
}
The menu header at present is fairly close to the logo. To fix that, we add a bit of top and bottom margin.
Although the images aren’t technically part of the menu header, but our images are pretty large, and displaying them in their original size is pretty annoying. To fix that, we use the descendant selector .menu-item img
(any img
elements that are descendants of an element wiht a class of menu-item
), and set the width
equal to the parent container.
The menu items are right at the left edge of the screen. To fix that, we select the menu-container
class and set margin
property to 0 auto
, which centers the menu items horizontally. We set the max-width
property to 1300 pixels, so that the menu doesn’t occupy all of the width on large screens. We also add a bit of padding on the left and right.
To center the “Our Menu” message horizontally, we set text-align
to center
on the element with the class of menu-header
. We also change the font of that message to the Playpen Sans font, using the script
CSS variable. To separate the text after the “Our Menu” message, we adding a bit of top and bottom padding to the p
element descendant of the element with a class of menu-header
.
Here’s what the top of the page looks like now:
Now, let’s style the buttons:
.menu-buttons {
display: flex;
justify-content: center;
margin: 1.5rem 0;
}
.menu-button {
font-family: var(--Noto);
font-size: 1.1rem;
background: none;
border: none;
color: var(--lightgray);
margin: 0 1rem;
cursor: pointer;
}
.active-button {
color: var(--lightblue);
opacity: 0.8;
}
We select the div
element wrapping the buttons with a class selector .menu-buttons
. We use CSS Flexbox to layout the buttons neatly in a row. We configure Flexbox to center the buttons horizontally within the parent container. We also a bit of margin around the row of buttons on the top and bottom. We set the cursor
property for the menu-button
class to cursor
so that the user gets a bit of visual feedback that they are on an active element, when hovering over the buttons. At any given time, the user would be viewing one menu category: All, Breakfast, Lunch, or Dinner. The button corresponding to the currently selected category has an additional class of active-button
. To style the selected button, we give a distinctive color and opacity to the active-button
class.
Here’s what the top of the page looks like now:
Styling the menu items
Let’s style the menu items section now. We start with the following:
.menu-items {
margin: 2rem 0;
}
.menu-item {
border-radius: 4px;
background: var(--lightgray);
color: var(--black);
margin: 0.6rem 0;
box-shadow: 0 0 16px rgba(0, 0, 0, 0.75);
}
.menu-item img {
border-radius: 4px;
}
We use the menu-items
class selector and give it a bit of top and bottom margin. This sets it comfortably away from the menu header and the bottom of the page.
Next, we turn our attention to the individual item displays. We use the menu-item
class selector and start with rounded borders. We set a light gray background color, and black text color. By default, the individual menu items are quite close to each other. To set them comfrotably apart, we apply a bit of top and bottom margins. We also opt for a shadow beneath the menu item. The first zero in the box-shadow
statement is the x offset. A positive value for this attribute means that we want the shadow to appear to the right of the element (as if there was a light shining from its left hand side). A negative value means a shadow displaced towards the left (as if there was a light shining from its right hand side). A zero value means that the shadow falls right under the element, equally towards the left and right (as if there’s a light shining on it from the very front). Similarly, the second zero means that we want the shadow equally spreading above and below the element. We give the shadow a 16 pixels spread radius, and set its color to be black with a 75% opacity. Finally, we select the img
elements inside the menu item and give it a rounded border to match its parent element.
Here’s what the top of the page looks like now:
Styling the item details
Now, let’s style the item name, price, and description.
.item-details {
padding: 1rem;
}
.item-name {
font-family: var(--Roboto);
color: var(--darkblue)
}
.item-price {
padding: 0.8rem 0;
font-size: 1.3rem;
opacity: 0.8;
}
.item-rating {
list-style-type: none;
display: flex;
}
.item-category {
font-weight: 600;
}
.item-category span {
font-weight: 400;
}
The item name and description are too close to the edge. So, we give it a bit of padding all around with the item-details
class selector. We set the Roboto font for the item name, and give it a bluish color. We add a bit of padding on the top and bottom to the item price. We increase the font size for the item price a little bit and reduce the opacity. The rating stars were displayed vertically since they are in an unordered list. To set them up nicely horizontal, we call Flexbox to our rescue. Also, in order to get rid of the bullets in the list, we set list-style-type
to none
. We give a thicker stroke the text “Categories”, and a thinner stroke to the actual category.
Here’s what the item details look like now:
Adding some oomph
Let’s make the design a bit jazzy. Firstly, we’ll give a bit of distinctive styling to a menu button when the user hovers the mouse over it. Secondly, we’ll have the menu item photos fade in, i.e., appear slowly on the screen when the page loads. The same effect will be visible when the user filters the items, but that filtering remains for us to implement.
Button animation
Let’s show a line underneath the menu button that a user hovers the mouse over. Also, let’s reduce the opacity of the button text while the user is hovering over it.
.menu-button::after {
content: '';
margin-top: 0.5rem;
display: block;
width: 0;
height: 2px;
background: var(--lightgray);
transition: width 0.4s ease-out;
}
.menu-button:hover::after {
width: 100%;
}
.menu-button:hover {
opacity: 0.8;
}
We create a pseudo-element (::after
) and set its content
to blank. We give this element a bit of a top margin so that it is a bit displaced from the corresponding menu-button
element. We set its width
to 0, so that it isn’t visible. Don’t worry, we’ll define the condition when it should actually be visible. Since we want to give it a width, it needs to be a block
element, which is why we set the display
property. We give this a height
of 2 pixels. We give it a light gray background color, and finally assign a value to its transition
property. This last setting says that when the width of this pseudo-element changes, it should change over 0.4 seconds, and this change in width should be fast in the beginning, and slower towards the end (the ease-out
keyword). Where does this change in width come from? That is defined next in the :hover
pseudo selector. When a user hovers over an element with a class of menu-button::after
, the width of this pesudo-element should increase to 100% of its parent, which is the same as the menu-button
element. Finally, we also decrease the opacity of the text of the menu button when a user hovers the mouse over it.
With these changes, when you hover the mouse over a menu button, you should see a line appear to grow under it from the left until it is equal in width to the menu button text. When you move the mouse pointer away from this button, you’ll see this line gradually disappear. Also, the text will appear to dim when you hover over a menu button.
Here’s a snapshot of a menu button that a user is hovering a mouse over:
Menu item animation
Next up, let’s animate the menu items.
.menu-item {
/* Existing style specification */
animation: appear 2s;
}
@keyframes appear {
from{
opacity:0;
}
to {
opacity: 1;
}
}
We define a new animation named appear
. We specify the animation to go from
an opacity
of 0
, i.e., not visible, to an opacity
of 1
, i.e., visible in full glory. We connect this animation to the menu-item
class specification and set it to appear over a period of 2 seconds. Now, when the page loads, you’ll see the menu items appear in view slowly over a period of 2 seconds.
Filtering the results
Next up, let’s enable filtering the items when the menu category buttons are clicked.
const buttons = document.querySelectorAll('.menu-button')
const items = document.querySelectorAll('.menu-item')
let selectedButton = 'All'
buttons.forEach( (btn) => {
btn.addEventListener('click', () => {
clearButtons()
selectedButton = btn.id
btn.classList.add('active-button')
showItems()
})
})
function clearButtons() {
buttons.forEach( (btn) => btn.classList.remove('active-button'))
}
function showItems() {
items.forEach( (item) => {
const cat = item.querySelector('p span').innerText
if ((selectedButton === 'All') || (cat === selectedButton)) {
item.style.display = 'grid';
}
else {
item.style.display = 'none';
}
})
}
We need access to the elements for the buttons and the menu items. To achieve that, we use the document.querySelectorAll()
method and store all the buttons in the variable buttons
and all the menu items in the variable items
. We also define a variable named selectedButton
to hold the currently selected item category. The default active category is “All”, that’s why we initialized selectedButton
to 'All'
.
Next up, we associate mouse click event handlers with all of the button elements. We iterate over the button
collection using the Array.forEach()
method. For each button object, we call the addEventListener()
method to associate an anonymous function as the event listener.
What needs to happen on a menu button click is that the currently active selection should be changed. We can do this by:
- removing the
active-button
class from all buttons - adding this class to the button just clicked
- update the display to hide any menu items that don’t have the category that we clicked on
In the click event handler anonymous function we first call a clearButton()
function that removes the class active-button
from all buttons. Then, we update the global variable selectedButton
to the button we just clicked. Refer to the markup for the buttons: <button type=”button” class=”menu-button” id=”Breakfast”>Breakfast</button>
. The id
attribute holds the category text. That’s why we set selectedButton
to btn.id
. Next, we add the active-button
class the button that was clicked. Finally, we call the showItems()
function.
The showItems()
function relies on the selectedButton
variable to hold the currently selected category. It was intialized correctly, and our mouse click event handler updates it correctly, so that base is covered. What we do inside showItems()
is to iterate over the items
collection and either set the display
property of an item to none
to hide it, or to grid
to make it visible.
How do we get the item category for an item as we iterate over the items collection? Recall from our markup that this is what an item description looks like:
<div class="item-details">
<h3 class="item-name">Hot Dog</h3>
<!-- <div class="line"></div> -->
<h4 class="item-price">$3.50</h4>
<ul class="item-rating">
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star_half</span></li>
<li><span class="material-icons">star_outline</span></li>
</ul>
<p class="item-category">Categories: <span>Dinner</span></p>
</div>
So, one way to access an item’s category is with the element selector p span
. We can make it more specific, if we want, but I’ll keep it like this. We use the querySelector()
method with an argument of ‘p span’
to access the span
element with the item category, then use the innerText
property to access the category text itself. We compare this text agains the value of the selectedButton
variable. If the value ofselectedButton
is 'All'
or it matches that of the current item’s category, then we need to display this item. Note how we’ve put the condition that checks for 'All'
first in the if
statement. This helps speed up comparisons. If this comparison evaluates to true
, then the language runtime will skip the other comparison since the or
of anything with a true
is true.
Responsive design
Now that our filtering is complete, let’s focus on making the design responsive. If you are on a large screen, I’m sure you share my feelings that the item displays are too big.
Let’s reduce the item height and image size for screens that are at least 600 pixels wide. Also, let’s put the item description and item image side by side. For that, let’s append the following to the CSS file we have so far:
@media only screen and (min-width: 600px) {
.menu-item {
display: grid;
grid-template-columns: 25% auto;
align-items: center;
padding: 1rem;
column-gap: 1rem;
}
.item-details {
padding: 0;
}
}
We use a media query, and change the styles for two classes. First, we use CSS Grid to style the menu item. We configure CSS Grid to set up a two column layout by setting the grid-template-columns
property to 25% auto
. The presence of two values, here, indicates that we want a two-column display. The fist value indicates the width of the first column (25% of its parent), and the second value (auto
) indicates that the second column should occupy all remaining width of the parent element.
The item description would look better if it were vertically centered within an item. For that, we set the align-items
property to center
. Furthermore, we give a bit of padding on all sides of the menu-item
container, and set a bit of a column gap to set the item description apart from the item image. Now, that we’ve added a column gap, we no longer need the padding around that we previously had on the item-details
class, so we set it to zero.
Here’s what our page looks like on a screen with at least 600 pixels width, such as a tablet:
For screens smaller than that, such as cellphones, here’s what a menu item looks like (this is the style that we have developed previously):
For desktop or laptop screens with a greater resolution, we can afford to leave a bit of gap on either side of the navigation portion. Append the following to the CSS file we have so far:
@media only screen and (min-width: 768px) {
.menu-header p {
width: 75%;
margin: 0 auto;
}
}
Here’s what our navigation portion looks on a scree more than 768 pixels wide (on the right), and less than 768 pixels, but greater than 600 pixels (on the left):
Finally, for even larger screens, we’d like to squeeze the navigation portion’s width even further, and use a multi-column layout for the menu items. Here’s what we can append to the CSS file:
@media only screen and (min-width: 992px) {
.menu-header p {
width: 55%;
}
.menu-items {
display: grid;
grid-template-columns: repeat(2, 1fr);
column-gap: 2rem;
}
}
The menu-header
part should be self-explanatory by now. For the menu-items
class, we select CSS Grid, and ask it to layout the menu items using the repeat
function. The first argument represents the number of columns to produce, i.e., 2. The second argument says that all columns should have equal width. The repeat
function suggests that the number of rows should be auto-adjusted. We also specify a column gap to set the menu items reasonably apart.
Here’s what our page looks like on a large screen:
That’s all folks!
That concludes this blog. Play with the code to practice your skills. If you are going for more of a full-stack thing, add a backend and fetch the menu items from an API, rather than hard-coding them on the frontend. Try adding a shopping cart, authentication, a feedback form, a rating system that uses actual customer-provided ratings.
You may download the source code from this github repository. You may also copy-paste the files given below. First, the HTML:
<!DOCTYPE html>
<html>
<head>
<title>Restaurant Menu with Filtering</title>
<link rel="stylesheet" href="assets/style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600&family=Great+Vibes:wght@400;600&family=Noto+Sans:wght@400;600&family=Playpen+Sans:wght@400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Icons" />
</head>
<body>
<nav>
<h2 class="logo">Eater's Delight</h2>
</nav>
<section class="menu">
<div class="menu-container">
<div class="menu-header">
<h2>Our menu</h2>
<p>Choose from our delightful menu items. We cater to all kinds of culinary taste. You may filter the menu items by type.</p>
</div>
<div class="menu-buttons">
<button type="button" class="menu-button active-button" id="All">All</button>
<button type="button" class="menu-button" id="Breakfast">Breakfast</button>
<button type="button" class="menu-button" id="Lunch">Lunch</button>
<button type="button" class="menu-button" id="Dinner">Dinner</button>
</div>
<div class="menu-items">
<div class="menu-item">
<div class="item-image">
<img src="assets/img/img1.jpeg" alt="food image">
</div>
<div class="item-details">
<h3 class="item-name">Omnivorous Delight</h3>
<!-- <div class="line"></div> -->
<h4 class="item-price">$13.99</h4>
<ul class="item-rating">
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star_outline</span></li>
</ul>
<p class="item-category">Categories: <span>Dinner</span></p>
</div>
</div>
<div class="menu-item">
<div class="item-image">
<img src="assets/img/img2.jpeg" alt="food image">
</div>
<div class="item-details">
<h3 class="item-name">Hot Dog</h3>
<!-- <div class="line"></div> -->
<h4 class="item-price">$3.50</h4>
<ul class="item-rating">
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star_half</span></li>
<li><span class="material-icons">star_outline</span></li>
</ul>
<p class="item-category">Categories: <span>Dinner</span></p>
</div>
</div>
<div class="menu-item">
<div class="item-image">
<img src="assets/img/img3.jpeg" alt="food image">
</div>
<div class="item-details">
<h3 class="item-name">Double Trouble</h3>
<!-- <div class="line"></div> -->
<h4 class="item-price">$7.50</h4>
<ul class="item-rating">
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star_half</span></li>
</ul>
<p class="item-category">Categories: <span>Lunch</span></p>
</div>
</div>
<div class="menu-item">
<div class="item-image">
<img src="assets/img/img4.jpeg" alt="food image">
</div>
<div class="item-details">
<h3 class="item-name">Vegetable Salad</h3>
<!-- <div class="line"></div> -->
<h4 class="item-price">$3.99</h4>
<ul class="item-rating">
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star_half</span></li>
<li><span class="material-icons">star_outline</span></li>
</ul>
<p class="item-category">Categories: <span>Lunch</span></p>
</div>
</div>
<div class="menu-item">
<div class="item-image">
<img src="assets/img/img5.jpeg" alt="food image">
</div>
<div class="item-details">
<h3 class="item-name">Champion's breakfast</h3>
<!-- <div class="line"></div> -->
<h4 class="item-price">$9.50</h4>
<ul class="item-rating">
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star_outline</span></li>
</ul>
<p class="item-category">Categories: <span>Breakfast</span></p>
</div>
</div>
<div class="menu-item">
<div class="item-image">
<img src="assets/img/img6.jpeg" alt="food image">
</div>
<div class="item-details">
<h3 class="item-name">Cylinder Salad</h3>
<!-- <div class="line"></div> -->
<h4 class="item-price">$4.99</h4>
<ul class="item-rating">
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star_half</span></li>
<li><span class="material-icons">star_outline</span></li>
</ul>
<p class="item-category">Categories: <span>Lunch</span></p>
</div>
</div>
<div class="menu-item">
<div class="item-image">
<img src="assets/img/img7.jpeg" alt="food image">
</div>
<div class="item-details">
<h3 class="item-name">Chicken Poppers</h3>
<!-- <div class="line"></div> -->
<h4 class="item-price">$6.99</h4>
<ul class="item-rating">
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star_half</span></li>
</ul>
<p class="item-category">Categories: <span>Breakfast</span></p>
</div>
</div>
<div class="menu-item">
<div class="item-image">
<img src="assets/img/img8.jpeg" alt="food image">
</div>
<div class="item-details">
<h3 class="item-name">Veggie Patties</h3>
<!-- <div class="line"></div> -->
<h4 class="item-price">$1.99</h4>
<ul class="item-rating">
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star_outline</span></li>
</ul>
<p class="item-category">Categories: <span>Breakfast</span></p>
</div>
</div>
<div class="menu-item">
<div class="item-image">
<img src="assets/img/img9.jpeg" alt="food image">
</div>
<div class="item-details">
<h3 class="item-name">Seafood Salad</h3>
<!-- <div class="line"></div> -->
<h4 class="item-price">$13.99</h4>
<ul class="item-rating">
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star</span></li>
<li><span class="material-icons">star_outline</span></li>
<li><span class="material-icons">star_outline</span></li>
</ul>
<p class="item-category">Categories: <span>Dinner</span></p>
</div>
</div>
</div>
</div>
</section>
<script src="app.js"></script>
</body>
</html>
The CSS:
*, ::after, ::before {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--Roboto: 'Roboto', 'Open Sans', sans-serif;
--Noto: 'Noto Sans', 'Open Sans', sans-serif;
--Monts: 'Montserrat', 'Open Sans', sans-serif;
--script: 'Playpen Sans', 'Open Sans', sans-serif;
--vibes: 'Great Vibes', 'Open Sans', sans-serif;
--bred: #AB3428;
--orange: #F49E4C;
--yellow: #F5EE9E;
--lightblue: #C4C6E7;
--darkblue: #3B8EA5;
--lightgray: #eeeeee;
--black: #000;
}
body {
font-family: var(--Monts);
font-size: 16px;
line-height: 1.3;
background-color: var(--bred);
color: var(--lightgray);
}
.logo {
font-family: var(--vibes);
font-size: 2.5rem;
color: var(--yellow);
}
nav {
padding: 1.5rem;
}
.menu {
margin: 1.5rem 0;
}
.menu-container {
margin: 0 auto;
max-width: 1300px;
padding: 0 1rem;
}
.menu-item img {
width: 100%;
}
.menu-header {
text-align: center;
}
.menu-header h2 {
font-family: var(--script);
}
.menu-header p {
padding: 1.5rem 0;
}
.menu-buttons {
display: flex;
justify-content: center;
margin: 1.5rem 0;
}
.menu-button {
font-family: var(--Noto);
font-size: 1.1rem;
background: none;
border: none;
color: var(--lightgray);
margin: 0 1rem;
cursor: pointer;
}
.menu-button::after {
content: '';
margin-top: 0.5rem;
display: block;
width: 0;
height: 2px;
background: var(--lightgray);
transition: width 0.4s ease-out;
}
.menu-button:hover::after {
width: 100%;
}
.menu-button:hover {
opacity: 0.8;
}
.active-button {
color: var(--lightblue);
opacity: 0.8;
}
.menu-items {
margin: 2rem 0;
}
.menu-item {
border-radius: 4px;
overflow: hidden;
background: var(--lightgray);
color: var(--black);
margin: 0.6rem 0;
box-shadow: 0 0 16px rgba(0, 0, 0, 0.75);
animation: appear 2s;
}
@keyframes appear {
from{
opacity:0;
}
to {
opacity:1;
}
}
.menu-item img {
border-radius: 4px;
}
.item-details {
padding: 1rem;
}
.item-name {
font-family: var(--Roboto);
color: var(--darkblue)
}
.item-price {
padding: 0.8rem 0;
font-size: 1.3rem;
opacity: 0.8;
}
.item-rating {
list-style-type: none;
display: flex;
}
.item-category {
font-weight: 600;
}
.item-category span {
font-weight: 400;
}
@media only screen and (min-width: 600px) {
.menu-item {
display: grid;
grid-template-columns: 25% auto;
align-items: center;
padding: 1rem;
column-gap: 1rem;
}
.item-details {
padding: 0;
}
}
@media only screen and (min-width: 768px) {
.menu-header p {
width: 75%;
margin: 0 auto;
}
}
@media only screen and (min-width: 992px) {
.menu-header p {
width: 55%;
}
.menu-items {
display: grid;
grid-template-columns: repeat(2, 1fr);
column-gap: 2rem;
}
}
The JavaScript:
const buttons = document.querySelectorAll('.menu-button')
const items = document.querySelectorAll('.menu-item')
let selectedButton = 'All'
buttons.forEach( (btn) => {
btn.addEventListener('click', () => {
clearButtons()
selectedButton = btn.id
btn.classList.add('active-button')
showItems()
})
})
function clearButtons() {
buttons.forEach( (btn) => btn.classList.remove('active-button'))
}
function showItems() {
items.forEach( (item) => {
const cat = item.querySelector('p span').innerText
if ((selectedButton === 'All') || (cat === selectedButton)) {
item.style.display = 'grid';
}
else {
item.style.display = 'none';
}
})
}