How to Create a Fixed Header Which Animates on Page Scroll
In this tutorial, we’ll learn how to create a pattern seen on many websites these days: a fixed header which animates to a less obtrusive state as we scroll down the page. We’ll start with the basic structure, then get things working using CSS and pure JavaScript. Before closing, we’ll briefly cover how we can optimize our code as well as discussing challenges present when applying this kind of pattern to touch devices.
To get an idea of what we’re going to build, here’s the demo (you may prefer fullscreen view):
HTML Markup
We’ll start this exercise with the following markup–a header, containing a <nav>
and a couple of other nested elements:
1 |
<header> |
2 |
<nav> |
3 |
<h1> |
4 |
<a href="" class="logo">Logo</a> |
5 |
</h1> |
6 |
<ul> |
7 |
<li> |
8 |
<a href="">About</a> |
9 |
</li> |
10 |
<li> |
11 |
<a href="">Services</a> |
12 |
</li> |
13 |
<li> |
14 |
<a href="">Portfolio</a> |
15 |
</li> |
16 |
<li> |
17 |
<a href="">Contact</a> |
18 |
</li> |
19 |
</ul> |
20 |
<button class="toggle-menu" aria-label="Responsive Navigation Menu">☰</button> |
21 |
</nav> |
22 |
</header> |
23 |
|
24 |
<main> |
25 |
<!-- content here --> |
26 |
</main> |
The nav
element, which is part of the header, contains three elements; the logo, the main menu, and a placeholder button to trigger a responsive menu (below 1061px).
Note: If you click on this button, nothing much will happen. Creating the responsive menu is beyond the scope of this tutorial.
Initial CSS Styles
Now let’s have a look at some CSS styles to get things moving:
1 |
header { |
2 |
position: fixed; |
3 |
top: 0; |
4 |
width: 100%; |
5 |
padding: 20px; |
6 |
box-sizing: border-box; |
7 |
background: #DD3543; |
8 |
}
|
9 |
|
10 |
nav { |
11 |
display: flex; |
12 |
align-items: flex-end; |
13 |
justify-content: space-between; |
14 |
transition: align-items .2s; |
15 |
}
|
16 |
|
17 |
.logo { |
18 |
font-size: 2rem; |
19 |
display: inline-block; |
20 |
padding: 20px 30px; |
21 |
background: #F35B66; |
22 |
color: #fff; |
23 |
margin: 50px 0 0 50px; |
24 |
transition: all .2s; |
25 |
}
|
26 |
|
27 |
ul { |
28 |
display: flex; |
29 |
margin: 50px 50px 0 0; |
30 |
padding: 0; |
31 |
transition: margin .2s; |
32 |
}
|
33 |
|
34 |
li:not(:last-child) { |
35 |
margin-right: 20px; |
36 |
}
|
37 |
|
38 |
li a { |
39 |
display: block; |
40 |
padding: 10px 20px; |
41 |
}
|
42 |
|
43 |
.toggle-menu { |
44 |
display: none; |
45 |
font-size: 2rem; |
46 |
color: #fff; |
47 |
margin: 10px 10px 0 0; |
48 |
transition: margin .2s; |
49 |
}
|
50 |
|
51 |
main { |
52 |
display: block; |
53 |
padding: 0 20px; |
54 |
}
|
Here’s a brief explanation of the most important rules here:
- The
header
element is a fixed positioned element. - We use flexbox to layout the
nav
element. - The logo has
margin-top: 50px
andmargin-left: 50px
. Additionally, we give itpadding: 20px 30px
. - The main menu mirrors the logo, with
margin-top: 50px
andmargin-right: 50px
. - The responsive link button is hidden. It becomes visible when the viewport width is less than 1061px. Moreover, we set its top and right margins to 10px.
- We add the
transition
property to the elements whose property values will change in the future. In this way, we achieve a smooth transition effect between the initial state and the final state.
With these rules in place, the header looks like this:



