Making sidebar navigation with only the use of CSS grid layout in the template of Django
Sidebar navigation is a menu display method used on many websites, giving an impression that its implementation might be straightforward. However, creating sidebar navigation is more complex than it might seem at first.
One of the main difficulties arises when designing a sidebar with responsive behavior using a single code template. The complexity increases significantly when implementing the open/close functionality for different devices and adding swipe gestures to enhance the user experience.
To avoid this complexity, frameworks like Tailwind CSS offer sidebar navigation components to save you the trouble of building them from scratch. However, customizing these components can sometimes be even more challenging than creating them from scratch, especially when trying to meet specific design needs.
Considering these challenges, building a customizable and flexible sidebar navigation for your site is often best achieved by creating it yourself using CSS Grid layout. This approach allows for precise control and flexibility, resulting in a more maintainable solution.
In this blog post, we'll explain how to create your own sidebar navigation using CSS Grid as the main technique, including features like swipe-to-open functionality, without relying on CSS frameworks.
- Visualizing the Django Application
The attached images show how this site would be displayed on PC, mobile, and tablet respectively.
Images 1-3 depict PC designs, images 4-7 show mobile designs, and images 8-11 represent tablet designs.
For both PC and tablet designs, the sidebar is initially open. However, in the case of mobile designs, the sidebar is closed by default.
Each design starts in a specific state: image 1 for PC, image 4 for mobile, and image 8 for tablet.
2. Required Python Packages
The following packages will be used to implement this sidebar navigation:
- - django==4.05
- - django-environ==0.8.1
- - django-user-agents==0.4.0
django-user-agents is an important package in the list above. It enables us to separate files and code to be loaded for each device.
3. JS library
The JS library uses alpine.js and jQuery for this project.
I like alpine.js because it is considered an alternative to jQuery and can achieve various operations with a minimum code added to the HTML tag inside.
Initially, I planned to stop using jQuery for this project, but I found it more convenient to use in Chapter 11 due to its simplicity in handling specific interactions. Therefore, I decided to keep using it. Also, there is a chapter at the end of this tutorial that implements the swipe functionality on mobile devices, which will use a module called hammer.js.
4. Project File Structure
The initial file structure of this project is shown at right.
For this project, we only need to write the TEMPLATE part, so the models.py
and other default files are sufficient and need no modifications.
The project name is django_03
and the app name is blog
.
django_03/
├── blog/
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations/
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
├── django_03/
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── manage.py
As you will see later when you look at the code in more detail, the structure of the template file is as accompanying image:
The files header.html
, sidebar.html
, and main.html
are grouped together in home.html
using the include
tag.
The purpose of this is to intentionally separate the header and sidebar into distinct files, which clarifies the role of each file, improves readability, and facilitates customization.
5. Development Process
Create this website in the following steps.
5-1. Basic Django setup
To proceed with this tutorial, you will need a basic Django setup. However, as there are many articles and videos available online, we will not go into detail here.
If you prefer to use Docker for the setup, you can find instructions in this article on our blog.
Once you have the setup ready, please proceed to display "Hello world!" in your own browser.
5-2. Creating settings.py
To use django-user-agents
, specific settings need to be added to settings.py
. Please modify settings.py
as accompanying code:
In this project, django_user_agents
is important. Please ensure that it is included in the INSTALLED_APPS
and MIDDLEWARE
settings.
django_03/settings.py
from pathlib import Path
import environ
import os
env = environ.Env()
BASE_DIR = Path(__file__).resolve().parent.parent
environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
SECRET_KEY = os.getenv('SECRET_KEY')
DEBUG = os.getenv('DEBUG_VALUE') == 'TRUE'
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_user_agents', # here
'blog'
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django_user_agents.middleware.UserAgentMiddleware', # here
]
ROOT_URLCONF = 'django_03.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'django_03.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': os.getenv('DB_NAME'),
'USER': os.getenv('DB_USER'),
'PASSWORD': os.getenv('DB_PASSWORD'),
'HOST': os.getenv('DB_HOST'),
'PORT': os.getenv('DB_PORT'),
}
}
# Password validation
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
STATIC_URL = 'static/'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static"),
]
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
5-3. Writing the Code Foundation, including base.html
We will begin by writing the most basic part of the template: base.html
. Note that we will be adding alpine.js to base.html
.
Next, we will add the essential parts of home.html
, header.html
, main.html
, and sidebar.html
, as shown in the accompanying code.
templates/blog/base.html
{% load static %}
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<!-- alipine.js and jquery -->
<script defer src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- css -->
<link rel="stylesheet" href="{% static 'css/custom-style-base.css' %}">
<title>{% block title %}Grid Layout{% endblock %}</title>
</head>
<body>
<section class="grid">
{% block content %}{% endblock %}
</section>
</body>
</html>
templates/blog/home.html
{% extends 'base.html' %}
{% block content %}
{% include "blog/header.html" %}
{% include "blog/sidebar.html" %}
{% include "blog/main.html" %}
{% endblock %}
templates/blog/header.html
<header class="header"></header>
templates/blog/sidebar.html
<section id="sidenav" class="sidenav">
<div class="sidenav__contents"></div>
</section>
templates/blog/main.html
<main id="main" class="main">
<section class="main__text"></section>
</main>
5-4. Foundational CSS Code for Building Grid Layouts
After writing the basic HTML template, write the corresponding foundational CSS code. First, create a static
folder in the root of the project. Then, create a css
folder inside the static
folder, and finally create custom-style-base.css
.
In this css code, the :root
part centrally manages the colors used in this site, such as text colors.
Feel free to change the :root
section to your liking, as it reflects my personal taste.
static/css/custom-style-base.css
*{
padding:0;
margin:0;
box-sizing:border-box;
}
html{
font-size: 62.5%;
}
:root{
--main-font-family:'Roboto Condensed', sans-serif;
--main-background-color:#F5D982;
--header-background-color:#F29A79;
--sidebar-background-color:#DBAF79;
--reference-background-color:#E87479;
--header-border-color:#5C564E
}
body{
font-family: var(--main-font-family);
position: absolute;
top: 0;
bottom: 0;
}
5-5. Implementation of OPEN / CLOSE button
Next, create a function that opens and closes the sidebar by clicking the button. This is where Alpine.js finally comes into play.
Let's start with the PC version.
As you may know, CSS can combine selectors to refer to different properties and values than those referred to by a single selector.
For example, if you have two styles, .main{width:50%}
and .main.open{width:100%}
, <div class="main">
will have width:50%
, but <div class="main open">
will have width:100%
.
Therefore, in this example, if .open
can be added dynamically, it is possible to have a function like conditional branching in the CSS.
We will use this to create the open/close function of the button.
...
<body>
<section class="grid"
x-data="{
open:true,
close:true,
header_text:true,
}"
@toggle.window="
open = !open;
close = !close;
"
>
...
</section>
</body>
...
To implement the desired behavior, make the changes as accompanying code to header.html
, sidebar.html
, and main.html
.
In header.html
, there is a @click
event that toggles the open
and close
values of @toggle.window
in base.html
. Please pay attention to this event.
When the open
value in @toggle.window
is switched, it affects the :class
attribute in both sidebar.html
and main.html
. Specifically, when the true
or false
value of open
on the left side of ?
is toggled, the value on the right side of ?
(open
or ''
) is added as a class accordingly.
templates/blog/header.html
<header class="header" :class="[open ? 'open':'']">
<div id="header__text"
class="header__text"
@click="
$dispatch('toggle');
header_text = !header_text;
"
x-text="[header_text ? 'CLOSE' : 'OPEN']"
>CLOSE</div>
<div class="header__search">...</div>
</header>
templates/blog/sidebar.html
<section id="sidenav" class="sidenav" :class="[open ? 'open':'', close ? '':'close']">
...
</section>
templates/blog/main.html
<main id="main" class="main" :class="[open ? 'open':'', close ? '' : 'close']">
...
</main>
The accompanying diagram summarizes the relationship between the codes in each of the files above.
The open/close function implementation with buttons on the PC version is now complete. However, it will not work as is on mobile and tablet, since the initial sidebar position is different. To solve this, the User-Agent determines the code to be loaded for each device.
5-6. Separating the HTML and CSS code to load by the User-Agent
As we mentioned earlier, this blog has a slightly different design for each device, which makes it difficult to control the design with a single template. To solve this issue, apply different CSS and templates for each device depending on the User-Agent.
To detect the User-Agent, use a package called django-user-agents
. You can see how to use django-user-agents
on this page, so we won't describe it here. One thing to note is that the official django-user-agents
documentation recommends using a cache. However, using a cache can often result in difficult-to-handle errors, and it works comfortably without using a cache. That's why I currently don't use a cache.
5-7. Loading a Different CSS File for Each Device
To load different CSS files for each device, add the accompanying code to the head of base.html
and place device-specific CSS files in the static
folder at the root of the project.
To get started, add the following files to the static/css
folder.
custom-style-pc.css
custom-style-sp.css
custom-style-tablet.css
Next, add code to base.html
to isolate the template to be loaded.
templates/blog/base.html
{% load static %}
<html lang="en" data-theme="dark">
<head>
...
<link rel="stylesheet" href="{% static 'css/custom-style-base.css' %}">
{% if request.user_agent.is_pc %}
<link rel="stylesheet" href="{% static 'css/custom-style-pc.css' %}">
{% endif %}
{% if request.user_agent.is_mobile and 'iPad' not in request.user_agent.device %}
<link rel="stylesheet" href="{% static 'css/custom-style-sp.css' %}">
{% endif %}
{% if request.user_agent.is_tablet %}
<link rel="stylesheet" href="{% static 'css/custom-style-tablet.css' %}">
{% endif %}
...
<title>{% block title %}Grid Layout{% endblock %}</title>
</head>
<body>
...
</body>
</html>
Next step, add the code in the <body>
tag to load for each device.
As well as css file loading, the code for HTML must also be separated for each device.
!!Replace all code in the body tag with the accompanying code.!!
templates/blog/base.html
...
<head>
...
</head>
<body>
{% if request.user_agent.is_mobile and 'iPad' not in request.user_agent.device %}
<section class="grid"
x-data="{
open:false,
close:true,
header_text:false,
}"
@toggle.window="
open = !open;
close = !close
"
>
{% endif %}
{% if request.user_agent.is_pc %}
<section class="grid"
x-data="{
open:true,
close:true,
header_text:true
}"
@toggle.window="
open = !open;
close = !close;
"
>
{% endif %}
{% if request.user_agent.is_tablet %}
<section class="grid"
x-data="{
open:true,
close:false,
header_text:false
}"
@toggle.window="
open = !open;
close = !close
"
>
{% endif %}
{% block content %}{% endblock %}
</section>
</body>
</html>
...
Add the accompanying code to header.html
, sidebar.html
, and main.html
as well.
templates/blog/header.html
<header class="header" :class="[open ? 'open':'']">
{% if request.user_agent.is_pc %}
<div class="header__button">
<div id="header__text__pc"
class="header__text"
@click="
$dispatch('toggle');
header_text = !header_text;
"
x-text="[header_text ? 'CLOSE' : 'OPEN']"
>CLOSE</div>
</div>
{% endif %}
{% if request.user_agent.is_mobile and 'iPad' not in request.user_agent.device %}
<div class="header__button">
<div id="header__text__sp"
class="header__text"
@click="
$dispatch('toggle');
header_text = !header_text;
"
>OPEN</div>
</div>
{% endif %}
{% if request.user_agent.is_tablet %}
<div class="header__button">
<div id="header__text__tablet"
class="header__text"
@click="
$dispatch('toggle');
header_text = !header_text;
"
x-text="[header_text ? 'OPEN':'CLOSE']"
>CLOSE</div>
</div>
{% endif %}
<div class="header__search">HEADER</div>
</header>
templates/blog/sidebar.html
{% if request.user_agent.is_pc %}
<section id="sidenav" class="sidenav" :class="[open ? 'open':'', close ? '':'close']">
{% endif %}
{% if request.user_agent.is_mobile and 'iPad' not in request.user_agent.device %}
<section id="sidenav" class="sidenav" :class="[open ? 'open':'']">
<div
class="sidenav__button"
@click="
$dispatch('toggle');
header_text = !header_text;
"
>CLOSE</div>
{% endif %}
{% if request.user_agent.is_tablet %}
<section id="sidenav" class="sidenav" :class="[close ? 'close':'']" >
{% endif %}
<div class="sidenav__contents">
<div class="sidenav__title"><a class="sidebar__link" href="{% url 'blog:post_list' %}">SIDE BAR</a></div>
<div class="sidenav__menu">
<ul class="sidenav__items">
<li class="sidenav__item">item</li>
</ul>
</div>
</div>
</section>
templates/blog/main.html
{% if request.user_agent.is_pc %}
<main id="main" class="main" :class="[open ? 'open':'', close ? '' : 'close']">
{% endif %}
{% if request.user_agent.is_mobile and 'iPad' not in request.user_agent.device %}
<main id="main" class="main" :class="[open ? 'open' : '']">
{% endif %}
{% if request.user_agent.is_tablet %}
<main id="main" class="main" :class="[close ? 'close' : '']">
{% endif %}
<section class="main__text">
<div class="main__title">MAIN</div>
<div class="main__content">{% lorem 30 p %}</div>
</section>
<section class="main__reference">
<div class="main__reference__contents">
<div class="main__reference__title">Reference</div>
<div class="main__reference__content">{% lorem %}</div>
</div>
</section>
</main>
5-8 Adding CSS Based on Device
Now let's write CSS for the PC, SP, and tablet versions, which will be contained in custom-style-pc.css
, custom-style-sp.css
, and custom-style-tablet.css
, respectively.
In custom-style-sp.css
, I included code for viewing the smartphone in landscape mode at the end of the file.
static/css/custom-style-pc.css
.grid{
position: relative;
width: 100vw;
height: 100%;
min-height: 100vh;
overflow: auto;
display: grid;
grid-template-columns: 30rem 1fr;
grid-template-rows: 8rem 1fr;
grid-template-areas:
'header header'
'sidenav main'
}
/**************
header
***************/
.header {
grid-area: header;
position: sticky;
top: 0;
width: 100%;
height: 8rem;
font-size: 3rem;
background-color:var(--header-background-color);
z-index: 50;
display: grid;
grid-template-columns: 30rem 1fr 20vw;
justify-content: center;
align-items: center;
}
.header__text{
width: auto;
height: auto;
border: var(--header-border-color) 2px solid;
display: flex;
justify-content: center;
align-items: center;
margin: 0 0 0 1rem;
}
.header__search{
display: flex;
justify-content: center;
align-items: center;
}
/**************
sidebar
***************/
.sidenav{
grid-area: sidenav;
position: fixed;
width: 30rem;
height: auto;
top: 8rem;
bottom: 0;
font-size: 2rem;
padding:1rem;
transition: all 0.3s;
background-color:var(--sidebar-background-color);
overflow:auto;
z-index:20;
}
.sidenav::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}
.sidenav.open{
left:0;
}
.sidenav.close{
left:-30rem;
}
.sidenav__title{
font-size: 3rem;
text-align: center;
}
.sidenav__item{
font-size: 2rem;
list-style-type: none;
}
.dummy.open {
display: none;
}
/************************
main
************************/
.main{
grid-area: main;
position: absolute;
width: 100%;
height: auto;
left: 0;
transition: all 0.3s;
display: grid;
grid-template-columns: 1fr 20vw;
grid-template-rows: auto;
}
.main.open{
width: auto;
height: auto;
left: 0;
}
.main.close {
width: auto;
height: auto;
left:-30rem;
}
.main__text{
width: auto;
height: auto;
padding: 1rem;
background-color: var(--main-background-color);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.main__title{
font-size: 3rem;
}
.main__content{
font-size: 2rem;
/*background-color: var(--main-background-color)*/
}
/************************
Reference
************************/
.main__reference{
width: 20vw;
padding: 1rem;
font-size: clamp(1rem, 2vw, 2rem);
background-color: var(--reference-background-color);
}
.main__reference__contents{
position: fixed;
width: auto;
}
.main__reference__title{
font-size: clamp(1rem, 3vw, 3rem);
}
.main__reference__content{
width: 90%;
font-size: clamp(1rem, 2vw, 2rem);
}
@media (max-width:1024px) {
.main.open{
width: auto;
grid-template-columns: auto;
}
.main__reference{
display: none;
}
.reference{
display: none;
}
}
@media only screen and (max-width: 767px){
.header{
grid-template-columns: 15rem 1fr;
}
.main.open{
width: auto;
filter: brightness(0.5);
left: -30rem;
grid-template-columns: auto;
}
.main.close {
position: absolute;
left: -30rem;
grid-template-columns: auto;
}
.dummy.open {
display: block;
position: fixed;
width: calc(100% - 30rem);
height: 100%;
top: 8rem;
left: 30rem;
z-index: 100;
}
}
static/css/custom-style-sp.css
.grid{
position: relative;
width: 100vw;
height: 100%;
min-height: 100vh;
overflow: auto;
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: 6rem 1fr;
grid-template-areas:
'header header'
'sidenav main'
}
/**************
header
***************/
.header {
grid-area: header;
position: absolute;
top: 0;
width: 100%;
height: 6rem;
font-size: 4rem;
background-color:var(--header-background-color);
display: grid;
grid-template-columns: 30vw 1fr;
justify-content: center;
align-items: stretch;
}
.header.open {
filter: grayscale(0.8);
}
.header__button {
padding: 1vw 1vw 1vw 5vw;
}
.header__text{
width: auto;
height: 100%;
font-size: 2rem;
border: var(--header-border-color) 2px solid;
display: flex;
justify-content: center;
align-items: center;
}
.header__search{
display: flex;
justify-content: center;
align-items: center;
}
/**************
sidebar
***************/
.sidenav{
grid-area: sidenav;
position: fixed;
top: 0;
bottom:0;
left: -70vw;
width: 70vw;
height: 100%;
padding: 2vw;
transition: all 0.3s;
background-color:var(--sidebar-background-color);
overflow: auto;
z-index:20;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 5rem auto;
}
.sidenav.open {
position: fixed;
top: 0;
bottom: 0;
left: 0;
width: 70vw;
height: 100%;
transition: all 0.3s;
}
.sidenav__button{
width: auto;
height: 4rem;
font-size: 2rem;
border: var(--header-border-color) 2px solid;
display: flex;
justify-content: center;
align-items: center;
}
.sidenav__title{
font-size: 4rem;
text-align: center;
}
.sidenav__item{
font-size: 2rem;
}
/************************
main
************************/
.main {
grid-area: main;
width: auto;
height: 100%;
transition: all 0.3s;
display: grid;
grid-template-columns: auto;
grid-template-rows: 5fr 1fr;
grid-template-areas:
'maincol'
'reference';
align-items: center;
justify-content: center;
}
.main.open{
filter:grayscale(0.8);
}
.main__text {
width: auto;
height: auto;
background-color: var(--main-background-color);
padding: 1rem 1rem 1rem 5vw;
}
.main__title{
font-size: 4rem;
text-align: center;
}
.main__content{
font-size: 2rem;
}
/************************
Reference
************************/
.main__reference{
background-color: var(--reference-background-color);
padding: 5vw;
}
.main__reference__title{
font-size: 4rem;
text-align: center;
}
.main__reference__content{
font-size: 2rem;
}
@media only screen and (orientation:landscape) {
.header__text{
width: 100%;
height: 3rem;
}
.sidenav {
width: 30vw;
height: auto;
left: -30vw;
padding: 1rem;
overflow: auto;
grid-template-rows: 5rem auto;
}
.sidenav.open {
left: 0;
width: 30vw;
}
.sidenav__button{
width: 100%;
height: 3rem;
margin: 0 0 1rem 0;
}
}
static/css/custom-style-tablet.css
.grid{
position: relative;
width: 100vw;
height: 100%;
min-height: 100vh;
overflow: auto;
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: auto 1fr;
grid-template-areas:
'header header'
'sidenav main'
}
/**************
header
***************/
.header {
grid-area: header;
position: fixed;
top: 0;
width: 100%;
height: 6rem;
font-size: 4rem;
background-color:var(--header-background-color);
z-index: 50;
display: grid;
grid-template-columns: 30vw 1fr;
justify-content: center;
align-items: stretch;
}
.header__button {
padding: 1rem 1rem 1rem 2rem;
}
.header__text{
width: 100%;
height: 100%;
font-size: 2rem;
border: var(--header-border-color) 2px solid;
display: flex;
justify-content: center;
align-items: center;
}
.header__search{
display: flex;
justify-content: center;
align-items: center;
}
/**************
sidebar
***************/
.sidenav{
grid-area: sidenav;
position: fixed;
top: 5rem;
left: 0;
bottom:0;
width: 30vw;
height: 100%;
padding: 2rem;
transition: all 0.3s;
background-color:var(--sidebar-background-color);
overflow: auto;
z-index:20;
}
.sidenav.close{
left:-30vw;
}
.sidenav__title{
font-size: 3rem;
text-align: center;
}
.sidenav__item{
font-size: 2rem;
list-style-type: none;
}
/************************
main
************************/
.main{
grid-area: main;
position: absolute;
width: auto;
height:100%;
top:5rem;
/*bottom: 0;*/
left:30vw;
/*right:0;*/
transition: all 0.3s;
display: grid;
grid-template-columns: auto;
grid-template-rows: 5fr 1fr;
grid-template-areas:
'maincol'
'reference';
align-items: center;
justify-content: center;
}
.main.close{
left:0;
}
.main__text {
width: auto;
height: auto;
background-color: var(--main-background-color);
padding: 2rem;
}
.main__title{
font-size: 3rem;
text-align: center;
}
.main__content{
font-size: 2rem;
}
/************************
Reference
************************/
.main__reference{
background-color: var(--reference-background-color);
padding: 2rem;
}
.main__reference__title{
font-size: 3rem;
text-align: center;
}
.main__reference__content{
font-size: 2rem;
}
5-9. Open/Close the Sidebar menu automatically on PC at a certain width
At this point, we have created a basic sidebar menu design that is customized for each device. However, there are still some details that need to be adjusted based on the width of the device screen. We need to ensure that the sidebar menu automatically opens and closes on PC screens when it reaches a certain width.
We are introducing two new directives: @resize.window
and x-init
. @resize.window
detects the width of the browser in real-time and changes the values of open
, close
, and header_text
when the browser's width is 641px.
Additionally, x-init
is used under @resize.window
to set the initial values of open
, close
, and header_text
when the page is loaded. This boundary value is also set to 641px.
templates/blog/base.html
<!doctype html>
...
<head>
...
</head>
<body>
{% if request.user_agent.is_mobile and 'iPad' not in request.user_agent.device %}
<section class="grid"
x-data="{
open:false,
close:true,
header_text:false,
}"
@toggle.window="
open = !open;
close = !close
"
>
{% endif %}
{% if request.user_agent.is_pc %}
<section class="grid"
x-data="{
open:true,
close:true,
header_text:true
}"
@toggle.window="
open = !open;
close = !close;
"
<!-- add following code -->
@resize.window="
if(window.innerWidth < 767){
open = false;
close = false;
header_text = false;
}else{
open = true;
close = true;
header_text = true;
}
"
x-init="
sidebar_init = window.innerWidth < 767;
if(sidebar_init){
open = false;
close = false;
header_text = false;
}
"
<!-- Add code up to this part -->
>
{% endif %}
...
</section>
</body>
</html>
5-10. Swipe to Open/Close
To enhance the user experience on mobile and tablet devices, we want to provide the capability to open and close the sidebar by swiping. This feature will be in addition to the button function. To implement this functionality, follow the three steps below in base.html
.
8-1. Add the Hammer.js CDN to base.html
.
8-2. Add the JavaScript code to call hammer.js
after the HTML code.
8-3. Add the alpine.js
directives to the mobile and tablet code.
Hammer.js is a library that enables touch gestures. You can access the Hammer.js documentation here.
To implement these steps, add the accompanying code:
templates/blog/base.html
...
<html lang="en" data-theme="dark">
<head>
...
<!-- Add following code -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js"></script>
<link rel="stylesheet" href="{% static 'css/custom-style-base.css' %}">
<title>{% block title %}Grid Layout{% endblock %}</title>
</head>
<body>
{% if request.user_agent.is_mobile and 'iPad' not in request.user_agent.device %}
<section class="grid"
...
<!-- Add following code -->
@my-event.window="
header_text = !header_text
open = !open;
close = !close
"
...
>
{% endif %}
{% if request.user_agent.is_pc %}
...
{% endif %}
{% if request.user_agent.is_tablet %}
<section class="grid"
...
<!-- Add following code -->
@my-event.window="
header_text = !header_text
open = !open;
close = !close
"
...
>
{% endif %}
{% block content %}{% endblock %}
</section>
</body>
</html>
<script>
<!-- Here is Hammer.js code -->
let myElement = document.querySelector('#swipe');
let mc = new Hammer(myElement);
mc.get('swipe').set({threshold: 5});
mc.on('swiperight', function (e) {
console.log('swiperight')
let event = new CustomEvent('my-event', {});
window.dispatchEvent(event);
});
mc.on('swipeleft', function (e) {
console.log('swipeleft');
let event = new CustomEvent('my-event', {});
window.dispatchEvent(event);
});
</script>
Next, add the following code to sidebar.html
.
templates/blog/sidebar.html
<section id="sidenav" class="sidenav" :class="[open ? 'open':'']">
...
</section>
<!-- add following code -->
{% if request.user_agent.is_pc %}
<div id="dummy" class="dummy" :class="[open ? 'open':'']"></div>
{% endif %}
{% if request.user_agent.is_mobile and 'iPad' not in request.user_agent.device %}
<div id="swipe" class="swipe" :class="[open ? 'open':'']"></div>
{% endif %}
{% if request.user_agent.is_tablet %}
<div id="swipe" class="swipe" :class="[open ? 'open':'']"></div>
{% endif %}
Add the CSS code needed to implement the swipe function.
static/css/custom-style-sp.css
...
/**************
swipe
***************/
.swipe{
width: 5vw;
height: 100%;
position: fixed;
top: 0;
bottom:0;
z-index: 100;
}
.swipe.open {
position: fixed;
width: 30vw;
height: 100%;
left: 70vw;
z-index: 100;
}
@media only screen and (orientation:landscape) {
...
.swipe{
width: 5vw;
}
.swipe.open{
width: 70vw;
left: 30vw;
}
}
static/css/custom-style-tablet.css
/**************
swipe
***************/
.swipe{
position: fixed;
width: 5vw;
height: 100%;
top: 5rem;
bottom:0;
z-index: 100;
}
.swipe.open {
position: fixed;
width: 5vw;
height: 100%;
top: 5rem;
left: 30vw;
z-index: 100;
}
5-11. When a Link in the sidebar is clicked, the sidebar should close before going to the next page
As of the time of writing, I believed that I had completed the implementation without encountering any issues. However, after running it for a while, I found a problem.
Specifically, on mobile devices, clicking on a link in the sidebar causes the page to transition before the sidebar navigation is completely closed. This behavior is not aesthetically pleasing.
To address this issue, modify the code so that the page transitions after the sidebar navigation is completely closed. To achieve this, use the JavaScript setTimeout
function to extend the time between page transitions until the end of the slide-out animation.
To implement this change, add the accompanying jQuery code to base.html
.
templates/blog/base.html
...
<head>
...
</head>
<body>
...
</body>
</html>
<script>
{% if request.user_agent.is_mobile %}
$(".sidebar__link").click(function(e) {
e.preventDefault();
const href = $(this).attr('href');
const sidenav = $('#sidenav').hasClass('open')
const main = $('#main').hasClass('open')
if(sidenav && main){
$('#sidenav, #main').removeClass('open')
setTimeout(()=>{window.location.href = href},300);
}
});
{% endif %}
{% if request.user_agent.is_pc %}
$(".sidebar__link").click(function(e) {
const windowSize = $( window ).width();
if(windowSize < 767) {
e.preventDefault();
const href = $(this).attr('href');
const sidenav = $('#sidenav').hasClass('open')
const main = $('#main').hasClass('open')
if (sidenav && main) {
$('#sidenav, #main').removeClass('open')
setTimeout(() => {
window.location.href = href
}, 300);
}
}
});
{% endif %}
</script>
...
All done! I hope you were able to follow along with the video at the beginning of this article.
As previously mentioned, one of the benefits of creating a site using only a grid layout is that the code is concise and easy to customize. I would be delighted if you were able to use this code to create a site with sidebar navigation in your design.
You can find the GitHub repository for this code here.