Created | ![]() |
Favourites | Opened | Upvotes | Comments |
8. Oct 2019 | 0 | 0 | 273 | 0 | 0 |
Index :
Appendixes :
In this tutorial I will demo 3 typical types of sticky menus :
All the menus created in this tutorial will exists in the context of the standard layout created in the CSS Grid blog post. However, here I will fast re-create that css grid based standard layout.
The standard layout have the following parts:
The standard layout is build using a css grid container one grid item for each part of the layout. In addition each part of the layout (each grid item) have an inner content div as to separate the layout divs from the content divs (which will be necessary later on).
<div id="divGridLayout">
<div id="divGridTop" class="grid-item">
<div id="divTop">Top</div>
</div>
<div id="divGridLeft" class="grid-item">
<div id="divLeft">Left</div>
</div>
<div id="divGridMain" class="grid-item">
<div id="divMain">Main</div>
</div>
<div id="divGridRight" class="grid-item">
<div id="divRight">Right</div>
</div>
<div id="divGridFooter" class="grid-item">
<div id="divFooter">Footer</div>
</div>
</div>
The standard layout css is setting up the grid and forcing the footer to the bottom (see CSS Grid blog post for an explaination).
html {
height: 100%; /* push footer to bottom */
}
body {
height: 100%; /* push footer to bottom */
padding: 0;
margin: 0;
}
#divGridLayout {
max-width: 926px;
height: 100%; /* push footer to bottom */
margin: 0 auto; /* center the grid horizontally */
display: grid;
grid-gap: 10px;
grid-template-columns: 200px auto 200px;
grid-template-rows: 50px auto 50px; /* auto : push footer to bottom */
}
#divGridLayout > div.grid-item {
border: 2px solid #ccc;
border-radius: 5px;
background-color: #fff;
padding: 10px;
color: #d9480f;
}
#divGridTop {
grid-column-start: 1;
grid-column-end: 4;
}
#divGridFooter {
grid-column-start: 1;
grid-column-end: 4;
}
With the standard layout in place, we can get to the actual subject : sticky menus.
The current header will scroll with the web page and disappear from the viewport as you scroll down the page. The purpose of this section is to create a header that stays fixed at the top of the viewport as the user scrolls down the page - so the functionality in the header is always readily available for the user.
It is not possible to re-purpose the grid header, instead we need to introduce a new header div separated from the css grid - this new header div will hide the grid header and stays fixed no matter how much the user scrolls.
Here is the new full HTML :
<div id="divFixedTop">Top</div> <!-- added #divFixedTop div -->
<div id="divGridLayout">
<div id="divGridTop" class="grid-item">
<div id="divTop">Top</div>
</div>
<div id="divGridLeft" class="grid-item">
<div id="divLeft">Left</div>
</div>
<div id="divGridMain" class="grid-item">
<div id="divMain">Main</div>
</div>
<div id="divGridRight" class="grid-item">
<div id="divRight">Right</div>
</div>
<div id="divGridFooter" class="grid-item">
<div id="divFooter">Footer</div>
</div>
</div>
All the old CSS is still good, we only need to add the following :
#divFixedTop {
position: fixed;
top: 0;
left: 0;
right: 0;
width: 926px; /* width equal to #divGrid - remove if you want to span full viewport width */
margin: 0 auto; /* remove if you want to span full viewport width */
height: 50px; /* height equal to #divGridTop */
z-index: 11;
background-color: #f7f7f7;
}
So creating a fixed header is fairly simple.
The context menu is much like the fixed header as both are fixed in place relative to the ViewPort. However, the context menu can also be moved around by the user - using the mouse to drag the context menu into any place the user like.
The context menu requires no additional HTML as I have chosen to create it using Javascript (Javascript is needed anyway to move it around).
So the Javascript will :
document.addEventListener("DOMContentLoaded", () => {
ContextMenu.initialize();
});
ContextMenu = {
initialize: function () {
/* first setup variables to recalculate position upon windows resizing */
ContextMenu.Layout = {};
ContextMenu.Layout.Left = {};
ContextMenu.Layout.Left.divLayout = document.querySelector("#divGridLeft");
ContextMenu.Layout.Left.x = ContextMenu.Layout.Left.divLayout.getBoundingClientRect().x;
ContextMenu.Layout.Left.y = ContextMenu.Layout.Left.divLayout.getBoundingClientRect().y;
/* then draw the context menu */
ContextMenu.draw();
},
startDrag: function (e) {
e = e || event;
e.preventDefault();
document.addEventListener("mousemove", ContextMenu.doDrag, false);
document.addEventListener("mouseup", ContextMenu.endDrag, false);
ContextMenu.mouseY = e.clientY;
ContextMenu.mouseX = e.clientX;
},
doDrag: function (e) {
e = e || event;
e.preventDefault();
var eClientY = e.clientY;
var eClientX = e.clientX;
mDiffY = eClientY - ContextMenu.mouseY;
mDiffX = eClientX - ContextMenu.mouseX;
if (ContextMenu.divContext.offsetLeft <= 0 && mDiffX < 0) {
ContextMenu.divContext.style.left = 0;
return;
}
else if (ContextMenu.divContext.offsetTop <= 50 && mDiffY < 0) {
ContextMenu.divContext.style.top = "50px";
return;
}
else if (ContextMenu.divContext.offsetLeft + ContextMenu.divContext.offsetWidth >= document.body.clientWidth && mDiffX > 0) {
ContextMenu.divContext.style.left = (document.body.clientWidth - ContextMenu.divContext.offsetWidth) + "px";
return;
}
else if (ContextMenu.divContext.offsetTop + ContextMenu.divContext.offsetHeight >= document.body.clientHeight && mDiffY > 0) {
ContextMenu.divContext.style.top = (document.body.clientHeight - ContextMenu.divContext.offsetHeight) + "px";
return;
}
ContextMenu.mouseY += mDiffY;
ContextMenu.mouseX += mDiffX;
ContextMenu.divContext.style.left = ContextMenu.divContext.offsetLeft + mDiffX + "px";
ContextMenu.divContext.style.top = ContextMenu.divContext.offsetTop + mDiffY + "px";
/* re-calculating distanceToFixPoint for re-positioning */
var divGridLeftRect = ContextMenu.Layout.Left.divLayout.getBoundingClientRect();
ContextMenu.distanceToFixPointX = ContextMenu.divContext.offsetLeft - divGridLeftRect.x
ContextMenu.distanceToFixPointY = ContextMenu.divContext.offsetTop - divGridLeftRect.y
},
endDrag: function (e) {
e = e || event;
e.preventDefault();
document.removeEventListener("mousemove", ContextMenu.doDrag, false);
document.removeEventListener("mouseup", ContextMenu.endDrag, false);
},
draw: function () {
ContextMenu.viewPortWidth = document.body.clientWidth;
ContextMenu.viewPortHeight = document.body.clientHeight;
var divContext = document.createElement("div");
divContext.style.position = "fixed";
divContext.style.border = "solid 1px red";
divContext.style.backgroundColor = "#fff";
divContext.style.width = "33px";
divContext.style.height = "80px";
var divMove = document.createElement("div"); divContext.appendChild(divMove);
divMove.style.cursor = "move";
var iconMove = document.createElement("i"); divMove.appendChild(iconMove);
iconMove.style.fontSize = "2rem";
iconMove.style.color = '#ccc';
iconMove.className = "far fa-arrows";
divContext.style.left = (ContextMenu.viewPortWidth / 2 + 242) + "px";
divContext.style.top = (ContextMenu.viewPortHeight / 2) + "px";
ContextMenu.divContext = divContext;
document.body.appendChild(divContext);
/* regarding re-positioning */
ContextMenu.distanceToFixPointX = divContext.offsetLeft - ContextMenu.Layout.Left.x;
ContextMenu.distanceToFixPointY = divContext.offsetTop - ContextMenu.Layout.Left.y;
/* setup listeners */
divMove.onmousedown = function () {
ContextMenu.startDrag();
}
window.addEventListener('resize', ContextMenu.reposition, false);
},
reposition: function () {
var divGridLeftRect = ContextMenu.Layout.Left.divLayout.getBoundingClientRect();
var newLeft = (divGridLeftRect.x + ContextMenu.distanceToFixPointX);
var newTop = (divGridLeftRect.y + ContextMenu.distanceToFixPointY);
/* be sure the context menu is kept within the ViewPort */
if (newLeft < 0) {
newLeft = 0;
}
else if (newLeft > document.body.clientWidth - ContextMenu.divContext.offsetWidth) {
newLeft = document.body.clientWidth - ContextMenu.divContext.offsetWidth;
}
if (newTop < 50) {
newTop = 50; // height of fixed top bar
}
else if (newTop > document.body.clientHeight - ContextMenu.divContext.offsetHeight) {
newTop = document.body.clientHeight - ContextMenu.divContext.offsetHeight;
}
ContextMenu.divContext.style.left = newLeft + "px";
ContextMenu.divContext.style.top = newTop + "px";
}
}
That was a long javascript, however the dragging around and re-position works very well.
The context menu is now ready to be added any context relevant functionality you may want - that is your responsibility though.
Notice the "Filter Categories" box : it is originally top aligned with the tree structure under the "My Topiqs" header - however as the page scrolls down the "My Topiqs" header moves out of sight and the tree structure continually moves up, but the "Filter Categories" box stops moving just before it is about to disappear under the top bar, thereby staying within the ViewPort no matter how much you scroll.
Here I will demo a more simple example though : a header and a sticky box in the left grid item - notice how the "Some header" disappears and the "Always visible" box moves to the top and then fixes at the top until the page scrolls up again and the "Always visible" box moves down to initial position.
Here is the full HTML
<div id="divFixedTop">Top</div>
<div id="divGridLayout">
<div id="divGridTop" class="grid-item">
<div id="divTop">Top</div>
</div>
<div id="divGridLeft" class="grid-item">
<div id="divLeft"><h2>Some header</h2></div>
<div id="divStickyBox">Always visible</div> <!-- the sticky box CANNOT be a child of #divLeft because #divLeft does not vertically fill the ViewPort -->
</div>
<div id="divGridMain" class="grid-item">
<div id="divMain">Main</div>
</div>
<div id="divGridRight" class="grid-item">
<div id="divRight">Right</div>
</div>
<div id="divGridFooter" class="grid-item">
<div id="divFooter">Footer</div>
</div>
</div>
And here is the CSS for the sticky box
#divStickyBox {
position: sticky;
top: 60px;
border: 2px solid rgb(233,171,88);
background-color: rgba(233,171,88,.5);
height: 120px;
display: flex; /* just for centering of the text Always visible */
align-items: center;
justify-content: center;
}
With regard to the HTML, It is important that the #divStickyBox is placed within the #divGridLeft and not the #divLeft. This is because the #divLeft container and with it ALL it's content will scroll out of sight as the user scrolls down. It doesn't matter if the #divStickyBox is sticky if it's container is scrolled out of sight - #divStickyBox exists in it's container and will be scrolled out as well.
A sticky box should ALWAYS exist in a container that cannot scroll out of sight, in our example the #divGridLeft will never scroll out of sight (but document.body can also be used or a container with the tallest content)
Here I will create a menu - sometimes called a sidebar - in the right grid item of the standard layout.
In case we want the sidebar to not move as we scroll the main body we typically just set the sidebar css position property to fixed like this #divRight { position:fixed } - this will work as long as the sidebar is SHORTER than the ViewPort height.
If on the other hand, the sidebar have the potiental to be LONGER than the ViewPort height, the position:fixed solution falls apart.
The following is a breakdown of relevant situations : (position:fixed on the sidebar)
I know of no way to solve the problem with CSS alone - the real solution involves Javascript. The existing HTML & CSS is still good, we only need to add the following Javascript to make the sidebar work under all of the above conditions.
The Javascript below creates a Layout object with sub objects for each grid item and 2 functions, initialize() & setMenuRight(). The Layout.initialize() function gets relevant DOM objects & sizes and Layout.setMenuRight() sets up the sidebar.
document.addEventListener("DOMContentLoaded", () => {
Layout.initialize();
});
Layout = {
Top: {
divLayout: null,
divLayoutHeight: 0,
divContent: null
},
Left: {
divLayout: null,
divLayoutWidth: 0,
divContent: null,
divContentHeight: 0,
divContentWidth: 0,
divContentNaturalTop: 0,
isFixed: false
},
Main: {
divLayout: null,
divLayoutWidth: 0,
divContent: null,
divContentHeight: 0
},
Right: {
divLayout: null,
divLayoutWidth: 0,
divContent: null,
divContentHeight: 0,
divContentWidth: 0,
divContentNaturalTop: 0,
isFixed: false
},
gridGap: 10,
scrollTolerance: 0,
initialize: function () { // must be called after DOMContentLoaded
Layout.Top.divLayout = document.getElementById("divGridTop");
Layout.Top.divLayoutHeight = Layout.Top.divLayout.offsetHeight;
Layout.Left.divLayout = document.getElementById("divGridLeft");
Layout.Left.divLayoutWidth = Layout.Left.divLayout.offsetWidth;
Layout.Left.divContent = document.getElementById("divLeft");
Layout.Left.divContentHeight = Layout.Left.divContent.offsetHeight;
Layout.Left.divContentWidth = Layout.Left.divContent.offsetWidth;
Layout.Left.divContentTop = Layout.Left.divContent.offsetTop;
Layout.Main.divLayout = document.getElementById("divGridMain");
Layout.Main.divLayoutWidth = Layout.Main.divLayout.offsetWidth;
Layout.Main.divContent = document.getElementById("divMain");
Layout.Main.divContentHeight = Layout.Main.divContent.offsetHeight;
Layout.Main.divContentTop = Layout.Main.divContent.offsetTop;
Layout.Right.divLayout = document.getElementById("divGridRight");
Layout.Right.divLayoutWidth = Layout.Right.divLayout.offsetWidth;
Layout.Right.divContent = document.getElementById("divRight");
Layout.Right.divContentHeight = Layout.Right.divContent.offsetHeight;
Layout.Right.divContentWidth = Layout.Right.divContent.offsetWidth;
Layout.Right.divContentTop = Layout.Right.divContent.offsetTop;
Layout.setSidebars();
document.addEventListener("scroll", function () {
Layout.setSidebars();
});
},
setSidebars: function () {
var horizontalBorders = 0;
var availableVerticalViewSpace = window.innerHeight - Layout.Main.divContentTop;
var scrollTop = document.documentElement.scrollTop;
var viewportBottomPosition = scrollTop + window.innerHeight;
setMenuLeft();
setMenuRight();
function setMenuLeft() {
// much like setMenuRight
}
function setMenuRight() {
if (scrollTop == 0 || Layout.Right.divContentHeight > Layout.Main.divContentHeight) {
Layout.Right.divContent.style.position = "static";
Layout.Right.divContent.style.top = 0;
Layout.Right.isFixed = false;
}
else {
if (Layout.Right.divContentHeight < availableVerticalViewSpace) {
var naturalTop = Layout.Main.divContentTop;
Layout.Right.isFixed = true;
Layout.Right.divContent.style.width = (Layout.Right.divContentWidth - horizontalBorders) + "px";
Layout.Right.divContent.style.position = "fixed";
Layout.Right.divContent.style.top = naturalTop + "px";
}
else {
var restTop = window.innerHeight - Layout.Right.divContentHeight - Layout.scrollTolerance;
if (Layout.Right.isFixed) {
if (scrollTop < Math.abs(restTop)) { // scrollTop is positive while restTop is negative
Layout.Right.isFixed = false;
Layout.Right.divContent.style.position = "static";
Layout.Right.divContent.style.top = 0;
Layout.initialize(); // this will avoid flckering of side menus in case of infinite content scroll
}
}
else {
if (Layout.Right.divContentHeight + Layout.Right.divContentTop + Layout.scrollTolerance <= viewportBottomPosition) {
Layout.Right.isFixed = true;
Layout.Right.divContent.style.width = (Layout.Right.divContentWidth - horizontalBorders) + "px";
Layout.Right.divContent.style.position = "fixed";
Layout.Right.divContent.style.top = restTop + "px";
}
}
}
}
}
}
}
You now call Layout.initialize() on DOMContentLoaded :
document.addEventListener("DOMContentLoaded", () => {
Layout.initialize();
}
Your sidebar will be working properly whether short or tall.