Behive Tech Registry

Sidebar Layout

v0.4.0

A dumb CSS Grid skeleton with sidebar, topbar, and content regions. It has no opinion on what the sidebar or topbar contain — the elements passed in are responsible for their own `grid-area: sidebar` / `grid-area: topbar` placement (e.g. via `@behivetech/atoms.sidebar`).

pnpm add @behivetech/templates.sidebar-layout

registry/behivetech/templates/sidebar-layout · depends on @behivetech/get-class-name

sidebar-layout.tsx
1import { type HTMLAttributes, type ReactNode } from "react";
2import { getClassName } from "@behivetech/get-class-name";
3import styles from "./sidebar-layout.module.scss";
4
5export interface SidebarLayoutProps extends HTMLAttributes<HTMLDivElement> {
6 /** Additional class names to merge with the component root element */
7 className?: string;
8 /** Sidebar region content; the element rendered here owns its own `grid-area: sidebar` placement */
9 sidebar: ReactNode;
10 /** Topbar region content; the element rendered here owns its own `grid-area: topbar` placement */
11 topbar: ReactNode;
12 /** Main content region */
13 children?: ReactNode;
14}
15
16/**
17 * Dumb CSS Grid skeleton with sidebar, topbar, and content regions.
18 * Composition of the actual sidebar/topbar/content is left entirely to the
19 * consumer — this only owns the grid tracks and the content scroll area.
20 */
21export const SidebarLayout = ({
22 className,
23 sidebar,
24 topbar,
25 children,
26 ...rest
27}: SidebarLayoutProps) => {
28 const [rootClass, getChildClass] = getClassName({
29 className,
30 rootClass: "BHT__SidebarLayout",
31 styles,
32 });
33
34 return (
35 <div {...rest} className={rootClass}>
36 {sidebar}
37 <div className={getChildClass("main")}>
38 {topbar}
39 <main className={getChildClass("content")}>
40 <div className={getChildClass("inner")}>{children}</div>
41 </main>
42 </div>
43 </div>
44 );
45};
sidebar-layout.module.scss
1.BHT__SidebarLayout {
2 display: grid;
3 grid-template-columns: var(--sidebar-layout-sidebar-width, 13.75rem) 1fr;
4 grid-template-rows: var(--sidebar-layout-topbar-height, 3.5rem) 1fr;
5 grid-template-areas:
6 "sidebar topbar"
7 "sidebar content";
8 min-height: 100vh;
9
10 &__main {
11 grid-column: 2;
12 display: contents;
13 }
14
15 &__content {
16 grid-area: content;
17 background: var(--md-sys-color-background, var(--color-background));
18 color: var(--md-sys-color-on-background, var(--color-text-body));
19 overflow-y: auto;
20 }
21
22 &__inner {
23 padding: var(--spacing-8);
24 max-width: var(--sidebar-layout-content-max-width, 75rem);
25 }
26}
index.ts
1export { SidebarLayout } from "./sidebar-layout.js";
2export type { SidebarLayoutProps } from "./sidebar-layout.js";
sidebar-layout.composition.tsx
1import { SidebarLayout } from "./sidebar-layout.js";
2
3export const BasicSidebarLayout = () => (
4 <SidebarLayout sidebar={<nav>Sidebar</nav>} topbar={<header>Topbar</header>}>
5 Page content
6 </SidebarLayout>
7);
sidebar-layout.spec.tsx
1import { render, screen } from "@testing-library/react";
2import { SidebarLayout } from "./sidebar-layout.js";
3
4describe("SidebarLayout", () => {
5 it("renders children", () => {
6 render(
7 <SidebarLayout sidebar={<nav>Nav</nav>} topbar={<header>Topbar</header>}>
8 Hello
9 </SidebarLayout>,
10 );
11 expect(screen.getByText("Hello")).toBeTruthy();
12 });
13
14 it("renders the sidebar and topbar content", () => {
15 render(
16 <SidebarLayout sidebar={<nav>Nav</nav>} topbar={<header>Topbar</header>}>
17 Content
18 </SidebarLayout>,
19 );
20 expect(screen.getByText("Nav")).toBeTruthy();
21 expect(screen.getByText("Topbar")).toBeTruthy();
22 });
23
24 it("applies additional className", () => {
25 const { container } = render(
26 <SidebarLayout className="custom" sidebar={<nav />} topbar={<header />}>
27 Content
28 </SidebarLayout>,
29 );
30 expect((container.firstChild as HTMLElement).className).toContain("custom");
31 });
32});