Building a responsive sidebar application layout
I wanted to share a CSS and webcomponent layout that I’ve been pretty happy with in a few projects. With webcomponents now widely supported, I was able to remove one of the last bit of inline scripts I had left in docket converting it to a webcomponent. The layout offers a layout with a 250px sidebar and content area. In smaller viewports the menu collapses to the side, but can be opened with a full height narrow button that toggles the sidebar to its normal width. I wanted to try out more of the newer feature in CSS that I’ve not had a chance to play with much having been working on backend code in my day job for the last couple years.
Our application layout is going to require a small amount of structural HTML:
- <div class="layout-three-quarter">
- <side-bar data-expanded="false">
- <div class="menu">
- <ul>
- <li>Projects</li>
- <li>People</li>
- <li>Settings</li>
- </ul>
- </div>
- <button class="expander" title="Show menu" data-expander="1">
- =
- =
- =
- </button>
- </side-bar>
- <section class="content">
- application content goes here
- </section>
- </div>
This HTML gives us a minimal skeleton to gets us a sidebar container, a menu that can scroll and collapse, and a toggle button that we’ll add some interaction to. Don’t worry too much about the side-bar
element, we’ll be defining that with a webcomponent later. Our layout skeleton contains the application body, and the styling for this structural HTML looks like:
- :root {
- /* I really like using CSS custom properties for layout and design variables.
- Custom properties replace magic numbers and let you get one of the most
- useful features of CSS preprocessors with no tooling.
- */
- --sidebar-width: 250px;
- --space: 8px;
- /* Separate our palette from use cases.
- Makes restyling simpler in the future, and helps build design language on your team.
- Having variables for colors also makes adding alternate palettes and themes very easy
- as you can redefine the variables with more specific selectors to retheme almost everything.
- */
- --color-gray-100: #f0ecea;
- --color-bg-low: var(--color-gray-100);
- }
- .layout-three-quarter {
- height: 100%;
- padding-left: var(--sidebar-width);
- }
- side-bar,
- .content {
- /* Math in CSS replaces a bunch of scenarios I would use sass for */
- padding-bottom: calc(var(--space) * 5);
- }
- side-bar {
- position: fixed;
- top: 0;
- left: 0;
- height: 100%;
- width: var(--sidebar-width);
- padding-top: calc(var(--space) * 3);
- background: var(--color-bg-low);
- transition: left 0.26s ease-in-out;
- }
- .content {
- padding-top: calc(var(--space) * 5);
- padding-right: calc(var(--space) * 5);
- padding-left: calc(var(--space) * 5);
- }
This is the basic structure of the layout of the application. We have a fixed width sidebar. The interior of the menu is styled with some padding.
- .menu {
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- gap: calc(var(--space) * 3);
- padding-left: calc(var(--space) * 5);
- height: 100%;
- overflow-y: auto;
- }
- .expander {
- display: none;
- }
When we’re in a mobile viewport we hide the menu:
- :root {
- --z-active-ui: 100;
- --color-darken: #333;
- }
- @media (max-width: 600px) {
- .layout-three-quarter {
- padding-left: 10px;
- }
- .content {
- padding-top: calc(var(--space) * 3);
- padding-left: calc(var(--space) * 4);
- padding-right: calc(var(--space) * 2);
- }
- side-bar {
- transition: transform 0.25s ease-in;
- transform: translateX(calc(var(--sidebar-width) * -1 + 10px));
- z-index: var(--z-active-ui);
- }
- .expander {
- cursor: pointer;
- display: block;
- justify-content: center;
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- width: 10px;
- height: 100%;
- background: color-mix(in srgb, var(--color-bg-low), var(--color-darken) 20%);
- padding: 0;
- border: none;
- border-radius: 0;
- }
- .expander:focus,
- .expander:hover {
- box-shadow: none;
- }
- side-bar[data-expanded="true"] {
- transform: translateX(0px);
- }
- }
If you’ve not written CSS in a while, I’m using CSS custom properties and calc() here. These are well supported features that help simplify maintenance of CSS and reduce reliance on pre-processors.
Toggle Behavior
For our mobile menu toggle behavior, we’ll use a webcomponent. While Shadow DOM is a marquee feature of webcomponents, shadow DOM has a few sharp edges when it comes to CSS, so I’ve been avoiding it. Instead I use template fragments in my view rendering to compose complex elements all in ‘light DOM’. Our webcomponent for this sidebar is quite simple:
- class SideBar extends HTMLElement {
- connectedCallback() {
- const button = this.querySelector('[data-expander]');
- if (!button) {
- console.error('Could not find expander for side-bar');
- return;
- }
- const sidebar = this;
- button.addEventListener('click', function (evt) {
- evt.preventDefault();
- const current = sidebar.dataset.expanded;
- sidebar.dataset.expanded = current === 'false' ? 'true' : 'false';
- });
- }
- }
- customElements.define('side-bar', SideBar);
This can be added to a script module tag on the page, or as a separate file and included in an asset bundler you’re using. Webcomponents can be great if you need to maintain organization specific components. Whether you make your components with react or with server rendered HTML, webcomponents can provide a decent complexity off-ramp for some applications. So there we have it, a simple reusable layout component. I’ve put a demo of this code together in codepen .
There are no comments, be the first!