Responsive design and media queries
A responsive page rearranges itself to suit the screen, from a small phone to a wide monitor, with one set of HTML.
Step 1: the viewport tag
<meta name="viewport" content="width=device-width, initial-scale=1">Without it, phones pretend to be about 980 pixels wide and shrink the whole page to fit, and nothing else in this lesson works.
Step 2: fluid by default
Most responsiveness comes from not fixing sizes: use percentages, fr, max-width rather than width, and wrapping:
<style>
* { box-sizing: border-box; }
body { margin: 0; padding: 12px; font-family: system-ui; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; }
.card { padding: 12px; border-radius: 10px; background: #f4f2ff; }
.card img { display: block; max-width: 100%; height: auto; border-radius: 6px; }
</style>
<div class="grid">
<div class="card"><img src="https://picsum.photos/seed/r1/400/250" alt=""><p>Python</p></div>
<div class="card"><img src="https://picsum.photos/seed/r2/400/250" alt=""><p>SQL</p></div>
<div class="card"><img src="https://picsum.photos/seed/r3/400/250" alt=""><p>React</p></div>
<div class="card"><img src="https://picsum.photos/seed/r4/400/250" alt=""><p>CSS</p></div>
</div>The grid fits as many columns of at least 160px as there's room for, and max-width: 100% stops images from spilling out of their cards. There isn't a single media query.
Media queries
A media query applies CSS only while a condition is true, most often a minimum width:
/* phones first: one column */
.layout { display: grid; gap: 16px; }
/* from 700px wide: a sidebar and the content */
@media (min-width: 700px) {
.layout { grid-template-columns: 220px 1fr; }
}
/* from 1100px wide: a third column */
@media (min-width: 1100px) {
.layout { grid-template-columns: 220px 1fr 280px; }
}This is mobile first: the base CSS is for small screens, and each min-width query adds to it as the screen grows. It usually needs less CSS than starting with the desktop layout and undoing it.
This preview reports its own width and the rules that apply. Press Playground, open the preview in a new tab, and resize the window to watch it change:
<style>
body { margin: 0; padding: 12px; font-family: system-ui; }
.layout { display: grid; gap: 10px; }
.layout > div { padding: 14px; border-radius: 8px; background: #eef0ff; }
.label::after { content: "Small screen: one column"; }
@media (min-width: 500px) {
.layout { grid-template-columns: 140px 1fr; }
.label::after { content: "500px and up: two columns"; }
}
@media (min-width: 900px) {
.layout { grid-template-columns: 140px 1fr 160px; }
.label::after { content: "900px and up: three columns"; }
}
</style>
<p><strong class="label"></strong> (<span id="w"></span>px wide)</p>
<div class="layout"><div>Menu</div><div>Content</div><div>Extras</div></div>
<script>
// runs once the page has a size, and again whenever it changes
const show = () => (document.querySelector("#w").textContent = innerWidth);
new ResizeObserver(show).observe(document.documentElement);
</script>Typical breakpoints sit around 600px (large phones), 900px (tablets) and 1200px (desktops), but the best ones are wherever your layout starts to look wrong.
Container queries
A media query asks about the screen. A container query asks about the element's own container, so a component adapts wherever it's placed: in a sidebar, a modal or a wide column. Drag the bottom-right corner of the dashed box:
<style>
body { font-family: system-ui; }
.holder { container-type: inline-size; resize: horizontal; overflow: hidden;
width: 520px; min-width: 200px; max-width: 100%; padding: 10px;
border: 2px dashed #b7adff; border-radius: 10px; }
.card { display: grid; gap: 10px; padding: 10px; border-radius: 10px; background: #f4f2ff; }
.card img { display: block; width: 100%; border-radius: 8px; }
.card p { margin: 4px 0 0; }
@container (min-width: 380px) {
.card { grid-template-columns: 140px 1fr; align-items: center; }
}
</style>
<div class="holder">
<div class="card">
<img src="https://picsum.photos/seed/cq/400/260" alt="">
<div><strong>Container queries</strong><p>Narrow: stacked. Wide: side by side.</p></div>
</div>
</div>container-type: inline-size turns an element into a container, and @container (min-width: ...) rules respond to its width rather than the window's.
Asking about preferences
Media queries can also read the visitor's settings:
<style>
body { padding: 14px; font-family: system-ui; background: white; color: #111; }
.spin { width: 40px; height: 40px; border: 4px solid #ddd; border-top-color: #5a48f5; border-radius: 50%;
animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-color-scheme: dark) {
body { background: #0f1117; color: #e9ecf5; }
}
@media (prefers-reduced-motion: reduce) {
.spin { animation-duration: 4s; }
}
</style>
<p>This preview follows your system's light or dark setting.</p>
<div class="spin"></div>
<p>If your system asks for reduced motion, the spinner slows right down.</p>prefers-color-scheme: darklets a site follow the system's dark mode, as CodeMIRAI does.prefers-reduced-motion: reduceis chosen by people whom motion makes dizzy or distracted. Respect it by calming animations down.