Chapter 09
Layouts — From CSS Flexbox/Grid to Dockable Workspaces
Modern CSS layouts for React — Flexbox, Grid, container queries, and JS-driven dockable workspaces (flexlayout-react, react-grid-layout).
Published 2026-05-23
🎯 Chapter Goal — After this chapter you can express any common page layout in modern CSS with no JS; choose between Flexbox, Grid, container queries, and JS-driven layout engines for a given screen; and build IDE-style dockable workspaces with
flexlayout-reactor draggable/resizable dashboards withreact-grid-layout, including persistence.🧭 Prerequisites — Ch 7 (shadcn) and Ch 8 (tokens). Working knowledge of CSS Flexbox and Grid basics.
🔹 9.1 Modern CSS Flexbox patterns
Most “layout JS” code solves problems CSS already solves. Patterns that actually come up:
Holy Grail
.shell { display: flex; flex-direction: column; min-height: 100vh; }
.shell-main { flex: 1; display: flex; } /* takes remaining height */
.shell-side { flex: 0 0 240px; } /* fixed-width sidebar */
.shell-content { flex: 1; min-width: 0; } /* min-width:0 prevents overflow */
┌────────────────────────────────────────────┐
│ Header │
├──────────────┬─────────────────────────────┤
│ Sidebar │ Content (flex: 1) │
│ 240px │ │
├──────────────┴─────────────────────────────┤
│ Footer │
└────────────────────────────────────────────┘
Sticky footer
body { min-height: 100vh; display: flex; flex-direction: column; }
main { flex: 1; }
footer { /* automatically pushed to bottom when main is short */ }
Auto-wrapping tag row
.tags { display: flex; flex-wrap: wrap; gap: 0.5rem; }
Equal-height cards
.row { display: flex; gap: 1rem; align-items: stretch; }
.card { flex: 1; }
“Logical end” alignment (push one item to the far side)
.bar { display: flex; gap: 1rem; }
.bar .pushed { margin-inline-start: auto; }
The margin-inline-start: auto trick beats justify-content: space-between when only one item needs to flex away — preserves natural spacing for the others.
🔹 9.2 CSS Grid for page layouts
Why Grid over Flexbox — two-dimensional layouts (rows AND columns simultaneously), named areas, alignment across rows and columns.
grid-template-areas — readable, declarative shells:
.shell {
display: grid;
min-height: 100vh;
grid-template-columns: 240px 1fr;
grid-template-rows: 56px 1fr 40px;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
}
.shell-header { grid-area: header; }
.shell-sidebar { grid-area: sidebar; }
.shell-main { grid-area: main; }
.shell-footer { grid-area: footer; }
Responsive card grid — the auto-fit + minmax pattern:
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem;
}
One card per row on narrow screens; four columns on wide screens; no media queries.
auto-fit vs auto-fill: when columns can’t fill the row, auto-fit collapses empty tracks (cards stretch to fill); auto-fill keeps the empty tracks (cards stay at minmax min).
subgrid — children inherit the parent’s column lines. Use for aligning rows of unequal-height items across cards:
.row { display: grid; grid-template-columns: 1fr 2fr 1fr; }
.row-item { display: grid; grid-template-columns: subgrid; }
🔹 9.3 Container queries
Components adapt to their container, not the viewport. The 2026 default for component-level responsiveness — covered in detail in Ch 8 §8.9.
.widget-wrap { container-type: inline-size; }
.widget {
display: grid;
gap: 1rem;
}
@container (min-width: 400px) {
.widget { grid-template-columns: 1fr 1fr; }
}
The same <InvoiceCard> at 320 / 480 / 720 px renders three different layouts.
🔹 9.4 flexlayout-react — dockable tabbed workspaces
Some UIs need user-rearrangeable tabbed regions — IDEs, log explorers, dev tools, audio/video editors. flexlayout-react (https://www.npmjs.com/package/flexlayout-react) is the canonical React library for this.
npm install flexlayout-react
📄 layout JSON — the persisted shape
const json: IJsonModel = {
global: { tabEnableClose: false },
borders: [],
layout: {
type: 'row',
children: [
{ type: 'tabset', weight: 25, children: [{ type: 'tab', name: 'Files', component: 'files' }] },
{ type: 'tabset', weight: 50, children: [{ type: 'tab', name: 'Log', component: 'log' }] },
{ type: 'tabset', weight: 25, children: [{ type: 'tab', name: 'Details', component: 'details' }] },
],
},
};
📄 the component
import { Layout, Model, type TabNode } from 'flexlayout-react';
import 'flexlayout-react/style/light.css';
const model = Model.fromJson(json);
const factory = (node: TabNode) => {
switch (node.getComponent()) {
case 'files': return <FileTree />;
case 'log': return <LogStream />;
case 'details': return <Details />;
default: return null;
}
};
export const Workspace = () => (
<Layout
model={model}
factory={factory}
onModelChange={(m) => localStorage.setItem('layout', JSON.stringify(m.toJson()))}
/>
);
What you get out of the box:
- Drag a tab → docks beside another tabset.
- Splitter handles between tabsets.
- Pop a tab out as a floating window.
- “Reset to default layout” — just call
Model.fromJson(defaultJson).
┌─ Log Explorer ──────────────────────────────────────────┐
│ ┌── Files ──┐ ┌── Log [stream.log] [errors.log] ─────┐ │
│ │ app/ │ │ 2026-05-22 12:00:01 INFO start... │ │
│ │ ▾ logs/ │ │ 2026-05-22 12:00:02 WARN retry... │ │
│ │ err.log│ │ 2026-05-22 12:00:03 ERROR timeout │ │
│ └───────────┘ └──────────────────────────────────────┘ │
│ ┌── Details ────────────────────────┐ │
│ │ Stack trace, request id, … │ │
│ └──────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
▲ ▲
tab drag handle splitter (drag to resize)
🔹 9.5 react-grid-layout — draggable/resizable dashboards
For analytics, monitoring, BI tools where every user wants their own widget arrangement.
npm install react-grid-layout
import GridLayout, { type Layout } from 'react-grid-layout';
import 'react-grid-layout/css/styles.css';
import 'react-resizable/css/styles.css';
const initialLayout: Layout[] = [
{ i: 'kpi-1', x: 0, y: 0, w: 3, h: 2 },
{ i: 'chart-1', x: 3, y: 0, w: 6, h: 4 },
{ i: 'table-1', x: 0, y: 2, w: 12, h: 6 },
];
export const Dashboard = () => {
const [layout, setLayout] = useState<Layout[]>(() =>
JSON.parse(localStorage.getItem('dash-layout') ?? 'null') ?? initialLayout,
);
return (
<GridLayout
className="layout"
layout={layout}
cols={12}
rowHeight={48}
width={1200}
onLayoutChange={(next) => {
setLayout(next);
localStorage.setItem('dash-layout', JSON.stringify(next));
}}
>
<div key="kpi-1"><KpiCard /></div>
<div key="chart-1"><RevenueChart /></div>
<div key="table-1"><InvoiceTable /></div>
</GridLayout>
);
};
Items are positioned by the i key in the layout array, matched to the key on each child.
View vs edit modes: add an isDraggable={editMode} / isResizable={editMode} prop driven by a toggle. Read-only by default; opt into rearrangement.
🔹 9.6 Decision matrix
| Need | Pick |
|---|---|
| Static page layout (header / sidebar / content) | Flexbox or CSS Grid |
| Card grid that wraps | CSS Grid repeat(auto-fit, minmax) |
| Component adapts to its column | Container queries |
| IDE-style tabbed panels, user drag/drop | flexlayout-react |
| Dashboard widgets the user resizes | react-grid-layout |
| Drag-to-reorder list | @dnd-kit (mentioned, not covered) |
🔹 9.7 Performance inside layout containers
Layout JS libraries re-render their entire child set on drag/resize events. Three rules:
- Memoize widget content. Without it, your
<RevenueChart>re-renders during every drag tick. The React 19 compiler handles most cases (Ch 2 §2.1); verify with the Profiler. - Virtualize long lists inside dockable panels. A 50,000-line log inside
flexlayout-reactwill freeze on splitter drag without virtualisation. Cross-link Ch 32.3 for@tanstack/react-virtual/react-virtualized/react-window. - Animate on the compositor.
transformandopacitystay on the GPU;widthandheighttrigger layout/paint.react-grid-layoutusestransformcorrectly out of the box.
🪤 Common Pitfalls
- Using JS layout engines for static pages — CSS does it cheaper.
- Forgetting
container-type→@containerqueries silently no-op. - Saving layout but not restoring on mount.
- Not memoising widget content → drag re-renders everything.
- Mixing
flexlayout-reactand shadcn Dialog → portal/z-index conflicts. - Long lists inside dockable panels without virtualisation → freeze on resize.
✅ Recap
- Default to CSS. Reach for JS layout only when users will rearrange the UI.
flexlayout-reactfor tabbed dockable workspaces;react-grid-layoutfor resizable dashboards.- Persist layout JSON; offer “reset to default.”
- Virtualise content inside dynamic layouts.
- Container queries replace most media queries for component-level responsiveness.
🔗 Further Reading
- https://www.npmjs.com/package/flexlayout-react — FlexLayout React (dockable panes).
- https://github.com/react-grid-layout/react-grid-layout — react-grid-layout (draggable grid).
- https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_container_queries — MDN container-queries reference.
- Josh Comeau — “Interactive guide to Flexbox” / “Interactive guide to CSS Grid”.
- Ch 32.3 — virtualisation deep dive.
In the book — not on the site
Each topic has an 🧠 Under-the-hood subsection — the algorithm, the data structures, what React DevTools surfaces, debugging recipes. Plus a 🧪 hands-on lab per chapter with a starter repo. Reserved for the book.