Flex and Grid

Tailwind CSS Fundamentals

Chapter 4 · Layout: Display, Flexbox and Grid

Layout is where Tailwind earns its keep. Flexbox and grid are CSS features, and the classes are thin names for their properties, so this chapter is as much about knowing what flexbox and grid do as about knowing the class names. For each family I give the class, the CSS it generates, and numbers measured in a real browser, because the clearest way to understand justify-between or flex-auto is to see where the boxes land. Positioning and overflow are chapter 5.

Every example here was run
CSS is copied from Tailwind CSS 4.3.3's output. Every position and width in the tables was measured in headless Chrome, using fixed-size boxes so the numbers can be checked by hand. Fractions such as 189.33 are real: the browser divides space in thirds without rounding.

Display

display decides how an element lays out its children and how it sits among its neighbours. The classes use the CSS names:

ClassCSSWhat it does
block, inline, inline-blockdisplay: block | inline | inline-blockthe normal flow values
flex, inline-flexdisplay: flex | inline-flexchildren become flex items (full-width box or inline box)
grid, inline-griddisplay: grid | inline-gridchildren become grid items
hiddendisplay: noneremoved from the layout entirely
contentsdisplay: contentsthe element's own box disappears; its children behave as if they were its parent's children
flow-rootdisplay: flow-roota box that contains its floats and stops margins collapsing through it
invisiblevisibility: hiddennot painted, but still takes up its space

The last two rows are easy to confuse, so I measured them. Three 40px-high blocks, the middle one hidden: with hidden the third block started at 40px (the middle one took no space); with invisible it started at 80px (the middle one was a 40px gap). Use hidden to take something out of the page and invisible to keep the layout stable while hiding it.

Flexbox: One Row or Column

Put flex on a container and its children are laid out along one line, the main axis; the other direction is the cross axis. Everything below is set on the container, except where noted.

  • Direction. flex-row (the default, flex-direction: row), flex-col (column), flex-row-reverse, flex-col-reverse. The main axis follows the direction.
  • Wrapping. flex-wrap lets items move onto a second line; flex-nowrap (the default) squeezes them into one; flex-wrap-reverse wraps upwards. Combine with gap-4 (chapter 2).

Along the main axis, justify-* spreads the free space. I measured three 100px-wide items in a 600px container (so 300px free), and the left edge of each item:

ClassCSSLeft edges of items 1, 2, 3
justify-startjustify-content: flex-start0, 100, 200
justify-centercenter150, 250, 350
justify-endflex-end300, 400, 500
justify-betweenspace-between0, 250, 500
justify-aroundspace-around50, 250, 450
justify-evenlyspace-evenly75, 250, 425

justify-between puts the first item at the start and the last at the end with equal gaps between (150px each here). justify-around gives each item the same space on both sides, so the outer gaps are half the inner ones (50 and 100). justify-evenly makes all four gaps equal (75).

Across the cross axis, items-* aligns the items in the container; self-* overrides it for one item. I measured three items of different heights (20px, 40px and 60px) in a 96px-high row; each cell is the item's top offset:

ClassCSSTop offsets (item 1, 2, 3)
items-startalign-items: flex-start0, 0, 0
items-centercenter38, 28, 18
items-endflex-end76, 56, 36
items-stretchstretch (the default)0, 0, 0, and items that have no height of their own grow to 96px
items-baselinebaseline8, 8, 0 (the items' text baselines line up; the third item has bigger text)

Stretch is why flex children in a row end up the same height without you asking, as in a row of cards. An item with its own height (the 20px one) keeps it; the others stretched to 96px. There is also content-* (align-content) for the spacing of wrapped lines.

How Items Share Space: flex-1, flex-auto, grow, shrink

These classes go on the items. Three items in a 600px row with widths w-20, w-32 and w-48 (80, 128 and 192px, 400px in total, 200px free):

Class on each itemCSSMeasured widthsWhat happened
flex-1flex: 1200, 200, 200starts from 0 and shares everything equally, ignoring their widths
flex-autoflex: auto146.66, 194.67, 258.67starts from each item's own size, then shares the 200px of free space equally (+66.67 each)
growflex-grow: 1146.66, 194.67, 258.67same as flex-auto here
flex-noneflex: none80, 128, 192fixed: no growing and no shrinking
flex-initialflex: 0 auto80, 128, 192can shrink but never grows

So flex-1 is for "equal columns" and for "take whatever is left" (a search box between a logo and a button); flex-auto or grow is for "let content decide first, then fill". shrink-0 stops an item being squeezed, which you saw on the price in chapter 2, and grow-0 and shrink are the opposite switches. basis-1/2 and basis-64 set the starting size (flex-basis) as a fraction or a spacing step. And remember min-w-0 from chapter 2 for flex items that contain text which should truncate.

order-* changes the visual order without moving the HTML: order-1 is order: 1, order-first is order: -9999, order-last is order: 9999, and order-none is 0. Keep the HTML in a sensible reading order first: screen readers and the Tab key follow the HTML, not the visual order.

Grid: Rows and Columns Together

Use grid when you care about both directions at once. grid makes the container, and grid-cols-3 defines three equal columns:

<div class="grid grid-cols-3 gap-4 w-[600px]"> <div>1</div> <div class="col-span-2">2</div> <div>3</div> </div>
.grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } .col-span-2 { grid-column: span 2 / span 2; } measured widths of items 1, 2, 3: 189.33, 394.67, 189.33

