building a 3-column grid

the classic profile / content / sidebar layout.

beginner css layout
no prior grid experience needed! if you can write a <div>, you can follow along. we'll build this up piece by piece and explain exactly what each line is doing.

what we're building

a 3-column layout where the left and right columns have a fixed width, and the middle column stretches to fill whatever space is left. this is the classic "profile / content / sidebar" pattern you see on old-school personal sites — a little profile card on one side, a sidebar with a calendar or links on the other, and your main content filling the middle.

the tool that makes this easy is css grid. before grid existed, people used floats or tables to fake this layout, which was fragile and annoying to maintain. grid lets us describe the whole structure in a few lines.

live preview: here's the finished result running live! try resizing your browser window narrower than 860px to see it switch to the mobile layout.

step 1 — set up the container

wrap your three columns in one parent element. this parent is what we'll turn into the grid — think of it as the "frame" that holds everything:

<div class="grid">
  <div class="col-left">left column</div>
  <div class="col-center">center column</div>
  <div class="col-right">right column</div>
</div>

right now this is just plain html — no styling at all. if you opened this in a browser, the three divs would just stack on top of each other like normal blocks, one below the other. that's expected! the layout only happens once we add css in the next step.

step 2 — define the grid columns

the key property is grid-template-columns. we give the left and right columns fixed pixel widths, and let the center column take up whatever space remains with 1fr:

.grid {
  display: grid;
  grid-template-columns: 220px 1fr 230px;
  gap: 18px;
  align-items: stretch;
}
  • display: grid — this is what actually turns the div into a grid container. without this line, none of the other grid properties do anything.
  • 220px — fixed width for the left column, no matter how wide the browser window is
  • 1fr — the center column takes all remaining space. fr stands for "fraction" — it means "give this column a share of whatever's left over"
  • 230px — fixed width for the right column
  • gap: 18px — spacing between all three columns, so you don't need margins on every child
  • align-items: stretch — makes every column the same height, matching whichever column is tallest

example: if the browser window is 1000px wide, here's how the math works out:

total width:      1000px
left column:      -220px
right column:     -230px
2 gaps (18px ea):  -36px
─────────────────────────
center column:     514px

the browser does this calculation automatically, every time the window resizes. you never have to compute it by hand — that's the whole point of 1fr.

step 3 — make the last box in each column stretch

if you want the last widget in each column to grow and fill the remaining vertical height (handy for a scrollable to-do list, for example), turn each column into a flex container and let its last child grow:

.col-left, .col-center, .col-right {
  display: flex;
  flex-direction: column;
}

.col-left .box:last-child,
.col-center .box:last-child,
.col-right .box:last-child {
  flex-grow: 1;
}

here's what each part is doing:

  • display: flex — turns each column into its own mini flex layout, separate from the outer grid
  • flex-direction: column — stacks the widgets inside the column vertically, top to bottom
  • flex-grow: 1 — only applied to the last box in each column, so it's the one that expands to fill any leftover vertical space

picture it like this: if your left column has a profile card (150px tall) and a links widget below it, and the whole column is 600px tall, the links widget will stretch to fill the remaining 450px instead of leaving empty space at the bottom.

tip: pair this with overflow-y: auto and a fixed min-height on the box body so a long list scrolls internally instead of pushing the whole page down.
.box-body {
  overflow-y: auto;
  min-height: 200px;
}

step 4 — make it responsive

3 fixed columns don't work on mobile — the columns get too squished, and 220px + 230px alone barely leaves room for content on a 375px-wide phone screen. the cleanest fix is to redefine the grid entirely inside a media query using grid-template-areas, so you can freely reorder or pair up widgets on small screens:

@media (max-width: 860px) {
  .grid {
    grid-template-columns: 1fr 1fr;
    grid-template-areas:
      "profile   calendar"
      "content   content";
  }
  .col-left, .col-center, .col-right {
    display: contents; /* lets children ignore which column they came from */
  }
  #box-profile  { grid-area: profile; }
  #box-calendar { grid-area: calendar; }
  #box-content  { grid-area: content; }
}

grid-template-areas lets you literally draw the layout using text. each quoted string is one row of the grid, and each word inside it is a cell:

"profile   calendar"   ← row 1: profile and calendar side by side
"content   content"    ← row 2: content spans both columns

then each element gets placed by matching its grid-area name to one of those words — for example, the element with id="box-profile" gets grid-area: profile, so it lands wherever "profile" appears in the drawing above.

why display: contents? normally, an element's position on the grid depends on which html column it's physically nested inside. display: contents makes the wrapper "disappear" from the grid's perspective — as if you deleted the div but kept its children — so those children become free to be placed into any named area, even ones that used to belong to a completely different desktop column.

putting it all together

here's the full css in one place, combining every step above:

.grid {
  display: grid;
  grid-template-columns: 220px 1fr 230px;
  gap: 18px;
  align-items: stretch;
}

.col-left, .col-center, .col-right {
  display: flex;
  flex-direction: column;
}

.col-left .box:last-child,
.col-center .box:last-child,
.col-right .box:last-child {
  flex-grow: 1;
}

@media (max-width: 860px) {
  .grid {
    grid-template-columns: 1fr 1fr;
    grid-template-areas:
      "profile   calendar"
      "content   content";
  }
  .col-left, .col-center, .col-right {
    display: contents;
  }
  #box-profile  { grid-area: profile; }
  #box-calendar { grid-area: calendar; }
  #box-content  { grid-area: content; }
}

result

you now have a layout that's a clean 3-column grid on desktop, and reflows into a simpler 2-column arrangement on mobile — without duplicating any html. the same elements just get reassigned to different spots depending on screen size, all from css alone.