In this updated tutorial, we’ll create a responsive admin dashboard layout with CSS and a touch of JavaScript. To build it, we’ll borrow some ideas from the WordPress dashboard, such as its collapsible sidebar menu.
Throughout the tutorial, we’ll face plenty of challenges, but ones that will give us good practice for enhancing our front-end skills.
Admin Dashboard Demo
Without further ado, let’s have a look at the final admin dashboard demo. Pay attention to the following things:
- Hit the Collapse button at the foot of the sidebar to see the collapsible nav in action
- Hit the toggle switch at the foot of the sidebar to reveal the light and dark admin themes
- Check out the full-screen version to play with its responsiveness
Envato Elements: Unlimited Web Design Downloads
Before we get into the meat of the tutorial, I wanted to mention a great source for web designers looking for ready-made templates and other digital assets to download. Envato Elements offers unlimited CSS templates, as well as fonts, photos, mockups, and more.
For example, the TailStack CSS Admin Dashboard Template lets you implement exactly the kind of CSS admin dashboard we're creating in today's tutorial, without writing a single line of code. Just download it and put it to use.
Or, if you want to code your own dashboard in JavaScript and CSS, read on for the tutorial.
1. Begin With the Page Markup
To kick off the markup we’ll need an SVG, a header, and a section:
<svg style="display:none;">...</svg> <header class="page-header">...</header> <section class="page-content">...</section>
SVG Sprites
As you might imagine with any admin dashboard we’re going to need a bunch of icons. Thankfully, Envato Elements provides an ever-expanding collection of useful vector icons, so let’s take advantage of that library and download these Trade and Dashboard Icons.
Rather than including them directly on the page via an img
or svg
tag, let’s go one step further and create an SVG sprite. To do this, we’ll wrap all the icons in an SVG container. The container should be hidden, so we’ll apply display: none
to it. If we don’t hide it, a big empty area will appear at the top of the page.
Each icon will be placed inside a symbol
element with a unique ID and a viewBox
attribute which will depend on the icon size. We can then render the target icon whenever we need it by calling the use
element (I’ll show you how in a minute).
Additionally, we’ll put our logo within this container.
Later, we’ll examine how the colors of these SVGs will change depending on the theme mode, but for now let’s just become familiar with the markup needed for the SVG sprite:
<svg style="display:none;"> <symbol id="logo" viewBox="0 0 140 59"> <g> <path d="M6.8 57c0 .4-.1.7-.2.9-.1.2-.3.4-.4.5-.1.1-.4.199-.5.3-.2 0-.3.1-.5.1-.1 0-.3 0-.5-.1-.2 0-.4-.101-.5-.3-.2 0-.4-.2-.5-.4-.1-.2-.2-.5-.2-.9V44.7h-2c-.3 0-.6-.101-.8-.2-.2-.1-.3-.2-.5-.4s-.2-.3-.2-.4v-.4c0-.1 0-.2.1-.399 0-.2.1-.301.2-.4.1-.1.3-.3.5-.4.1 0 .4-.1.7-.1h2.1v-3.5c0-1 .1-1.9.3-2.7C4.1 35 4.5 34.3 5 33.7c.5-.6 1.1-1.1 1.9-1.4.8-.3 1.7-.5 2.7-.5.9 0 1.5.101 1.8.4.3.3.5.6.5 1.1 0 .3-.1.601-.3.9-.2.3-.6.4-1.2.4h-.6c-.6 0-1.1.101-1.5.301-.4.199-.7.5-.9.8C7.2 36 7 36.5 7 37c-.1.5-.1 1-.1 1.6V42h2.7c.3 0 .6.1.8.2.2.1.3.2.5.399.1.101.2.301.2.401 0 .2.1.3.1.4 0 .1 0 .3-.1.399 0 .2-.1.3-.2.4-.1.1-.3.3-.5.399-.2.101-.5.2-.8.2H6.8V57z" /> <path d="M30.4 50.2c0 1.3-.2 2.5-.7 3.5-.5 1.1-1.1 2-1.9 2.8-.8.8-1.8 1.4-2.8 1.8-1.1.4-2.3.601-3.5.601-1.3 0-2.4-.2-3.5-.601-1.1-.399-2-1-2.8-1.8-.8-.8-1.4-1.7-1.9-2.8-.5-1.101-.7-2.2-.7-3.5s.2-2.4.7-3.5c.5-1.101 1.1-2 1.9-2.7.8-.8 1.7-1.4 2.8-1.8 1.1-.4 2.3-.601 3.5-.601 1.3 0 2.4.2 3.5.601 1.1.399 2 1 2.8 1.8.8.8 1.4 1.7 1.9 2.7.5 1.1.7 2.3.7 3.5zm-3.4 0c0-.8-.1-1.5-.4-2.3-.2-.7-.6-1.4-1.1-1.9s-1-1-1.7-1.3c-.7-.3-1.5-.5-2.4-.5s-1.7.2-2.4.5-1.3.8-1.7 1.3c-.5.5-.8 1.2-1.1 1.9-.2.699-.4 1.5-.4 2.3s.1 1.5.4 2.3c.2.7.6 1.4 1.1 1.9.5.6 1 1 1.7 1.3s1.5.5 2.4.5 1.7-.2 2.4-.5 1.3-.8 1.7-1.3c.5-.601.8-1.2 1.1-1.9.3-.7.4-1.5.4-2.3z" /> <path d="M38.1 44.8h.1c.4-.899 1-1.7 1.9-2.3s1.8-.9 2.9-.9c.5 0 1 .101 1.3.301.4.199.6.6.6 1.1 0 .6-.2 1-.6 1.2-.4.2-.8.3-1.4.3h-.2c-1.3 0-2.4.5-3.2 1.4-.8.899-1.3 2.3-1.3 4.1v7c0 .4-.1.7-.2.9-.1.199-.3.399-.4.5-.2.1-.4.199-.5.3-.2 0-.3.1-.5.1-.1 0-.3 0-.5-.1-.2 0-.4-.101-.5-.3-.1-.2-.3-.301-.4-.5C35 57.7 35 57.4 35 57V43.5c0-.4.1-.7.2-.9.1-.199.3-.399.4-.5.2-.1.3-.199.5-.199s.3-.101.5-.101c.1 0 .3 0 .4.101.2 0 .3.1.5.199.2.101.3.301.4.5.1.2.2.5.2.9v1.3z" /> <path d="M49.2 51.3c0 .7.2 1.4.5 2 .3.601.7 1.2 1.2 1.601.5.5 1.1.8 1.7 1.1s1.3.4 2 .4c1 0 1.8-.2 2.5-.5.7-.4 1.2-.801 1.8-1.2.2-.2.4-.3.6-.4.2-.301.3-.301.5-.301.4 0 .7.1 1 .4.3.199.4.6.4 1 0 .1 0 .3-.1.5s-.2.4-.4.7c-1.6 1.7-3.7 2.5-6.3 2.5-1.3 0-2.4-.199-3.5-.6s-2-1-2.8-1.8c-.8-.8-1.4-1.7-1.8-2.7-.4-1.1-.7-2.3-.7-3.6 0-1.301.2-2.5.6-3.5.4-1.101 1-2 1.8-2.801.8-.8 1.7-1.399 2.7-1.8 1-.399 2.2-.6 3.4-.6 2.1 0 3.8.6 5.2 1.8s2.3 2.9 2.6 5.2c0 .3.1.5.1.6v.5c0 1.101-.6 1.7-1.7 1.7H49.2V51.3zm9.9-2.5c0-.7-.1-1.3-.3-1.8-.2-.6-.5-1.1-.9-1.5s-.9-.7-1.4-1c-.6-.2-1.2-.4-2-.4-.7 0-1.4.101-2 .4-.6.2-1.2.6-1.6 1-.5.4-.8.9-1.1 1.5-.3.6-.5 1.2-.5 1.8h9.8z" /> <path d="M77.9 55.1c.399-.3.8-.5 1.199-.5.4 0 .7.101 1 .4.2.3.4.6.4.9 0 .199 0 .5-.1.699a1.856 1.856 0 01-.599.701c-.7.5-1.399.9-2.3 1.2s-1.8.4-2.7.4c-1.3 0-2.5-.2-3.5-.601-1.1-.399-2-1-2.8-1.8s-1.4-1.7-1.8-2.7c-.4-1.1-.7-2.3-.7-3.6s.2-2.5.7-3.601c.4-1.1 1.1-2 1.8-2.8.8-.8 1.7-1.399 2.8-1.8 1.101-.4 2.2-.6 3.5-.6.9 0 1.7.1 2.601.399C78.2 42 79 42.4 79.6 43l.7.7c.101.2.2.5.2.7 0 .399-.1.8-.4 1-.3.3-.6.399-1 .399-.199 0-.399 0-.5-.1-.2-.099-.4-.199-.7-.499-.301-.3-.7-.5-1.2-.7s-1-.3-1.7-.3c-.9 0-1.6.2-2.3.5s-1.2.8-1.7 1.3-.8 1.2-1.1 1.9c-.2.699-.4 1.5-.4 2.3s.1 1.5.3 2.2c.2.699.6 1.3 1 1.899.5.5 1 1 1.7 1.3.7.301 1.4.5 2.3.5.7 0 1.3-.1 1.8-.3.4-.099.9-.299 1.3-.699z" /> <path d="M94.6 56.2h-.1c-.6.899-1.4 1.6-2.3 2.1-.9.5-2 .7-3.3.7-.7 0-1.301-.1-2-.3-.7-.2-1.4-.5-1.9-.9-.6-.399-1.1-.899-1.4-1.6-.4-.7-.6-1.5-.6-2.4 0-1.3.3-2.2 1-3 .7-.7 1.6-1.3 2.7-1.7 1.1-.399 2.3-.6 3.7-.699 1.399-.101 2.8-.2 4.199-.2v-.5c0-1.2-.399-2.101-1.1-2.7s-1.7-.9-3-.9c-.7 0-1.4.101-2 .301-.6.199-1.3.5-1.9 1-.3.199-.699.3-1 .3-.3 0-.6-.101-.899-.4-.2-.2-.4-.6-.4-.899 0-.2.101-.5.2-.7s.3-.4.6-.601c.7-.5 1.601-1 2.5-1.3 1-.3 2-.5 3.2-.5s2.2.2 3.101.5c.899.3 1.6.8 2.199 1.4.601.6 1 1.3 1.301 2.1.3.8.399 1.601.399 2.5V56.9c0 .3-.1.6-.2.899-.1.201-.2.401-.4.501-.2.101-.3.2-.5.2s-.3.1-.4.1c-.1 0-.3 0-.399-.1-.2 0-.301-.1-.5-.2-.201-.1-.301-.3-.401-.5s-.2-.5-.2-.899v-.7h-.2zm-.9-5.5c-.8 0-1.7 0-2.5.1-.9.101-1.7.2-2.4.4s-1.3.5-1.8.9-.7 1-.7 1.7c0 .5.101.9.3 1.2.2.3.5.6.801.8.3.2.699.4 1.1.4.4.1.8.1 1.2.1 1.5 0 2.7-.5 3.5-1.399.8-.9 1.2-2.101 1.2-3.5v-.9h-.7v.199z" /> <path d="M111.4 45.4c-.5-.5-1-.801-1.5-1-.5-.2-1.101-.301-1.601-.301-.399 0-.7 0-1.1.101-.4.1-.7.2-1 .399-.3.2-.5.4-.7.7s-.3.601-.3 1c0 .7.3 1.2.899 1.601.601.3 1.601.6 2.801.899.8.2 1.5.4 2.199.7.7.3 1.301.6 1.801 1s.899.8 1.199 1.4c.301.5.4 1.199.4 1.899 0 1-.2 1.8-.6 2.5-.4.7-.9 1.2-1.5 1.7-.601.4-1.301.7-2.101.9-.8.199-1.6.3-2.399.3-1 0-2-.2-2.9-.5-1-.3-1.8-.8-2.5-1.4-.3-.3-.5-.5-.6-.7-.098-.198-.098-.398-.098-.598 0-.4.101-.8.4-1 .3-.3.6-.4 1-.4.399 0 .8.2 1.2.5.5.5 1.1.801 1.699 1.101.601.3 1.2.399 1.9.399.4 0 .8 0 1.2-.1.399-.1.7-.2 1-.4.3-.199.6-.399.8-.699.2-.301.3-.7.3-1.2 0-.8-.399-1.3-1.1-1.7s-1.8-.7-3.2-1c-.6-.1-1.1-.3-1.7-.5-.6-.2-1.1-.5-1.6-.8s-.8-.8-1.101-1.3c-.3-.5-.399-1.2-.399-2 0-.9.2-1.601.5-2.301.401-.6.801-1.2 1.401-1.6.601-.4 1.2-.7 2-.9.7-.199 1.5-.3 2.301-.3.899 0 1.699.101 2.6.4.8.3 1.6.7 2.2 1.2.3.3.5.5.6.699.101.2.101.4.101.601 0 .399-.101.7-.4 1s-.6.399-1 .399c-.402-.199-.802-.399-1.102-.699z" /> <path d="M126 58.4c-.6.3-1.3.399-2.1.399-1.601 0-2.801-.399-3.601-1.3s-1.2-2.2-1.2-3.9v-9H117.2c-.3 0-.601 0-.8-.1-.2-.1-.4-.2-.5-.3-.101-.101-.2-.3-.2-.4 0-.2-.101-.3-.101-.399 0-.101 0-.2.101-.4 0-.2.1-.3.2-.4.1-.1.3-.3.5-.399.199-.101.5-.2.8-.2h1.899v-3.2c0-.399.101-.7.2-.899.101-.2.3-.4.4-.601.2-.1.399-.2.5-.3.2 0 .3-.1.5-.1.1 0 .3 0 .5.1.2 0 .3.1.5.3.2.101.3.3.399.601.101.199.2.6.2.899V42h3.2c.3 0 .6.1.8.2.2.1.3.2.5.399.102.101.202.301.202.401 0 .2.1.3.1.4 0 .1 0 .3-.1.399 0 .2-.1.3-.2.4-.1.1-.3.3-.5.3-.2.1-.5.1-.8.1h-3.2V53.2c0 1 .2 1.7.5 2.1.4.4.8.601 1.4.601.2 0 .5 0 .7-.101.199-.1.399-.1.6-.1.4 0 .7.1.9.399.199.301.3.601.3.9s-.101.5-.2.7c0 .401-.2.601-.5.701z" /> <path d="M133.2 44.8h.1c.4-.899 1-1.7 1.9-2.3.899-.6 1.8-.9 2.899-.9.5 0 1 .101 1.301.301.4.199.6.599.6 1.099 0 .6-.2 1-.6 1.2-.4.2-.801.3-1.4.3h-.2c-1.3 0-2.399.5-3.2 1.4-.8.899-1.3 2.3-1.3 4.1v7c0 .4-.1.7-.2.9-.1.199-.3.399-.399.5-.101.1-.4.199-.5.3-.2 0-.3.1-.5.1-.101 0-.3 0-.5-.1-.2 0-.4-.101-.5-.3-.2-.101-.3-.301-.4-.5-.1-.2-.2-.5-.2-.9V43.5c0-.4.101-.7.2-.9.101-.199.3-.399.4-.5.2-.1.3-.199.5-.199s.3-.101.5-.101c.1 0 .3 0 .399.101.2 0 .301.1.5.199.2.101.301.301.4.5.1.2.2.5.2.9v1.3z" /> </g> <g> <g> <path fill="#08A6DF" d="M70 32.9c-9.1 0-16.5-7.4-16.5-16.5 0-4.8 2.1-9.3 5.7-12.4.5-.4 1.2-.4 1.6.1.4.5.4 1.2-.1 1.6-3.1 2.7-4.9 6.6-4.9 10.7 0 7.8 6.4 14.2 14.2 14.2s14.2-6.4 14.2-14.2c0-7.8-6.4-14.1-14.2-14.1-1.9 0-3.7.4-5.4 1.1-.6.2-1.3 0-1.5-.6-.2-.6 0-1.3.6-1.5C65.7.4 67.8 0 70 0c9.1 0 16.5 7.4 16.5 16.5S79.1 32.9 70 32.9z" /> </g> <g> <path fill="#7C2A8A" d="M70 28.4c-6.6 0-11.9-5.4-11.9-11.9 0-6.6 5.4-11.9 11.9-11.9 5 0 9.5 3.2 11.2 7.9.5 1.3.7 2.6.7 4 0 .6-.5 1.1-1.101 1.1-.6 0-1.1-.5-1.1-1.1 0-1.1-.2-2.2-.601-3.3-1.399-3.8-5-6.4-9.1-6.4-5.3 0-9.6 4.3-9.6 9.6s4.3 9.6 9.6 9.6c.6 0 1.1.5 1.1 1.1.002.8-.498 1.3-1.098 1.3z" /> </g> <g> <path fill="#EC1848" d="M70 23.9c-4.1 0-7.4-3.3-7.4-7.4s3.3-7.4 7.4-7.4c.6 0 1.1.5 1.1 1.1 0 .6-.5 1.1-1.1 1.1-2.8 0-5.1 2.3-5.1 5.1s2.3 5.1 5.1 5.1 5.1-2.3 5.1-5.1c0-.6.5-1.1 1.101-1.1.6 0 1.1.5 1.1 1.1.099 4.2-3.201 7.5-7.301 7.5z" /> </g> </g> </symbol> <symbol id="down" viewBox="0 0 16 16"> <polygon points="3.81 4.38 8 8.57 12.19 4.38 13.71 5.91 8 11.62 2.29 5.91 3.81 4.38" /> </symbol> <symbol id="users" viewBox="0 0 16 16"> <path d="M8,0a8,8,0,1,0,8,8A8,8,0,0,0,8,0ZM8,15a7,7,0,0,1-5.19-2.32,2.71,2.71,0,0,1,1.7-1,13.11,13.11,0,0,0,1.29-.28,2.32,2.32,0,0,0,.94-.34,1.17,1.17,0,0,0-.27-.7h0A3.61,3.61,0,0,1,5.15,7.49,3.18,3.18,0,0,1,8,4.07a3.18,3.18,0,0,1,2.86,3.42,3.6,3.6,0,0,1-1.32,2.88h0a1.13,1.13,0,0,0-.27.69,2.68,2.68,0,0,0,.93.31,10.81,10.81,0,0,0,1.28.23,2.63,2.63,0,0,1,1.78,1A7,7,0,0,1,8,15Z" /> </symbol> <!-- more symbols here --> </svg>
And really, that’s all we need to create our inline SVG sprite.
Header
Moving on with our admin dashboard layout, let’s look at the page header.
Within it, we’ll define a nav
element that will serve as the wrapper for the following elements:
- The logo
- The Collapse button that will toggle the menu on mobile screens
- The menu itself. This will contain the menu links, two headings, a light/dark mode switch, and the collapse/expand button. It might be more semantically correct to have two individual menus and place the headings outside them, but you can approach things differently if you prefer.
Here’s how it’ll look like on wide screens (>767px):
The header structure:
<header class="page-header"> <nav> <a href="#0" aria-label="forecastr logo" class="logo"> <svg width="140" height="49"> <use xlink:href="#logo"></use> </svg> </a> <button class="toggle-mob-menu" aria-expanded="false" aria-label="open menu"> <svg width="20" height="20" aria-hidden="true"> <use xlink:href="#down"></use> </svg> </button> <ul class="admin-menu"> <li class="menu-heading"> <h3>Admin</h3> </li> <li> <a href="#0"> <svg> <use xlink:href="#pages"></use> </svg> <span>Pages</span> </a> </li> <!-- more list items here --> <li> <div class="switch"> <input type="checkbox" id="mode" checked> <label for="mode"> <span></span> <span>Dark</span> </label> </div> <button class="collapse-btn" aria-expanded="true" aria-label="collapse menu"> <svg aria-hidden="true"> <use xlink:href="#collapse"></use> </svg> <span>Collapse</span> </button> </li> </ul> </nav> </header>
Notice two things in the code above:
- How we use the
use
element to reference the target elements. - The ARIA attributes (
aria-expanded
,aria-label
,aria-hidden
) that we add to the toggle buttons. These attributes will help us make the component a bit more accessible. Later, we’ll discuss how their values will be updated based on the button’s state.
Section
The section will contain two nested sections.
<section class="page-content"> <!-- two sections here --> </section>
Section #1
Inside the first section, we’ll place the search form and some info (name, avatar, and notifications) about the current logged-in user.
Here’s its appearance on wide screens (>767px):
The section structure:
<section class="search-and-user"> <form> <input type="search" placeholder="Search Pages..."> <button type="submit" aria-label="submit form"> <svg aria-hidden="true"> <use xlink:href="#search"></use> </svg> </button> </form> <div class="admin-profile"> <span class="greeting">...</span> <div class="notifications"> <span class="badge">...</span> <svg> <use xlink:href="#users"></use> </svg> </div> </div> </section>
Again, notice that we add some ARIA attributes to the submit button.
Section #2
Inside the second section, just for enriching the demo with some dummy content, we’ll place a bunch of article placeholders. These might typically contain tabular data, charts, or feeds of some kind.
“Use a maximum of 5–7 different widgets to create a view. Otherwise, it will be hard for a user to focus and get a clear overview.” – Taras Bakusevych
Here’s its appearance on wide screens (>767px):
The section structure:
<section class="grid"> <article></article> <article></article> <article></article> <article></article> <article></article> <article></article> <article></article> <article></article> </section>
2. Define Some Basic Styles
With the markup for our admin dashboard ready, we’ll forge on with the CSS. The first step, as always, is to specify some CSS variables and common reset styles:
:root { --page-header-bgColor: #242e42; --page-header-bgColor-hover: #1d2636; --page-header-txtColor: #dde9f8; --page-header-headingColor: #7889a4; --page-header-width: 220px; --page-content-bgColor: #f0f1f6; --page-content-txtColor: #171616; --page-content-blockColor: #fff; --white: #fff; --black: #333; --blue: #00b9eb; --red: #ec1848; --border-radius: 4px; --box-shadow: 0 0 10px -2px rgba(0, 0, 0, 0.075); --switch-bgLightModeColor: #87cefa; --switch-sunColor: gold; --switch-moonColor: #f4f4f4; --switch-bgDarkModeColor: #1f1f27; } * { padding: 0; margin: 0; box-sizing: border-box; } ul { list-style: none; } a, button { color: inherit; } a { text-decoration: none; } button { background: none; cursor: pointer; } input { -webkit-appearance: none; } [type="checkbox"] { position: absolute; left: -9999px; } label { cursor: pointer; } button, input { border: none; } svg { display: block; } body { font: 16px/1.5 "Lato", sans-serif; }
Note: for simplicity, I won’t walk through all the CSS rules in the tutorial. There are almost 550 lines of CSS here. If you want to, you can check them all by clicking the CSS tab of the demo project. Besides that, if you are building a production site, it might be more manageable to use a CSS preprocessor like Sass for organizing these styles.
3. Define the Main Dashboard Styles
At this point, we’re ready to concentrate on the page styles.
Style the Header
The header will be a fixed position element. Its width will be 220px, and its height equal to the viewport height. In case its contents exceed the viewport height, a vertical scrollbar will appear.
The nav
element will behave as a flex container with a minimum height of 100%. Remember that its direct children are three:
- The logo
- The mobile menu toggle button
- The menu
The toggle button will be visible only on small screens (<768px). Here are the styles we need:
/*CUSTOM VARIABLES HERE*/ .page-header { position: fixed; top: 0; left: 0; right: 0; bottom: 0; overflow: auto; padding-top: 20px; width: var(--page-header-width); color: var(--page-header-txtColor); background: var(--page-header-bgColor); } .page-header nav { display: flex; flex-direction: column; min-height: 100%; } .page-header .toggle-mob-menu { display: none; }
Tip: in case you prefer an absolutely positioned header that covers the full page height, add the following styles:
body { position: relative; } .page-header { position: absolute; top: 0; left: 0; height: 100%; /*remove these styles*/ /*position: fixed; top: 0; left: 0; right: 0; bottom: 0; overflow: auto;*/ }
Menu Styles
The menu will serve as a flex container, and we’ll give it flex: 1
, so it’ll expand and cover the full parent height.
The last menu item will be given a margin-top: auto
because it should be positioned at the very bottom of the menu. This behavior will be clearer when the header scrollbar doesn’t appear. To test it, try to remove some menu items or check the demo on a tall screen.
The links and button inside the menu will also act as flex containers and their contents (text and icons) should be vertically aligned.
The menu headings will be a bit smaller compared to the other menu elements. Also, we’ll increase the spacing between their characters.
Here’s a part of the menu styles:
/*CUSTOM VARIABLES HERE*/ .page-header .admin-menu { display: flex; flex-direction: column; flex-grow: 1; margin-top: 35px; } .page-header .admin-menu li:last-child { margin-top: auto; margin-bottom: 20px; } .page-header .admin-menu li > * { width: 100%; padding: 12px 15px; } .page-header .admin-menu .menu-heading h3 { text-transform: uppercase; letter-spacing: 0.15em; font-size: 12px; margin-top: 12px; color: var(--page-header-headingColor); } .page-header .admin-menu a, .page-header .admin-menu button { display: flex; align-items: center; font-size: 0.9rem; }
Toggling Dark and Light Themes
To build the switch, we’ll borrow quite a lot of styles from another tutorial which explains how to create a toggle switch. Be sure to have a look and learn how we implement it with the CSS checkbox hack technique.
Our dashboard will support two theme options: the light option and the dark one. By default, the dark will be active and look like this:
When we click on the switch its appearance will change as follows:
Here are the associated styles:
/*CUSTOM VARIABLES HERE*/ .switch label { display: grid; grid-template-columns: auto auto; grid-column-gap: 10px; align-items: center; justify-content: flex-start; } .switch span:first-child { position: relative; width: 50px; height: 26px; border-radius: 15px; box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.4); background: var(--switch-bgLightModeColor); transition: all 0.3s; } .switch span:first-child::before, .switch span:first-child::after { content: ""; position: absolute; border-radius: 50%; } .switch span:first-child::before { top: 1px; left: 1px; width: 24px; height: 24px; background: var(--white); z-index: 1; transition: transform 0.3s; } .switch span:first-child::after { top: 50%; right: 8px; width: 10px; height: 10px; transform: translateY(-50%); background: var(--switch-sunColor); box-shadow: 0 0 4px 2px #ffdb1a; } .switch [type="checkbox"]:checked + label span:first-child { background: var(--switch-bgDarkModeColor); } .switch [type="checkbox"]:focus + label span:first-child { box-shadow: 0 3px 5px rgba(255, 255, 255, 0.25); } .switch [type="checkbox"]:checked + label span:first-child::before { transform: translateX(24px); } .switch [type="checkbox"]:checked + label span:first-child::after { left: 12px; width: 15px; height: 15px; background: transparent; box-shadow: -2px -5px 0 var(--switch-moonColor); transform: translateY(-50%) rotate(-72deg); }
Page Content Styles
Remember that the .page-content
section contains two sub-sections.
This section will be placed 220px away from the left side of the viewport. Plus, we’ll give it width: calc(100% - 220px)
. Note that its left
property value is equal to the header width.
Its styles:
/*CUSTOM VARIABLES HERE*/ .page-content { position: relative; left: var(--page-header-width); width: calc(100% - var(--page-header-width)); min-height: 100vh; padding: 30px; color: var(--page-content-txtColor); background: var(--page-content-bgColor); }
Search and User Styles
Also, remember that the .search-and-user
section contains two elements: the search form and the .admin-profile
.
To lay it out, we’ll use CSS Grid. The search form will cover the full available space, and there will be a 50px gap between it and its sibling. Both siblings will be vertically aligned.
The submit button inside the form will be absolutely positioned. It will only contain a decorative icon, and we’ll therefore need an ARIA attribute to allow screenreaders to interpret it and thereby make it accessible.
The .admin-profile
, which contains two elements, will behave as a flex container with vertically centered content. The badge (counter) element will be absolutely positioned inside its parent with horizontally and vertically centered content.
Here’s a part of the required styles for this section:
/*CUSTOM VARIABLES HERE*/ .search-and-user { display: grid; grid-template-columns: 1fr auto; grid-column-gap: 50px; align-items: center; background: var(--page-content-bgColor); margin-bottom: 30px; } .search-and-user form { position: relative; } .search-and-user form button { position: absolute; top: 50%; right: 15px; transform: translateY(-50%); } .search-and-user .admin-profile { display: flex; align-items: center; } .search-and-user .admin-profile .notifications { position: relative; } .search-and-user .admin-profile .badge { display: flex; align-items: center; justify-content: center; position: absolute; top: -10px; right: -3px; width: 18px; height: 18px; border-radius: 50%; font-size: 10px; color: var(--white); background: var(--red); }
Grid Styles
To lay out the articles on our admin dashboard, we’ll take advantage of CSS Grid. We’ll give all articles a fixed height of 300px. Apart from the first and last articles which will cover the full parent width, all the others will be part of a two-column layout.
The associated styles:
/*CUSTOM VARIABLES HERE*/ .page-content .grid { display: grid; grid-template-columns: repeat(2, 1fr); grid-gap: 30px; } .page-content .grid > article { display: flex; height: 300px; background: var(--page-content-blockColor); border-radius: var(--border-radius); box-shadow: var(--box-shadow); } .page-content .grid > article:first-child, .page-content .grid > article:last-child { grid-column: 1 / -1; }
4. Toggle Header
Each time we click on the collapse/expand button, the header state will change. If it’s expanded, it will collapse (leaving just icon variants of the menu items), and vice versa.
Keep in mind that this functionality will be available only on screens greater than 767px. For smaller screens, our header will have a different layout, which we’ll get to shortly.
During the collapsed state of the header, the body
element receives the collapsed
class. At that point, the following things happen:
- The header shrinks. Its width changes from 220px to 40px.
- In response to this, the
.page-content
section grows. Specifically, its width changes fromwidth: calc(100% - 220px)
towidth: calc(100% - 40px)
. In addition, itsleft
property value becomes 40px instead of 220px. - The logo, the menu headings, the menu links text, the light/dark theme switcher, and the menu button text disappear.
- The
aria-expanded
andaria-label
attribute values of the toggle button are updated. Plus, its icon is rotated 180 degrees, so it looks like an expand icon.
Here’s the JavaScript code that implements this functionality:
const body = document.body; const collapseBtn = document.querySelector(".admin-menu .collapse-btn"); const collapsedClass = "collapsed"; collapseBtn.addEventListener("click", function() { body.classList.toggle(collapsedClass); this.getAttribute("aria-expanded") == "true" ? this.setAttribute("aria-expanded", "false") : this.setAttribute("aria-expanded", "true"); this.getAttribute("aria-label") == "collapse menu" ? this.setAttribute("aria-label", "expand menu") : this.setAttribute("aria-label", "collapse menu"); });
And all the associated styles:
/*CUSTOM VARIABLES HERE*/ @media screen and (min-width: 768px) { .collapsed .page-header { width: 40px; } .collapsed .page-header .admin-menu li > * { padding: 10px; } .collapsed .page-header .logo, .collapsed .page-header .admin-menu span, .collapsed .page-header .admin-menu .menu-heading { display: none; } .collapsed .page-header .admin-menu svg { margin-right: 0; } .collapsed .page-header .collapse-btn svg { transform: rotate(180deg); } .collapsed .page-content { left: 40px; width: calc(100% - 40px); } }
5. Toggle Theme Switch
Each time we click on the toggle switch, the dashboard colors will change.
Remember that initially the dark mode will be enabled. But, as soon as the light mode becomes active, the html
element will receive the light-mode
class.
This class will update/override the values of many of the predefined CSS variables (especially the color ones) and hence change the theme appearance.
Here’s the required JavaScript code:
const html = document.documentElement; const switchLabel = document.querySelector(".switch label"); const switchLabelText = switchLabel.querySelector("span:last-child"); const lightModeClass = "light-mode"; switchInput.addEventListener("input", function () { html.classList.toggle(lightModeClass); if (html.classList.contains(lightModeClass)) { switchLabelText.textContent = "Light"; } else { switchLabelText.textContent = "Dark"; } });
And the associated styles:
/*CUSTOM VARIABLES HERE*/ .light-mode { --page-header-bgColor: #f1efec; --page-header-bgColor-hover: #b9e4e0; --page-header-txtColor: #2c303a; --page-header-headingColor: #979595; --page-content-bgColor: #fff; --box-shadow: 0 0 10px -2px rgba(0, 0, 0, 0.25); } .light-mode .page-header .admin-menu a:hover, .light-mode .page-header .admin-menu a:focus, .light-mode .page-header .admin-menu button:hover, .light-mode .page-header .admin-menu button:focus { color: var(--black); } .light-mode .page-header .logo svg, .light-mode .page-header .admin-menu a:hover svg, .light-mode .page-header .admin-menu a:focus svg, .light-mode .page-header .admin-menu button:hover svg, .light-mode .page-header .admin-menu button:focus svg { fill: var(--black); } .light-mode .switch [type="checkbox"]:focus + label span:first-child { box-shadow: 0 3px 5px rgba(0, 0, 0, 0.25); } @media screen and (max-width: 767px) { .light-mode .search-and-user .admin-profile svg { fill: var(--black); } }
Persist Selected Mode on Page Load
But we haven’t finished yet! It would be really nice if we could store the user’s theme preference and enable it each time they revisit the admin. We can achieve this by taking advantage of local storage.
Let’s look at the JavaScript code that will handle this logic:
... if (localStorage.getItem("dark-mode") === "false") { html.classList.add(lightModeClass); switchInput.checked = false; switchLabelText.textContent = "Light"; } switchInput.addEventListener("input", function () { if (html.classList.contains(lightModeClass)) { localStorage.setItem("dark-mode", "false"); } else { localStorage.setItem("dark-mode", "true"); } });
From here, you can go deeper and make even more complex assumptions by using the prefers-color-scheme
CSS media feature. For example, if the user hasn’t selected any mode, you can display the one that respects their operating system preference. But for now, let’s keep things simple.
6. Show Tooltip on Admin Menu Items
At this point, let’s add another new feature to the collapsible header.
As we discussed in the previous section, when the header becomes collapsed, the text of the menu links will disappear. This means, at that point, only the SVG icons will be visible. So, let’s display a tooltip that will give users a better understanding of what each link does.
To do so, each time a menu link (icon) is being hovered over, we’ll add the title
attribute to it, with the value being its span
’s text. But again, that should happen only when the header is collapsed and the window width is at least 768px.
Here’s the corresponding JavaScript:
const body = document.body; const menuLinks = document.querySelectorAll(".admin-menu a"); const collapsedClass = "collapsed"; for (const link of menuLinks) { link.addEventListener("mouseenter", function () { if ( body.classList.contains(collapsedClass) && window.matchMedia("(min-width: 768px)").matches ) { const tooltip = this.querySelector("span").textContent; this.setAttribute("title", tooltip); } else { this.removeAttribute("title"); } }); }
7. Going Responsive
On screens up to 767px wide, our page will look like this:
That’s a big difference from our sidebar arrangement, right? Let’s highlight the most important differences compared to the desktop version:
- Both the header and
.page-content
haveposition: static
andwidth: 100%
. - The flex direction of the
nav
element changes fromcolumn
torow
. - The mobile menu toggle button becomes visible.
- The menu is absolutely positioned right underneath the header and initially hidden. It will become visible each time we click on the toggle button.
- The collapse/expand button and the
.greeting
element are hidden. - The
.search-and-user
section is absolutely positioned and placed right next to the mobile menu toggle button.
Below you can see a part of the responsive styles:
@media screen and (max-width: 767px) { .page-header, .page-content { position: static; width: 100%; } .page-header nav { flex-direction: row; } .page-header .toggle-mob-menu { display: block; } .page-header .admin-menu { position: absolute; left: 98px; top: 57px; margin-top: 0; z-index: 2; border-radius: var(--border-radius); background: var(--page-header-bgColor); visibility: hidden; opacity: 0; transform: scale(0.95); transition: all 0.2s; } .page-header .admin-menu li:last-child button, .search-and-user .admin-profile .greeting { display: none; } .search-and-user { position: absolute; left: 131px; top: 10px; padding: 0; grid-column-gap: 5px; width: calc(100% - 141px); border-radius: var(--border-radius); background: transparent; } }
8. Toggle Mobile Menu
Each time we click on the toggle button, the menu state will change. If it’s expanded, it will collapse, and vice versa.
During the expanded state of the menu, the body
element receives the mob-menu-opened
class. At that point, the following things happen:
- The menu appears
- The
aria-expanded
andaria-label
attribute values of the toggle button are updated. Plus, its icon is rotated 180 degrees, so it looks like an expand icon
Here’s the required JavaScript code:
const body = document.body; const toggleMobileMenu = document.querySelector(".toggle-mob-menu"); toggleMobileMenu.addEventListener("click", function() { body.classList.toggle("mob-menu-opened"); this.getAttribute("aria-expanded") == "true" ? this.setAttribute("aria-expanded", "false") : this.setAttribute("aria-expanded", "true"); this.getAttribute("aria-label") == "open menu" ? this.setAttribute("aria-label", "close menu") : this.setAttribute("aria-label", "open menu"); });
And the associated CSS:
.page-header .toggle-mob-menu svg { transition: transform 0.2s; } .page-header .admin-menu { transition: all 0.2s; } .mob-menu-opened .toggle-mob-menu svg { transform: rotate(180deg); } .mob-menu-opened .page-header .admin-menu { transform: scale(1); visibility: visible; opacity: 1; }
Conclusion
That’s it, folks! We successfully built a fully functional admin dashboard layout. You’ll be able to expand on this foundation to create all kinds of admin interfaces. Hopefully, you enjoyed this journey as much as I did!
A Note on Accessibility
Just a last note: I’m certainly not an accessibility expert, yet I tried to make this UI more accessible by adding some common ARIA attributes. During this process, I checked the WordPress and CodePen dashboards for reference. There might be additional ARIA attributes that could have been included in the code. For example, I excluded the aria-controls
attribute which is responsible for identifying the related content, but that was because Aria-Controls is Poop.
As a next step, make sure to optimize the code by using a CSS preprocessor and group the JavaScript code into functions. And if you plan to use this admin in a real project, I’d love to know it!
As always, thanks for reading!
No comments:
Post a Comment