The 600px is split as (600 − 2 × 16) ÷ 3 = 189.33px per column, with 16px gaps (gap-4). Item 2 spans two columns: 2 × 189.33 + 16 = 394.67px, and item 3 wraps to the next row. The related classes:

  • grid-cols-1 … grid-cols-12 (repeat(N, minmax(0, 1fr))), grid-cols-none, and grid-cols-subgrid (grid-template-columns: subgrid, so a nested grid lines up with its parent's columns; I compiled it but did not test it in a browser).
  • grid-rows-3 (the same for rows), auto-rows-fr (every implicit row the same height), auto-cols-min.
  • col-span-2 (span 2 / span 2), col-span-full (1 / -1, from the first line to the last), col-start-2, col-end-4, and the same with row-.
  • grid-flow-col (fill columns first), grid-flow-row-dense (row dense: back-fill gaps left by spans).

Your own column layout. When the columns are not equal, write the template in square brackets, using an underscore where CSS has a space: grid-cols-[200px_1fr] compiles to grid-template-columns: 200px 1fr. In a 600px container with gap-4 I measured the two columns at 200px and 384px: a fixed sidebar and a main area that takes the rest. And the most useful arbitrary template needs no breakpoints at all:

<div class="grid grid-cols-[repeat(auto-fill,minmax(12rem,1fr))] gap-4">…</div>
in a 900px container: 4 columns, each 213px (900 - 3 x 16) / 4 = 213

That reads "as many columns as fit, each at least 12rem (192px), sharing the space". At 900px, four columns fit (a fifth would need 5 × 192 + 4 × 16 = 1024px) and they stretch to 213px; narrower screens get fewer columns, wider ones more. It is the simplest responsive card grid.

1fr can blow up: why grid-cols-2 uses minmax(0, 1fr)
The plain CSS value 1fr means "a share of the free space, but never smaller than the content". With a long unbreakable string in one cell, that "never smaller" wins. I put a 120-character word in the first cell of a 600px grid: with grid-cols-[1fr_1fr] the columns measured 1238.44px and 9.41px, so the page overflowed and the second column was crushed; with grid-cols-2 they were 300px and 300px. Tailwind's numbered columns are minmax(0, 1fr) precisely to stop this. If you write your own template, use minmax(0,1fr) rather than bare 1fr for columns that may hold long content, and pair it with break-words (chapter 2).

Placing Items and Centring

The place-* classes set two alignments at once: place-items-center is place-items: center (align and justify the items), place-content-center is place-content: center (the whole grid within its container), and place-self-center centres one item. justify-items-center is the horizontal half alone. They are mostly used for the question everyone asks: how do I centre something? I measured a 100 × 50px box in a 600 × 300px container with three recipes (offsets from the container's top-left corner):

RecipeClasses on the container (or the box)Box offset (left, top)
Flexboxflex items-center justify-center250, 125
Gridgrid place-items-center250, 125
Auto margins (horizontal only)mx-auto on the box, which needs a width250, 0

Both centre exactly: (600 − 100) ÷ 2 = 250 and (300 − 50) ÷ 2 = 125. Use grid place-items-center when there is a single child (it is two classes); use flex when there are several items to centre as a group, or when you need flex-col. A third way, absolute positioning with inset-0 m-auto, is in chapter 5.

Container and Aspect Ratio

The container class sets a width of 100% and then a max-width at each breakpoint:

container
.container { width: 100%; @media (width >= 40rem) { max-width: 40rem; } @media (width >= 48rem) { max-width: 48rem; } @media (width >= 64rem) { max-width: 64rem; } @media (width >= 80rem) { max-width: 80rem; } @media (width >= 96rem) { max-width: 96rem; } }

Two things to see in that output. It does not centre itself or add side padding (the generated rule has no margin or padding), so a page wrapper is normally container mx-auto px-4. And the steps are the breakpoints from chapter 7. Many projects skip container and use a plain mx-auto max-w-5xl px-4 (one fixed maximum width), which is simpler to reason about.

Aspect ratio keeps a box in proportion as its width changes. aspect-video uses the theme variable --aspect-video, which is 16 / 9 (Chrome reported 16 / 9); aspect-square is 1 / 1, and your own ratio goes in brackets (aspect-[4/3]) or in the shorter form aspect-3/2; aspect-auto removes it. A 256px-wide box measured 256 × 144 with aspect-video, 256 × 256 with aspect-square and 256 × 192 with aspect-[4/3]. The height follows the width, so you set one of them.

Two Recipes Worth Memorising

A navigation bar: the logo at the left, the links next to it, and a button pushed to the far right. The trick is ml-auto, a margin on the left that absorbs all the free space:

<nav class="flex items-center gap-6 px-6 py-3 border-b border-gray-200"> <a class="text-lg font-bold">Acme</a> <ul class="flex gap-4"><li>Home</li><li>Docs</li><li>Pricing</li></ul> <a class="ml-auto rounded-md bg-blue-600 px-3 py-1.5 text-white">Sign in</a> </nav>
in a 900px bar: logo starts 24px from the left (px-6) links start 24px after the logo (gap-6) the button ends 24px from the right edge (px-6) logo, links and button all centred on the same line, 30px from the top (items-center)

A sticky footer: on a short page the footer should sit at the bottom of the screen, not float in the middle. Make the body a column that is at least one screen tall, and let the main area grow:

<body class="min-h-dvh flex flex-col"> <header class="h-16">…</header> <main class="flex-1 p-4">Short page.</main> <footer class="h-12">Footer</footer> </body>
viewport height 405px with flex-1 on main: main 293px high, footer bottom = 405px (the bottom of the screen) without flex-1: main 56px high, footer bottom = 168px (just under the content)

min-h-dvh makes the body at least as tall as the screen, flex-col stacks the three parts, and flex-1 lets main take all the leftover height (293 = 405 − 64 − 48). Remove flex-1 and the leftover space has nowhere to go, so the footer rides up under the content.

Hands-On Exercises

Exercise 1

Build the navigation bar above in a 900px-wide page, then prove with measurements that the logo, link group and button share a centre line, that the button's right edge is exactly as far from the bar's edge as the logo is from its left edge, and what happens to the button's position if you delete ml-auto.

📄 View solution
Exercise 2

Build the sticky-footer page and show with measurements that the footer touches the bottom of the viewport. Then change the page so the footer stays at the bottom and the main area is vertically centred content: a heading and a paragraph in the middle of the leftover space.

📄 View solution
Exercise 3

Make a gallery in a 900px container: a three-column grid with 16px gaps, 16:9 tiles, five tiles, the first spanning two columns. Report the size and position of every tile. Then change it to a grid that chooses its own number of columns (each at least 12rem wide) and report how many columns you get at 900px.

📄 View solution

Chapter 4 Quick Reference

  • Display: block inline inline-block flex grid contents flow-root; hidden (removed from layout) vs invisible (keeps its space)
  • Flex container: flex, flex-col, flex-wrap, gap-*; main axis justify-start|center|end|between|around|evenly; cross axis items-start|center|end|stretch|baseline; one item self-*
  • justify-between with 3×100px in 600px: 0, 250, 500; justify-evenly: 75, 250, 425
  • Flex items: flex-1 (equal shares from 0), flex-auto/grow (own size, then share the rest), flex-none (fixed), shrink-0, basis-*, order-*, ml-auto pushes an item to the end
  • Grid: grid grid-cols-3 gap-4 (columns are minmax(0, 1fr)); col-span-2, col-span-full, col-start-*; custom grid-cols-[200px_1fr] (underscore = space); responsive without breakpoints grid-cols-[repeat(auto-fill,minmax(12rem,1fr))]
  • Plain 1fr can overflow on long content (measured 1238px + 9px); grid-cols-2 held 300px + 300px
  • Centring: flex items-center justify-center or grid place-items-center (both 250, 125 in 600×300); mx-auto centres horizontally only
  • container = width: 100% plus max-widths at 40, 48, 64, 80 and 96rem; add mx-auto px-4; aspect-video 16/9, aspect-square, aspect-[4/3]
  • Sticky footer: min-h-dvh flex flex-col on the page and flex-1 on main
Coming next
Tailwind CSS Fundamentals 5 covers positioning and stacking: relative, absolute, fixed and sticky, inset-*, z-*, overflow and scrolling, and images.