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 middlesrc
└── 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.
Navigations
Main navigation
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;
}
}