modern-react-spa

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-react or draggable/resizable dashboards with react-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

NeedPick
Static page layout (header / sidebar / content)Flexbox or CSS Grid
Card grid that wrapsCSS Grid repeat(auto-fit, minmax)
Component adapts to its columnContainer queries
IDE-style tabbed panels, user drag/dropflexlayout-react
Dashboard widgets the user resizesreact-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:

  1. 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.
  2. Virtualize long lists inside dockable panels. A 50,000-line log inside flexlayout-react will freeze on splitter drag without virtualisation. Cross-link Ch 32.3 for @tanstack/react-virtual / react-virtualized / react-window.
  3. Animate on the compositor. transform and opacity stay on the GPU; width and height trigger layout/paint. react-grid-layout uses transform correctly out of the box.

🪤 Common Pitfalls

  1. Using JS layout engines for static pages — CSS does it cheaper.
  2. Forgetting container-type@container queries silently no-op.
  3. Saving layout but not restoring on mount.
  4. Not memoising widget content → drag re-renders everything.
  5. Mixing flexlayout-react and shadcn Dialog → portal/z-index conflicts.
  6. 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-react for tabbed dockable workspaces; react-grid-layout for 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

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.

Topics in this chapter (7)
  1. 9.1 Modern CSS Flexbox patterns
  2. 9.2 CSS Grid for page layouts
  3. 9.3 Container queries
  4. 9.4 flexlayout-react — dockable tabbed workspaces
  5. 9.5 react-grid-layout — draggable/resizable dashboards
  6. 9.6 Decision matrix
  7. 9.7 Performance inside layout containers