Session 7 — CSS Layout Mastery + Building the Periodic Table
We'll go from zero to building a complete, responsive Periodic Table of Elements — entirely with CSS Grid.
Main axis, cross axis, justify-content, align-items, flex-wrap and gap.
grid-template-columns/rows, fr unit, gap, grid-column/row span, and named areas.
The mental model for choosing the right tool every time.
Build a fully interactive, color-coded periodic table with precise grid placement.
auto-fit, minmax(), clamp(), and overflow-x for making complex grids mobile-friendly.
Flexbox is a one-dimensional layout model. It arranges items along a single axis — either a row or a column. Think of it as a smart line of items.
The container gets display: flex. The children become flex items and follow the container's rules.
/* Step 1: Turn on Flexbox */ .container { display: flex; /* Direction — which way items flow */ flex-direction: row; /* row | column | row-reverse | column-reverse */ /* Main axis alignment (horizontal if row) */ justify-content: flex-start; /* flex-start | center | flex-end | space-between | space-around | space-evenly */ /* Cross axis alignment (vertical if row) */ align-items: stretch; /* stretch | flex-start | center | flex-end | baseline */ /* Wrapping — should items wrap to next line? */ flex-wrap: nowrap; /* nowrap | wrap | wrap-reverse */ /* Space between items */ gap: 1rem; } /* On individual children */ .item { flex: 1; /* shorthand for flex-grow flex-shrink flex-basis */ align-self: center; /* override align-items for this item only */ order: 2; /* change visual order without changing HTML */ }
justify-content controls spacing along the main axis. align-items controls spacing along the cross axis. If you flip flex-direction, these axes swap too.
The flex shorthand on children is the most powerful property:
/* flex: grow shrink basis */ .item { flex: 1 1 0; } /* grow, can shrink, no base size (equal columns) */ .item { flex: 0 0 200px; } /* fixed 200px, does not grow or shrink */ .item { flex: 2; } /* grow twice as fast as flex:1 items */ .item { flex: none; } /* don't grow or shrink (keep natural size) */
Click the buttons to change Flexbox properties and see the result in real time. The live CSS updates below the demo.
CSS Grid is a two-dimensional layout system. It controls both rows and columns simultaneously. It's the right tool for complex, page-level layouts.
/* ── On the container ── */ .container { display: grid; /* Define columns: 3 equal columns using the fr (fraction) unit */ grid-template-columns: 1fr 1fr 1fr; /* or: repeat(3, 1fr) */ /* Define rows: first row 80px, rest auto */ grid-template-rows: 80px auto auto; /* Space between cells */ gap: 1rem; /* row-gap + column-gap shorthand */ row-gap: 1rem; column-gap: 0.5rem; } /* ── On individual children ── */ .item-a { grid-column: 1 / 3; /* from line 1 to line 3 (spans 2 cols) */ grid-column: span 2; /* shorthand: just span 2 columns */ grid-row: 1 / 3; /* spans 2 rows */ } /* ── Named template areas (cleaner for complex layouts) ── */ .layout { display: grid; grid-template-areas: "header header header" "sidebar content content" "footer footer footer"; grid-template-columns: 200px 1fr 1fr; } .header { grid-area: header; } .sidebar { grid-area: sidebar; } .content { grid-area: content; } .footer { grid-area: footer; }
fr stands for fraction of the available space. repeat(3, 1fr) creates 3 equal columns that share all available space. 2fr 1fr means the first column gets twice as much space as the second.
The most powerful responsive grid trick is repeat(auto-fit, minmax()):
/* Creates as many columns as fit, each at least 200px wide */ .card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; } /* → 1400px screen → ~6 columns → 800px screen → ~3 columns → 400px screen → 1 column (stacks) No media queries needed! */
Change the column layout and see how items reflow. Notice how span still works correctly.
Both are powerful. The key is knowing which problem each one solves best.
flex-wrap| Property | Flexbox | Grid |
|---|---|---|
| Dimensions | 1D (row OR column) | 2D (rows AND columns) |
| Defined by | Content (items decide size) | Container (template defines tracks) |
| Item placement | Sequential flow | Explicit grid line / area |
| Best for | UI components, navbars | Page layouts, complex grids |
| Overlap | Hard (needs tricks) | Easy with grid-area |
| Browser support | Excellent (all browsers) | Excellent (all modern browsers) |
Use Flexbox for the items inside a component. Use Grid for the overall page layout or any grid of cards. You can — and should — nest them.
The Periodic Table is a perfect Grid use case: 118 elements across 18 columns and 7 periods, with precise placement and intentional gaps. Let's break down every decision.
The outermost .periodic-table div becomes the grid. We declare 18 columns (one per group) and 10 rows — not 7. Here is why 10:
.periodic-table { display: grid; /* 18 columns — one per group. minmax prevents cells below 52px */ grid-template-columns: repeat(18, minmax(52px, 1fr)); /* 10 rows: 7 periods + 1 empty gap + 2 for lanthanide/actinide rows */ grid-template-rows: repeat(10, auto); gap: 3px; }
1fr alone would let cells shrink to zero on small screens. minmax(52px, 1fr) says: "be at least 52px, but grow equally if space is available." Combined with overflow-x: auto on the wrapper, this keeps every element readable at any screen size.
By default, Grid places children sequentially — filling left to right, one after another. If we used default placement, Hydrogen (H) would land in cell 1 and Helium (He) in cell 2, right next to H. That's wrong — He belongs in column 18.
The empty cells between H and He are not spacer divs — Grid holds them open automatically because no element has been assigned there. This is the key power of explicit placement.
Every element is stored as an array with 8 values. The last two fields — period and group — directly become grid-row and grid-column.
// [num, symbol, name, mass, category, period, group, series ] [1, 'H', 'Hydrogen', '1.008', 'hydrogen', 1, 1, null ], [2, 'He', 'Helium', '4.003', 'noble', 1, 18, null ], [3, 'Li', 'Lithium', '6.941', 'alkali', 2, 1, null ], [57, 'La', 'Lanthanum', '138.9', 'lanthanide',6, 3, 'lanthanide'], [89, 'Ac', 'Actinium', '(227)', 'actinide', 7, 3, 'actinide' ], /* period → grid-row (1–7 for main table, 9–10 for lanthanides/actinides) group → grid-column (1–18) series → tells us to override row to 9 or 10 instead of 6 or 7 */
Lanthanum (La) technically belongs in period 6, group 3. But if we placed it there, it would push Hafnium (Hf) to the right and break the table layout. The series field overrides its row to 9, moving it to the detached lanthanide row below — which is the traditional convention.
For each element we create a <div>, assign its category class for color, then set gridColumn and gridRow directly as inline styles.
ELEMENTS.forEach(([num, symbol, name, mass, cat, period, group, series]) => { const div = document.createElement('div') // Category class drives the color — no if/else needed div.className = `element cat-${cat}` // e.g. "element cat-alkali" // Lanthanides and actinides override their row if (series === 'lanthanide') { div.style.gridColumn = 3 + (num - 57) // La(57)→col3, Ce(58)→col4 … Lu(71)→col17 div.style.gridRow = 9 } else if (series === 'actinide') { div.style.gridColumn = 3 + (num - 89) // Ac(89)→col3, Th(90)→col4 … Lr(103)→col17 div.style.gridRow = 10 } else { div.style.gridColumn = group // directly from data (1–18) div.style.gridRow = period // directly from data (1–7) } table.appendChild(div) })
We want La (element 57) at column 3, Ce (58) at column 4, and so on up to Lu (71) at column 17. The formula 3 + (num - 57) does this:
The category string in the data (e.g. 'alkali') becomes a CSS class name (cat-alkali). The color is purely in CSS — the JS never checks which color to apply.
/* JS sets: div.className = `element cat-${cat}` */ /* CSS does the rest — no color logic in JavaScript */ .cat-hydrogen { background: #61dafbcc; border-color: #61dafb; color: #0a0a0f; } .cat-alkali { background: #ef4444cc; border-color: #ef4444; color: #fff; } .cat-alkaline { background: #f97316cc; border-color: #f97316; color: #fff; } .cat-transition{ background: #06b6d4cc; border-color: #06b6d4; color: #fff; } .cat-metalloid { background: #14b8a6cc; border-color: #14b8a6; color: #fff; } .cat-nonmetal { background: #eab308cc; border-color: #eab308; color: #1a1a1a; } .cat-halogen { background: #22c55ecc; border-color: #22c55e; color: #1a1a1a; } .cat-noble { background: #c084fccc; border-color: #c084fc; color: #fff; } .cat-lanthanide{ background: #f59e0bcc; border-color: #f59e0b; color: #fff; } .cat-actinide { background: #fb923ccc; border-color: #fb923c; color: #fff; } .cat-unknown { background: #64748bcc; border-color: #64748b; color: #fff; }
#ef4444cc — the last two hex digits (cc = 80% opacity in hex) make the background slightly transparent. This lets the dark background of the page subtly show through each element, giving the whole table a cohesive look instead of fully opaque flat blocks.
Without this, cells would be tall rectangles as the table scales to fit the screen. aspect-ratio: 1 forces height to always equal width — every cell stays a perfect square.
.element { display: flex; flex-direction: column; align-items: center; justify-content: center; /* Always square, regardless of column width */ aspect-ratio: 1; /* Font scales with viewport — no media queries */ font-size: clamp(7px, 0.9vw, 10px); } /* Notice: each element is ALSO a flex container! Grid positions the cell, Flexbox stacks the text inside it. This is the "nest flex inside grid" pattern. */
The .periodic-table is a Grid container — it places each element cell into the right slot. Each .element is a Flex container — it stacks the atomic number, symbol, name, and mass vertically inside that slot. This is the standard pattern: Grid for layout, Flexbox for content alignment inside each cell.
The periodic table cannot reflow into fewer columns — the columns represent chemical groups and their meaning is tied to position. The correct mobile strategy is horizontal scroll.
/* Wrapper: scroll horizontally on small screens */ .pt-wrapper { overflow-x: auto; -webkit-overflow-scrolling: touch; /* smooth momentum scroll on iOS */ } /* Table: never shrink below 980px so cells stay readable */ .periodic-table { min-width: 980px; } /* Compare: card grids CAN reflow — use auto-fit instead */ .card-grid { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); /* ↑ use this for anything where column count doesn't carry meaning */ }
Rows 1–7 are the 7 periods. Row 8 is empty on purpose (visual gap). Rows 9–10 hold lanthanides and actinides.
grid-column = group (1–18), grid-row = period (1–7). Unoccupied cells stay empty automatically — no spacers needed.
column = 3 + (atomicNumber − firstInSeries) maps each element to the correct column in rows 9–10.
Category string from data → CSS class → color from stylesheet. Separation of concerns.
Grid places each cell. Flexbox stacks atomic number, symbol, name, and mass inside each cell. Two layout systems, two jobs.
When layout carries meaning, let users scroll rather than reflowing into fewer columns. Use auto-fit/minmax only when column count is flexible.
There are three main strategies for responsive grids. Each suits a different type of content.
The grid figures out how many columns fit. Perfect for card grids and galleries.
When structure is semantically important (like the periodic table), let users scroll horizontally rather than breaking the layout.
Change grid-template-columns inside a @media block to switch from multi-column to single-column on small screens.
/* ── Strategy 1: auto-fit (no media queries) ── */ .card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; } /* ── Strategy 2: overflow scroll for complex layouts ── */ .table-wrapper { overflow-x: auto; /* horizontal scroll on small screens */ -webkit-overflow-scrolling: touch; /* smooth on iOS */ } .table-wrapper .periodic-table { min-width: 980px; /* prevent collapsing below readable size */ } /* ── Strategy 3: media query breakpoints ── */ .layout { display: grid; grid-template-columns: 250px 1fr; /* sidebar + content on desktop */ grid-template-areas: "header header" "sidebar content" "footer footer"; } /* Stack to single column on mobile */ @media (max-width: 768px) { .layout { grid-template-columns: 1fr; grid-template-areas: "header" "content" "sidebar" "footer"; } } /* ── Fluid font sizing with clamp() ── */ .element .el-symbol { /* min 8px, fluid, max 12px — no media queries */ font-size: clamp(8px, 1.2vw, 12px); }
The periodic table cannot reflow into fewer columns without losing its meaning — the columns represent groups with chemical properties. So the correct approach is overflow-x scroll with a fixed min-width, plus a visual hint on mobile that the table is scrollable.
Let's review the key concepts, then test your understanding.
Items flow in one direction. Use for navbars, button groups, centering.
Define rows and columns. Use for page layouts, card grids, complex UIs.
The most powerful responsive pattern — no media queries needed for card grids.
grid-column and grid-row position elements exactly — the key for the periodic table.
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)) do?Now that you know CSS Grid and Flexbox, you can build any layout on the web. The same Grid techniques power React component libraries like CSS Modules, Tailwind, and Styled Components.