Animating the Header
So far we’ve built the basic structure of our header. It’s now time to discuss the next steps:
- The
main
element should be positioned right underneath the header. Remember that the header haspositioned: fixed
, and it’s therefore positioned on top of themain
element. - The header should be animated as we scroll down the page.
To solve the first task, we add a padding-top
property to the main
element. The value of this property should be equal to the header’s height. In our case, we haven’t specified a fixed height for our header, so we’ll use some JavaScript to calculate it, and then add the corresponding padding to the main
element.
To solve the second task, we’ll do the following:
- Retrieve the number of pixels that the document has already been scrolled vertically.
- If this number is greater than 150px, we assign the
scroll
class to the header.
JavaScript
Here’s the required JavaScript code–we begin by defining some variables, calculating the height of the header
, then adding that value as padding-top
to the main
element:
1 |
var m = document.querySelector("main"), |
2 |
h = document.querySelector("header"), |
3 |
hHeight; |
4 |
|
5 |
function setTopPadding() { |
6 |
hHeight = h.offsetHeight; |
7 |
m.style.paddingTop = hHeight + "px"; |
8 |
}
|
For this demonstration, we use the offsetHeight
property to retrieve the header’s height. Keep in mind that we could equally have used the getBoundingClientRect()
method. It’s worth mentioning that this method may return fractional values.
Now onto the scrolling event:
1 |
function onScroll() { |
2 |
window.addEventListener("scroll", callbackFunc); |
3 |
function callbackFunc() { |
4 |
var y = window.pageYOffset; |
5 |
if (y > 150) { |
6 |
h.classList.add("scroll"); |
7 |
} else { |
8 |
h.classList.remove("scroll"); |
9 |
}
|
10 |
}
|
11 |
}
|
Here we take advantage of the window’s pageYOffset
property to calculate the number of pixels that our document has been scrolled vertically. Note that this property doesn’t work in older versions of IE (< 9). However, if you want to support any of these versions, a workaround is available here.
Then we use the classList
property to add and remove the scroll
class from our header. Not all browsers support this property however, so if you want to support any of these you may want to look at the classList.js and classie.js polyfills. For this example, we could have used the className
property to manipulate our single class, but in a real-world scenario this might not the ideal solution (in case we have multiple classes).
To wrap things up, we call our functions in two different cases:
- When the page loads
- and as we resize the browser window.
1 |
window.onload = function() { |
2 |
setTopPadding(); |
3 |
onScroll(); |
4 |
};
|
5 |
|
6 |
window.onresize = function() { |
7 |
setTopPadding(); |
8 |
};
|
CSS
As long as our scrolling exceeds the limit of 150px, a few additional CSS rules take place:
1 |
.scroll { |
2 |
box-shadow: 0 7px 0 0 rgba(0, 0, 0, .1); |
3 |
} |
4 |
|
5 |
.scroll .logo { |
6 |
padding: 10px 20px; |
7 |
font-size: 1.5rem; |
8 |
} |
9 |
|
10 |
.scroll nav { |
11 |
align-items: center; |
12 |
} |
13 |
|
14 |
.scroll .logo, |
15 |
.scroll ul, |
16 |
.scroll .toggle-menu { |
17 |
margin: 0; |
18 |
} |
Specifically, we make the following changes:
- Add a light gray box shadow to the header.
- Reduce the logo’s padding and font size.
- Change the alignment for the flex items across the cross-axis.
- Remove the margin from the logo, the menu, and the responsive link button.
The aforementioned rules result in this new header layout:



Going Responsive
As we’ve mentioned in a previous section, when the viewport width is less than 1061px, we hide the menu and show the responsive link button (which doesn’t actually do anything). Plus, we make a few other changes in the target elements.
Below you can see the initial appearance of the responsive header:



Here are the related CSS rules:
1 |
@media screen and (max-width: 1060px) { |
2 |
header { |
3 |
padding: 10px; |
4 |
}
|
5 |
nav { |
6 |
align-items: center; |
7 |
}
|
8 |
ul { |
9 |
display: none; |
10 |
}
|
11 |
.logo { |
12 |
font-size: 1.8rem; |
13 |
margin: 10px 0 0 10px; |
14 |
}
|
15 |
.toggle-menu { |
16 |
display: block; |
17 |
}
|
18 |
}
|
And here’s the header’s appearance once it’s been animated:



Performance Considerations
Now that we managed to give our header the desired behavior, let’s go one step further and discuss a few things about performance.
In our example, the code that manipulates the header’s animation (i.e. callbackFunc
) is executed when the scroll
event fires. This means it can be triggered hundreds of times or more. This may result in performance issues, especially when the callbackFunc
function contains a lot of stuff that should be done as we scroll up and down the page. In our specific case we’re dealing with a simple animation, but imagine a real-world scenario where we want to do more complex things like re-positioning elements and so on.
So what can we do about this? Well, there are many possible solutions, but for now let’s briefly discuss one of them. Specifically, we want to allow our function to be executed at most once every 200 milliseconds (this is an arbitrary value). To implement this functionality, we’ll take advantage of Lodash, a modern JavaScript utility library. This library provides the throttle function we need.
First, we include the library (happily there’s also the option to incorporate only the desired function) in our project, then we replace this code:
window.addEventListener("scroll", callbackFunc);
with this:
window.addEventListener("scroll", _.throttle(callbackFunc, 200));
To understand the difference, let’s make a simple test. We’ll initialize a counter that is incremented each time the callbackFunc
function is triggered.



Now try to scroll up and down the page. When you do so, you’ll notice that the counter’s value changes roughly every 200ms. Next, repeat this process without using Lodash. You’ll notice that the counter’s value changes much faster.
Again, although this optimization seems unnecessary for our simple example, you should take it into consideration when you’re dealing with expensive and repetitive tasks.
In the same way, we could have optimized the resize
event listener.
Browser Support
This effect works in most recent browsers and devices. However, the experience isn’t ideal in all mobile devices (e.g. iOS devices). This happens because the scroll
event behaves differently on desktop browsers compared to mobile devices. For example, on a desktop browser the scroll
event fires continuously, whereas on an iPad it occurs when you swipe and release your finger.
Knowing the issues above, you should decide whether or not it’s a sensible pattern to use on your web pages.
Conclusion
In this tutorial, we created a fixed header which is animated as we scroll down the page. I hope you liked the demo and you’ll use it as inspiration in your coming projects!