Skip to main content

Layouts

Overview

We define overall page structure in the form of layouts that will be presented to the user based on a specific state or a specific page. Those are considered as components that are shared across the pages and renders the application shell.

We can define different layouts inside layouts folder:

  • an app could have an Admin dashboard on /admin route which has a fixed header and a sidebar on the left side and scrollable content in the middle

    src
    └── layouts
    ├── AdminLayout
    └── AdminLayout.tsx
    ├── MainLayout
    └── MainLayout.tsx
    └── AccountLayout
    └── AccountLayout.tsx
  • an app could have only one page layout, regarding of auth state or specific page (same layout for every user and page) which can then be used as a wrapper around the app so every page can render the same shell (navigations, footers, ...)

    src
    └── layouts
    ├── PageLayout
    └── PageLayout.tsx

Almost every project has some form of navigation. Most used one is top navigation which we will call Navigation and/or sidebar navigation (Sidebar). These navigation layouts are used to support different app shell styles. They could handle user logged-in/logged-out states, mobile/desktop layouts, etc... In our example we will focus on having one layout and multiple navigations that define app shell.

src
└── layouts
       └── Layout.tsx
└── components    
        ├── Navigation
        |    └── Navigation.tsx
|    └── DesktopNavigation
| └── DesktopNavigation.tsx
|    └── MobileNavigation
| └── MobileNavigation.tsx
     ├── Sidebar
     └── Sidebar.tsx
// src/layouts/PageLayout/PageLayout.tsx

<>
<div className='page-layout'>
<Navigation />
<Sidebar />
<main className=`page-layout__content`>
// in here we can add a container element
// as a wrapper around page content or
// we can use it per-page basis
{children}
</main>
</div>

<InvitationDialog />
</>

Or if we want to use navigations and page content into a container for better readability and to prevent overflows and stretched elements on big screens.

// src/layouts/PageLayout/PageLayout.tsx

<>
<div className='page-layout'>
<div className='page-layout__container'>
<Navigation />
<Sidebar />
<main className=`page-layout__content`>
{children}
</main>
</div>
</div>

<InvitationDialog />
</>

Of course some projects may even require different navigations for every specific page. In that situation, we would leave the structure as mentioned above, but we would use those navigations in the page file itself, per-page need.



Every website or web application needs some kind of navigation so the user can navigate through pages. We define our main navigation component, which will act as a combination of DesktopNavigation and MobileNavigation.

// src/components/Navigation/Navigation.tsx

import DesktopNavigation from "../../components/DesktopNavigation";
import MobileNavigation from "../../components/MobileNavigation";

const Navigation = () => {
return (
<nav className="navbar">
<DesktopNavigation />
<MobileNavigation />
</nav>
);
};

export default Navigation;

Navigation component has following styles:

// src/components/Navigation/Navigation.scss

.navigation {
display: flex;
align-items: center;
justify-content: space-between;
background-color: black;
padding: 2rem;

.desktop-navigation {
&__menu {
display: flex;
justify-content: flex-end;

@include sm {
display: none;
}
}

&__link {
margin-left: 1rem;

&--active {
color: green;
}
}
}

.mobile-navigation {
display: none;

@include sm {
display: block;
}
}
}

