Responsive Design
Tailwind CSS Fundamentals
Chapter 7 · Responsive Design and Container Queries
A page that works at one width is a page that works on one device. Responsive design means the layout adapts: one column on a phone, two on a tablet, four on a
desktop. In Tailwind this is the same variant mechanism as chapter 6, with a screen size as the condition: md:grid-cols-2 is "two columns, from the
md width upwards". There is one rule to learn (it is mobile-first) and one newer tool to add (container queries, which respond to the size of a parent
rather than the screen).
Mobile-First: The One Rule
Tailwind's breakpoints are five named widths, defined in the theme and compiled to min-width media queries:
| Prefix | Theme value | Pixels | Generated media query |
|---|---|---|---|
| sm: | --breakpoint-sm: 40rem | 640px | @media (width >= 40rem) |
| md: | 48rem | 768px | @media (width >= 48rem) |
| lg: | 64rem | 1024px | @media (width >= 64rem) |
| xl: | 80rem | 1280px | @media (width >= 80rem) |
| 2xl: | 96rem | 1536px | @media (width >= 96rem) |
A class with no prefix applies at every width. A prefixed class applies at that width and everything wider. So you write the phone layout with plain classes first, and use the prefixes to change it as the screen grows. I put six background colours on one element and resized the viewport:
The boundaries are inclusive: at exactly 640px the sm: colour is already in force, at 639px it is not. And each colour holds until the next prefix takes over.
This is the idea that trips people up: sm: does not mean "on small screens". It means "from the small breakpoint (640px) up". If you want
something on phones only, the plain class is the phone style, and you cancel it above:
<head>:
<meta name="viewport" content="width=device-width, initial-scale=1">. Without it a mobile browser pretends the screen is about 980px wide and shrinks
the result, so none of your small-screen styles apply. I emulated a phone 390px wide and loaded the same page twice. Without the tag: innerWidth was
980, matchMedia('(min-width: 768px)') was true, and the md: colour applied, on a 390px phone. With the tag:
innerWidth was 390, the media query was false, and only the plain class applied. If your responsive layout "does nothing" on a real phone, check this first.
Max-Width, Ranges and Arbitrary Breakpoints
Mobile-first covers most needs, but you can also target "below" a width:
| Variant | Media query | Measured |
|---|---|---|
| max-md: | @media (width < 48rem) | applied at 767px, not at 768px (strictly below) |
| max-sm:, max-lg:, max-xl:, max-2xl: | width < 40rem, < 64rem, < 80rem, < 96rem | the same pattern |
| md:max-lg: | @media (width >= 48rem) { @media (width < 64rem) { … } } | applied from 768 to 1023 only: blue at 768 and 1023, not at 767 or 1024 |
| min-[600px]: | @media (width >= 600px) | not at 599px, applied at 600px |
| max-[600px]: | @media (width < 600px) | (compiled; the same strict rule) |
Use max-* and ranges sparingly: they are for the case where a style is needed only in a middle band, such as hiding something between tablet and
small-laptop sizes. A one-off width that is not on the scale goes in brackets. Adding your own named breakpoint to the theme (--breakpoint-xs) is chapter 9.
The same mechanism also has a few other conditions: landscape: and portrait: (orientation), print:
(@media print), and pointer-coarse: and pointer-fine: (@media (pointer: coarse | fine), handy for making touch targets
larger on touch screens). Dark mode, reduced motion and contrast are chapter 8.
Designing Upwards from a Phone
The workflow follows from mobile-first. Build the layout at about 360 to 390px wide with plain classes. Widen the window; the moment it looks wrong, add one breakpoint class that fixes that. Three patterns cover most pages, all measured:
A card grid that gains columns:
A navigation bar that shows its links from md and a menu button below it. The links are hidden md:flex (not displayed, then a flex
row from 768px) and the button is md:hidden (shown, then hidden from 768px). Measured: at 767px the links had display: none and the button
display: block; at 768px the links were flex and the button none. The open-and-close behaviour of the menu needs a few lines of
JavaScript or a <details> element; Tailwind handles only the showing and hiding.
Type and spacing that grow: p-4 text-2xl md:p-8 md:text-4xl measured 16px of padding and 24px text up to 767px, then 32px and 36px from 768px.
Images already behave, thanks to Preflight (chapter 1): img { max-width: 100%; height: auto }, so a large image shrinks to fit a small
screen. For a fixed shape use aspect-video w-full object-cover (chapters 4 and 5), and to show different images at different sizes combine
hidden md:block with md:hidden. Serving smaller files to smaller screens is HTML's job (srcset and <picture>),
not Tailwind's.
Container Queries: Responding to the Parent
Breakpoints ask "how wide is the screen?". That is the wrong question for a reusable component. The same product card might sit in a wide main column
and in a narrow sidebar on one page, at one screen width. A container query asks "how wide is the box I am in?". Mark the parent with
@container and use the @-prefixed variants on its descendants:
@container sets container-type: inline-size, which tells the browser to track the element's width so queries can use it. @md: then
applies when the nearest container is at least 28rem (448px) wide. Note that the container sizes are not the screen breakpoints: they come from the
container scale, which is smaller:
| Variant | Container width | Variant | Container width |
|---|---|---|---|
| @3xs: | 16rem (256px) | @xl: | 36rem (576px) |
| @2xs: | 18rem (288px) | @2xl: | 42rem (672px) |
| @xs: | 20rem (320px) | @3xl … @7xl: | 48, 56, 64, 72 and 80rem |
| @sm: | 24rem (384px) | @max-md: | width < 28rem |
| @md: | 28rem (448px) | @min-[400px]: | width >= 400px |
| @lg: | 32rem (512px) |
Here is the difference from breakpoints, measured. The viewport was 1200px wide in every case. A grid with grid-cols-1 @md:grid-cols-2 was placed inside a
container, and beside it a grid with grid-cols-1 md:grid-cols-2 (a screen breakpoint):
| Container width | Columns with @md: (container) | Columns with md: (screen) |
|---|---|---|
| 300px | 1 | 2 (the screen is wide, whatever the box) |
| 500px | 2 | 2 |
The container-query version adapted to its 300px box; the screen-breakpoint version squeezed two columns into it. The same component dropped anywhere on the page lays itself out correctly. Three more details, all measured:
- The query measures the container's content width, inside its padding. A container
w-[460px] p-4has 428px of content (460 − 32), which is below 448px, so@md:did not apply (1 column).w-[480px] p-4has exactly 448px of content, and@md:applied (2 columns). - Named containers let a nested layout pick which one it listens to:
@container/card(container-type: inline-size; container-name: card) and@md/card:(@container card (width >= 28rem)). A 500px named container gave 2 columns. - It is not the
containerclass from chapter 4. That one is a centred page wrapper with screen-width limits;@containerdeclares a query container. The names are similar and the two are unrelated.
By the rules of CSS containment (not something I measured), a container with container-type: inline-size cannot take its width from its own contents, so give
it a width from its parent or from layout, as the examples above do. As a rule of thumb: use screen breakpoints for the page's overall structure (header, columns, navigation) and
container queries for components that appear in more than one place.
Testing Responsive Layouts
In Chrome and Edge, press Ctrl+Shift+M in DevTools to open the device toolbar; Firefox has the same shortcut for its Responsive Design Mode. Drag the width handle slowly from 320px up to 1600px and watch each breakpoint fire (the toolbar can show the media-query bands), then try a phone preset to see the viewport meta tag working. Two checks are worth doing on every page: that nothing scrolls sideways at 320px (a common cause is a fixed width wider than the screen, or the 1fr issue from chapter 4), and that the touch targets are comfortable at phone sizes. An emulator is not a phone: test on a real device before you ship something important.
Hands-On Exercises
Build a page wrapper (mx-auto max-w-6xl p-4) holding a grid of eight 80px-high cards that has one column on phones, two from 640px and four from 1024px, with 16px gaps. Report the number of columns and the width of a card at viewport widths 390, 700, 1100 and 1500, and explain each card width from the classes.
Build one product card that stacks its image above its text in narrow spaces and puts them side by side in wide ones, using a container query. Place the identical card in a 700px main column and a 300px sidebar on the same 1200px-wide page. Show that the two copies lay out differently, and say what would have happened with md: instead of @md:.
Read-the-classes audit. For each element below, say what it resolves to at viewport widths 360, 640, 768 and 1024 before you measure, then check with a resized viewport: (a) text-sm md:text-base lg:text-lg (font size), (b) hidden sm:block md:hidden (display), (c) p-2 sm:p-4 max-sm:p-1 (padding), (d) text-red-500 md:max-lg:text-blue-500 (colour). Explain each surprise.
Chapter 7 Quick Reference
- Always
<meta name="viewport" content="width=device-width, initial-scale=1">; without it a 390px phone reportedinnerWidth980 and appliedmd: - Breakpoints:
sm640,md768,lg1024,xl1280,2xl1536 (width >= …, inclusive). Unprefixed = all sizes; prefixed = that size and up.sm:is not "on phones" - Write the phone layout first, add
sm: md: lg:only where it breaks:text-center sm:text-left,grid-cols-1 sm:grid-cols-2 lg:grid-cols-4,hidden md:flex+md:hidden max-md:(width < 48rem, strict), rangemd:max-lg:(768 to 1023), arbitrarymin-[600px]:/max-[600px]:- Other conditions:
landscape: portrait: print: pointer-coarse: pointer-fine: - Container query: parent
@container(container-type: inline-size), children@md:etc. (@3xs16rem …@md28rem …@7xl80rem),@max-md:,@min-[400px]: - The query measures the container's CONTENT width (inside its padding); name containers with
@container/cardand@md/card: - Screen breakpoints for page structure, container queries for reusable components; DevTools device toolbar is
Ctrl+Shift+M; check 320px for sideways scrolling
dark: and how to switch dark mode by a class or attribute, then
motion-safe: and motion-reduce:, contrast-more:, forced-colors: and print:.