In today’s tutorial, we’re going to build a top navigation bar with HTML and CSS. We will look at two different ways of building this navbar, one way with flexbox, and the other with CSS grid.
It’ll be a good way to compare the differences between the two approaches. And you can see which method that you like better.
Here’s what the finished navigation will look like:

On desktop, all the links will be on the same row, with Home on the left, and the other links on the right.

Then on mobile, we will have Home on the top row and the other links on the bottom row. And the links will be centered on the page.
Let’s get started by setting up our files.
Set up files
To create our navigation we’re going to create an index.html
file and a style.scss
file, which we will compile to style.css
using the VS Code Live Sass extension.
In our <head>
section we will be adding a <link>
element to load the style.css
file.
Now let’s get started building that navigation bar!
Create HTML markup
In our index.html
file, let’s first create the HTML markup for the navigation. I try to use semantic HTML tags as much as possible, to avoid using just divs for everything.
We’re going to create a <nav>
element, which will contain all our navigation links. Then we will put the links in an unordered list <ul>
element like this:
<nav>
<ul>
<li><a href="#">Home</a></li>
<li><a href="#">About</a></li>
<li><a href="#">Blog</a></li>
<li><a href="#">Contact</a></li>
</ul>
</nav>
This will be the basic navigation markup to start with. We’ll have to do some tweaks as we build the flexbox and grid versions, but you can start by just creating two copies of this <nav>
element.
If we load the index.html
in the browser, it’ll look pretty basic.

But not to worry! Let’s start adding our styles in the style.scss
file.
Start adding global styles and media queries
First off, I’ll add some global styles that I always try to use:
html {
font-size: 100%;
box-sizing: border-box;
}
body {
margin: 0px;
padding: 0px;
min-height: 100vh;
}
*, *::before, *::after {
box-sizing: inherit;
}
In addition, let’s make the colors a bit more interesting. In the <body>
tag we’ll set background-color: #1b1b1b
, and make sure all elements and <a>
links are white with color: #ffffff
so it’s dark themed.
Then for the <ul>
element in the <nav>
, : add list-style-type: none
to hide the bullets, and set margin and padding to 0px
.
Let’s also add some padding to the <nav>
element with padding: 10px
, and increase the padding for tablet and desktop styles using a media query:
nav {
padding: 10px;
@media (min-width: 600px){
padding: 20px;
}
}
Things are looking better! Instead of the default serif font, I’d like to use a Google Font, Open Sans.
We can import the font family into our style.scss
by adding this line to the very top:
@import url('https://fonts.googleapis.com/css?family=Open+Sans&display=swap');
Now in our styles for the <body>
element, we can set font-family: 'Open Sans', Arial, Helvetica, sans-serif
to load Open Sans or a backup font.
Add link styles with a linear-gradient bottom border
Let’s also make the link styles a bit more interesting.
We’re going to remove the default underline by setting text-decoration: none
to the <a>
elements. And I’d like to replace it with a thicker pink underline, using border-bottom
. I’m also going to create a gradient underline on hover to a pink to purple color.
For colors, I like creating Sass variables. At the top of the style.scss
file under the @import
statement, add the following variables:
$linkPink: rgb(216, 29, 216);
$linkPurple: rgb(98, 16, 250);
Using variables will make it simpler when we’re adding the colors to our other styles.
Now, to create the thicker underline, we can use a pseudo-element of the <a>
tag to have a bottom border.
Here’s the code for that:
a {
position: relative;
text-decoration: none;
}
a::before {
content: "";
position: absolute;
height: 3px;
right: 0px;
bottom: -3px;
left: 0px;
background-color: $linkPink;
}
What’s going on here is that we’re making the ::before
pseudo-element 3px tall and with the pink background. To make it positioned at the bottom, we need to set it to position: absolute
and make sure the <a>
element has position: relative
so it doesn’t fly up the page. (I explain more about this phenomenon here.)
Then, to position the pseudo-element under the text, we’re setting bottom: -3px
so there’s a bit of space between the link text and the line itself. And to make the line extend the full width of the link, we’re using left: 0px
and right: 0px
.

Now the links look a bit nicer!
Linear gradient hover style
Let’s go one more level of fancy and add a cool hover effect. What we want is for the underline to change to a gradient of pink to purple, and the link text to turn from white to pink when you hover over a link.
To do this, we’ll use the a:hover
selector with the following styles:
a:hover {
color: $linkPink;
&::before {
background: linear-gradient(to right, $linkPink 40%, $linkPurple 75%);
}
}
For the ::before
pseudo-element, the background will now change to a linear-gradient
. In the gradient function, we’re telling it to change from pink to purple as you move from left to right.
And we’re controlling where the gradient changes with the percentages. We want the first pink color to go from 0% (far left) to 40% width. And we want the second purple color to go from 100% (far right) to 75% width. Then there will be a gradient between 40% and 75% of the width of the element.
We’ve also changed the link text color to pink on hover.
So now when you hover over a link it will look like this:

Looks more fancy right?
Ok, now we’ve finished the global styles that we’ll be using for both the flexbox and grid navigation bars.
Let’s start writing the styles for the flexbox navigation.
Flexbox navigation
Divide links into flex child groups
I usually start by writing the desktop styles first, and then the mobile. Because desktop usually is more complicated with multiple columns, whereas on mobile things tend to be stacked in one column. But you can write your styles in whichever order makes sense for you.
In our desktop navigation, we want the Home link to be on the left, and the other three secondary links together on the right.
To make this happen with flexbox, we will need to divide the links into two groups: one for links on the left, and the other for links on the right. Both groups will then be flex child elements of the flex parent.
Going back to our HTML markup, we want the <nav>
element to be the flex parent. So let’s move the Home link out of the <ul>
and make it a direct child of the <nav>
element.
Then the rest of the links will stay inside the <ul>
.
Let’s also add a “flexbox” class to the <nav>
to keep our flexbox styles separate from our grid styles later on.
The updated markup will look like this:
<nav class="flexbox">
<a href="#">Home</a>
<ul>
<li><a href="#">About</a></li>
<li><a href="#">Blog</a></li>
<li><a href="#">Contact</a></li>
</ul>
</nav>
Add flexbox styles for desktop
Now, in our navigation, we want to turn on flexbox by setting the nav.flexbox
element to be display: flex
. Then we’ll also add the rule justify-content: space-between
to put the first group on the left and the other group on the right.
Make the <ul>
another flex parent with display: flex
and use justify-content: flex-end
to align the links to the right. And we’ll add align-items: center;
to vertically center the flex child elements.

Looking better!
Now, we want the three secondary links to also be next to each other in a flexbox arrangement. So we’ll make the <ul>
element a flex parent to the three links, and also set align-items: center
. We also want to add space between the links, so we’ll add margin-right: 20px
but make it 0px for the :last-child
selector. This ensures that the last link won’t have extra space.
Our desktop styles will now look like this:
nav.flexbox {
display: flex;
justify-content: space-between;
align-items: center;
ul {
display: flex;
align-items: center;
}
ul li {
margin-right: 20px;
&:last-child {
margin-right: 0px;
}
}
}
And the navigation bar will look like this:

Add flexbox styles for mobile
Now let’s add mobile styles. First off, let’s center the flex content on mobile.
Right now on mobile, the Home link is aligned to the left because of the justify-content: space-between
rule for the flex parent element.
We can center it by moving that style rule into a media query for larger widths. Then setting justify-content
to center
for mobile.
Next, we want the three secondary links to wrap to a new row, so let’s add flex-wrap: wrap
to the nav.flexbox
selector to enable wrapping.
The styles should then look like this:
nav.flexbox {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
@media (min-width: 600px){
justify-content: space-between;
}
}
And to force the wrapping, we’re going to make the <ul>
flex child take up 100% of the width, so it can only fit by moving under the Home link.
I like using helper classes, so we’re adding a class of “fullwidth” to the <ul>
element, and creating some style rules for the .fullwidth
selector in our nav.flexbox
rule.
First we’ll set flex: 1 0 100%
. This flex
property is shorthand for three flex properties: flex-grow
, flex-shrink
, flex-basis
.
We’re allowing it to grow if necessary with flex-grow
of 1. And we’re not allowing it to shrink with flex-shrink
of 0. Then we’re setting the default width to 100% using flex-basis
.
We’re also going to center the links in the flex parent on mobile, so we’ll add justify-content: center
.
However, we don’t want the secondary links to be fullwidth and centered for tablet or desktop. For larger device widths, we want the flex
property to be 0 1 auto
so it won’t grow, it can shrink if necessary, and the default width will be whatever the natural width of the content is.
Let’s add a media query for the desktop styles:
.fullwidth {
flex: 1 0 100%;
justify-content: center;
@media (min-width: 600px){
flex: 0 1 auto;
}
}
I could have also written a max-width
media query to target those first style rules only for mobile. But I try to use mobile style rules as the default, and then progressively adding larger min-width
media queries to avoid conflicts.
It’s really up to you which kind of media queries you use, but I recommend picking one type (min or max) to use most of the time.
Also, for the three secondary links, I’d like to set their widths so the “Blog” text is centered under the “Home” link. Right now it’s centered based on the total width of the second row links. But because “Contact” is a bit longer than “About,” the center of the row is slightly to the right of the center of the “Blog” text.

To fix this, we’ll make each flex child column in the second row all the same static width of 100px. So we’ll set a flex
property to 0 0 100px
and center the text. We only want those rules for mobile, so we’ll cancel them out for desktop by setting flex
to 0 0 auto
.
Let’s also reduce the space between items for mobile to 10px, and it’ll stay at 20px for desktop widths.
Here’s what the code for all that will look like:
ul li {
flex: 0 0 100px;
margin-right: 10px;
text-align: center;
@media (min-width: 600px){
flex: 0 0 auto;
margin-right: 20px;
}
&:last-child {
margin-right: 0px;
}
}
The last tweak we’ll do is adding some space between the first Home link and the three secondary links with a margin-bottom
for mobile. We’ll write a style rule targeting the <a>
element that’s a direct child of the nav.flexbox
parent, so only the Home link will be affected:
nav.flexbox {
> a {
margin-bottom: 10px;
@media (min-width: 600px){
margin-bottom: 0px;
}
}
}
Here’s what the mobile navigation will look like, with the Firefox flexbox inspector turned on, so you can see the lines for each column:

And that’s it for the flexbox navigation! Let’s now move on to building this out using only CSS grid.
CSS grid method
One of the main differences between flexbox and grid is that with flexbox, the arrangement of columns and rows is determined by the flex properties set on the flex child elements in the flex
property, and whether the flex parent is set to wrap or not. So you could say that flexbox follows a “content-first” approach.
With CSS grid, it’s a “grid-first” approach. You design your grid template (meaning the columns and rows) using grid properties in the parent element. And you can individually control where in that grid template the grid child elements are placed.
Due to this individual level of control, in our CSS grid navigation bar, we can put all the links including the first Home link, in the same parent <ul>
.
<nav class="grid">
<ul>
<li><a href="#">Home</a></li>
<li><a href="#">About</a></li>
<li><a href="#">Blog</a></li>
<li><a href="#">Contact</a></li>
</ul>
</nav>
Add grid styles for mobile
Let’s change it up with our grid navigation and start by writing the mobile styles first!
On mobile, we want the Home link to be on the first row, taking up 100% of the width. Then we want the three secondary links to be on a second row, and centered.
In our <ul>
grid parent element, we will turn on grid by setting display: grid
.
Now, let’s design our grid template. Since there will be three links on the second row, let’s create a three-column template.
I’d like to make the columns all the same width, but I don’t want them to be too wide. So let’s set grid-template-columns: repeat(3, 100px)
on the <ul>
grid parent.
We’re using the repeat()
function to create three columns of 100px width.
Another useful feature of CSS grid is the ability to create gutters (space) between grid items. So instead of having to add a margin to the grid children like in flexbox, we can simply set grip-gap: 10px
.
We can also center the entire grid on the page with justify-content: center
, and center the text in each column with justify-items: center
.
When we turn on the Firefox grid inspector (it’s really awesome, if you haven’t tried it, by the way) the grid will look like this:

We can clearly see our centered three column grid, but the Home link needs to be on its own row. We want it to take up three columns of width.
To do this, we’ll use a helper class again, adding a class of “fullwidth” to the Home <li>
element.
In our styles, we’ll give the .fullwidth
selector a style rule of grid-column: 1 / 4
. This will make that grid child column begin at the first vertical grid line, and extend up to the fourth gridline at the very end.
This is what it will look like, and we’ve turned on line numbers so you can see how they are numbered:

Add grid styles for desktop
For desktop, we want all the links to be on one row. And again, we want the Home link to be on the left, and then the three other links to be on the right side.
I’d also like to make each of the last three columns to be the size of the content it contains. This will make sure there’s the same amount of space between the links.
All this means that we’ll need to change the grid template for larger device widths, using a media query.
So what should we change the template to? We want all links on the same row, so we’ll want a four-column grid.
To take just one step in the right direction, let’s start off by setting changing the grid-template-columns
property from a repeat(3, 100px)
to repeat(4, 100px)
on the grid parent for widths greater than 600px.
This will create four columns, and then we can continue tweaking it.
Also, let’s update the .fullwidth
styles so it only takes up one column on desktop, by setting grid-column: 1 / 2
. This will make that first Home column only extend from grid line 1 to grid line 2.
Here’s what we have so far:

The links are in four columns, but since they’re all 100px wide they’re just centered on the page.
To make the first Home link all the way to the left, we need to make the grid itself take up 100% of the width of the page. And to do that, we can make just that first column in the grid template take up any available space.
So changing grid-template-columns
to 1fr repeat(3, 100px)
will leave the three secondary links at 100px wide. And the first column will take up the rest of the space.
Let’s see how that looks:

Getting there! However, to make the last three columns only as wide as their content, we use the fit-content()
function. If we set the last three columns to fit-content(50px)
then the columns will individually size to fit the content.
So our grid-template-columns
property for desktop will now be 1fr repeat(3, fit-content(50px))
.

The column widths look good!
Now let’s work on aligning the link text in the columns. Right now they’re centered due to the justify-items: center
rule that we used for mobile.
Let’s change that for desktop to justify-items: end
to align the links to the right. Then we can align just the Home link to the left by setting justify-self: start
for the .fullwidth
selector.
Now we can see the finished desktop navigation for grid, and it matches the flexbox navigation:

Flexbox or Grid?
In building this navigation, the flexbox styles took 46 lines of code in our style.scss
to write, and the HTML markup was a bit more complicated.
The grid styles took a slim 24 lines of code to write! A lot of the difference was because we had to add margin-bottom
and margin-right
style rules to add space between items on flexbox. And in grid that was all taken care of with the grid-gap
property.
I personally do really like the lightweight code needed for CSS grid. But flexbox is still definitely a good option if you’re more comfortable with flexbox!
You can see the source code here on Codepen.