Additionally, we could have desktop and mobile navigation styles in their own mixins (https://sass-lang.com/documentation/at-rules/mixin) or placeholder classes (https://sass-lang.com/documentation/style-rules/placeholder-selectors) and use them inside of Navigation.scss file.

So, the Navigation is divided to desktop and mobile versions. Desktop navigation has it's menu displayed with all the links until it hits that sm width breakpoint. After sm breakpoint is hit, we display mobile navigation that has a button which is a toggle for SideDrawer component which has all links combined from DesktopNavigation and MobileNavigation.


Desktop navigation

DesktopNavigation would look something like this:

// src/components/Navigation/DesktopNavigation/DesktopNavigation.tsx

import { navigationLinks } from "../shared/contants";

export default function DesktopNavigation() {
return (
<>
<a href={HOME_PAGE} className="desktop-navigation__logo">
Logo
</a>

<ul className="desktop-navigation__menu">
{navigationLinks.map((navLink) => (
<li className="desktop-navigation__item" key={navLink.id}>
<a className="desktop-navigation__link" href={navLink.url}>
{navLink.text}
</a>
</li>
))}
</ul>
</>
);
}

Mobile navigation

MobileNavigation would look something like this:

// src/components/Navigation/MobileNavigation/MobileNavigation.tsx

import { useState } from "react";
import { SideDrawer } from "./SideDrawer";

export default function MobileNavigation() {
const [isMobileNavigationOpened, setIsMobileNavigationOpened] = useState(false);

const handleToggleSideDrawer = () => {
setIsMobileNavigationOpened((value) => !value);
};

return (
<div className="mobile-navigation">
<button className="toggle-drawer-button" onClick={handleToggleSideDrawer}>
{isMobileNavigationOpened ? "Close " : "Open "}
mobile nav
</button>

<SideDrawer isOpened={isMobileNavigationOpened} />
</div>
);
}

Side drawer

SideDrawer is a React Portal component, meaning that we render it outside the DOM hierarchy of the parent component (https://beta.reactjs.org/reference/react-dom/createPortal). Its contains all links combined from both DesktopNavigation and MobileNavigation components. It also has isOpened prop which is coming from MobileNavigation component in which we put SideDrawer itself and with that prop we can toggle SideDrawer as we want.

// src/components/SideDrawer/SideDrawer.tsx

import { createPortal } from "react-dom";
import { sideDrawerLinks } from "../shared/contants";

type SideDrawerProps = {
isOpened: boolean;
};

export const SideDrawer = ({ isOpened }: SideDrawerProps) => {
return createPortal(
<div className={`side-drawer ${isOpened ? "side-drawer--active" : "side-drawer--hidden"}`}>
<ul className="side-drawer__menu">
{sideDrawerLinks.map((sideDrawerLink) => (
<li className="side-drawer__item" key={sideDrawerLink}>
<a href="#" className="side-drawer__link">
{sideDrawerLink}
</a>
</li>
))}
</ul>
</div>,
document.body
);
};

Besides Navigation, we can add the sidebar and hide it on smaller screens so it doesn't get in the way of the app content.

// src/components/Sidebar/Sidebar.tsx

import { sidebarLinks } from "../shared/contants";

export const Sidebar = () => {
return (
<aside className="sidebar">
<h3 className="sidebar__title">Sidebar</h3>

<ul className="sidebar__menu">
{sidebarLinks.map((sidebarLink) => (
<li className="sidebar__item" key={sidebarLink}>
<a href="#" className="sidebar__link">
{sidebarLink}
</a>
</li>
))}
</ul>
</aside>
);
};

You can see that we are following BEM class naming syntax. Based on this, we define our sidebar.scss file.

// src/components/Sidebar/sidebar.scss

.sidebar {
display: flex;
flex-direction: column;
background-color: rgb(84, 43, 43);
min-width: 25rem;
height: calc(100vh - 6.3rem); // 6.3 = height of header navigation
padding: 2rem;

@include sm {
display: none;
}

&__title {
color: white;
font-size: 3rem;
margin-bottom: 2rem;
}

&__item {
margin-bottom: 1rem;
}
}

We hide the sidebar on every screen that is smaller than what is define in sm breakpoint. You can see more about breakpoints and styles in general at SCSS and BEM.


Styling

We chose vanilla CSS/SCSS and/or styled components as our main way of styling elements. Below is an example of using style files scoped to the components in which they are gonna be used. Below you can see example of folder structure

src
└── layouts
├── PageLayout
└── PageLayout.tsx
└── pageLayout.scss
└── components
├── Navigation
└── Navigation.tsx
└── Navigation.scss
   └── DesktopNavigation
└── DesktopNavigation.tsx
└── DesktopNavigation.scss
   └── MobileNavigation
└── MobileNavigation.tsx
└── MobileNavigation.scss
├── Sidebar
└── Sidebar.tsx
└── sidebar.scss

On big screens we would have Navigation and Sidebar present as a shell that wraps app content. On smaller screens, we would want to clear the screen as much as possible so that user can have space to see the app content itself. To do that, we would have to hide or display those navigations, based on user actions.

Below is the example of main Layout.tsx file that represents our app shell structure. In there, we define how the user will see our navigations and layouts in general.

// src/layouts/PageLayout/PageLayout.tsx

import Navbar from "./NavbarLayout/Navbar";
import { Sidebar } from "./Sidebar";

interface LayoutProps {
children: React.ReactNode;
}

export const Layout = ({ children }: LayoutProps) => {
return (
<div className="layout">
<Navbar />

<div className="layout__container">
<Sidebar />
<main className="layout__content">{children}</main>
</div>
</div>
);
};

We define basic styles for PageLayout that's gonna be the base for all our other styles and, of course, it's prone to changes based on project needs:

// src/layouts/PageLayout/pageLayout.scss

.page-layout {
&__container {
display: flex;
}

&__content {
width: 100%;
padding: 2rem;
}
}