Skip to content
wannacode

Responsive design

In short: Responsive design makes a page work on every screen: the viewport meta tag, mobile first and media queries that change the layout by the width.

Problem

The button is off the screen

The product page was made on a monitor, and it looks fine there. A customer on a phone writes: "I can't order, there is no button". Here it is at the width of a phone, 360px:

HTML, CSS, JS

The "Order" button is on the right, off the screen. The page slides sideways, and most people do not think of sliding it. The code has no error: the picture is 400px, the text column is 360px, and they sit side by side. On a monitor 1000px wide that fits. On 360px it does not.

The idea

Screens have different widths

The same page opens on a phone (360–430px), on a tablet (about 800px) and on a computer (1000px and more). A page that adapts to every width is called responsive.

The first step is in <head>:

<meta name="viewport" content="width=device-width, initial-scale=1">

Without it the phone pretends to be a monitor: it lays the page out at 980px and shrinks it until it fits. The text gets tiny, and your CSS never sees the real 360px. With it, the width of the page is the width of the screen. In the sandbox we add this line for you, but in your own pages you write it.

The second step is the order in which you write the CSS. Mobile first means: the base rules are for the narrowest screen. There everything is one below the other, and that almost always fits. Then you add changes for wider screens with a media query:

.product {
display: grid;
gap: 16px;
}
@media (min-width: 700px) {
.product {
grid-template-columns: 1fr 1fr;
}
}

@media (min-width: 700px) is a condition: "the width is at least 700px". The rules inside apply only when the condition is true. Otherwise the browser skips them.

The third step is pictures. width="400" is good for the HTML: the browser keeps space while the picture loads. On a narrow screen, though, it needs a ceiling:

img {
max-width: 100%;
height: auto;
}

max-width: 100% means "no wider than its box", and height: auto keeps the proportions so it is not squashed.

See it

See the queries switch on

The window grows from 360px to 1000px. Watch the ruler at the top and the lines in the code.

body { margin: 0; padding: 16px; }
.cards {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
}
@media (min-width: 600px) {
.cards { grid-template-columns: 1fr 1fr; }
}
@media (min-width: 900px) {
.cards { grid-template-columns: repeat(4, 1fr); }
}
Step by step, in words
  1. A phone, 360px. The base rule is for the narrowest screen: one column. A card is 360 − 2 × 16 = 328px wide.
  2. The window gets wider and the column grows with it. At 599px the condition min-width: 600px is still false, and the browser skips the rules inside.
  3. At 600px the condition is true. Its rule comes after the base one and wins: two columns of 276px. The cards flow into the new grid.
  4. Wider again: the two columns grow to 425.5px each. min-width: 900px is still false.
  5. At 900px both queries are true. They set the same property, so the later one wins: four columns of 205px.
  6. At 1000px nothing new switches on. The four columns grow to 230px.
  7. The browser checks the queries again on every change of width: a turned phone, a resized window. Narrow it and the columns go back the same way.

A media query is not worked out once. The browser checks it on every change of width and lays the page out again at once. The two queries set the same property with the same specificity, so at 900px the later one wins. That is the cascade you already know.

Try it

Fix it for a phone

The page is 360px wide. Make it mobile first: the picture and the text one below the other, and nothing sticking out sideways. The picture is no wider than its box. From 700px up the picture and the text are in two columns. The check looks for sideways scrolling and for how many rows the picture and the text column sit on.

HTML, CSS, JS

Try it

Now for a computer

This page is already made for a phone: everything is in one column. Here the width is 1000px. Add a media query so that from 700px up the picture and the text are in two equal columns.

HTML, CSS, JS

Try it

Find the bug

On a computer there should be two columns, but the page stays in one. Why?

Pick the line with the bug.

Try it

Fill in

Which word makes the two columns apply from 700px up?

@media (: 700px) {
  .product {
    grid-template-columns: 1fr 1fr;
  }
}

The idea

The colour in one place

The green of the shop is in the CSS in four places: the price, the links, the button and the border. The shop changes its colour and someone has to find them all. Miss one and the page is in two colours.

A CSS variable (custom property) is a value with a name. The name starts with two dashes, and var() reads it:

:root {
--accent: #1a7f37;
}
button {
background: var(--accent);
}

:root is the topmost element, <html>. Variables are inherited like color, so whatever is in :root is seen everywhere in the page. The colour changes on one line.

The same gives a dark theme. @media (prefers-color-scheme: dark) is true when a person has chosen a dark theme on their phone or computer. Inside it you change only the values of the variables. The rules that use them stay the same.

See it

Light and dark

HTML, CSS, JS

The sandbox follows the theme of your browser, not the theme button of this site. Switch your system theme to dark and press "Run": the background, the text and the green change, while the CSS rules for .card, .price and button are the same. The green on dark is lighter, so it reads on the dark background.

Try it

A new colour for the shop

The shop changes the green #1a7f37 to orange #c2410c. Make a variable --accent in :root with the value #c2410c and use it in the four places where the green is now.

HTML, CSS, JS

The idea

States: pointed at, focused, disabled

A button does not look one way only. It can be under the mouse, focused from the keyboard, or disabled. CSS catches these with pseudo-classes: a word with a colon after the selector.

button:hover {
background: #116329;
}
button:focus-visible {
outline: 3px solid var(--accent);
outline-offset: 2px;
}
button:disabled {
background: #9ca3af;
cursor: not-allowed;
}
  • :hover: the mouse is over the element. A phone has no mouse, so important information is not hidden behind :hover.
  • :focus-visible: the element has focus and the browser decides the focus should show. From the keyboard it always shows. On a mouse click on a button it usually does not. That is why outline: none on :focus is a bug: a keyboard user loses track of where they are.
  • :disabled: the element has the disabled attribute. The "Sold out" button should look disabled, not just be disabled.

And the size. On a phone you press with a finger, and it is less precise than a mouse. A button 24px tall gets missed. Give buttons a height of at least 44px: min-height: 44px.

Try it

The buttons on a phone

A colleague removed the focus outline, and the buttons are short. Make it so: the buttons are at least 44px tall (min-height); on keyboard focus there is a 3px solid outline in the colour --accent; the disabled button "Sold out" is grey (#9ca3af) with the cursor not-allowed. The check presses Tab and looks at the "Order" button.

HTML, CSS, JS
More precisely, if you are curious

Viewport, more precisely. Without the meta tag, mobile browsers lay the page out at 980px and shrink it. In Chromium with an emulated 360px phone, innerWidth is 980 and @media (min-width: 700px) is true: the phone gets the computer layout. initial-scale=1 means "no shrinking at the start". Do not turn zoom off (user-scalable=no, maximum-scale=1): people with weak eyesight enlarge the page with two fingers.

Breakpoints. 600, 700, 900 are not sacred numbers. Put a breakpoint where your layout starts to look bad, not at the model of some phone. Some write queries in em (min-width: 40em): then they also follow the font size a person has chosen.

Queries not only for width. (hover: hover) is true when the main pointing device can hover (a mouse), and (pointer: coarse) is true for a finger. (prefers-reduced-motion: reduce) says a person does not want motion: remove the animations. (orientation: landscape) is for a sideways screen.

Variables, more precisely. var(--accent, #1a7f37) has a fallback value in case the variable is missing. A variable can change for part of the page: .sale { --accent: #c2410c; } and everything in .sale is orange. The value is checked only when it is used: --accent: 12px is allowed, but then color: var(--accent) is invalid and the colour becomes the inherited one. The property color-scheme: light dark tells the browser to colour its own parts (fields, scrollbars) in the dark theme too.

Size for a finger. WCAG 2.2 (criterion 2.5.8) asks for a target of at least 24 × 24px or enough space around it. Apple recommends 44 × 44pt, and Material Design 48 × 48dp.

Exercise

The catalogue on a phone

The catalogue was made for a computer: four cards of 220px in one row. Here the width is 360px. Make it for a phone: the cards in two columns, no sideways scrolling, pictures no wider than their card, the "Buy" buttons at least 44px tall (min-height). Put the green #1a7f37 in a variable --accent in :root and make the buttons use it. If you like, add four columns from 700px up as well.

HTML, CSS, JS

Test

Check yourself

1. You write mobile first. Which rule has no media query?
2. @media (min-width: 600px) with a window of 599px and of 600px?
3. :root has --accent: #c2410c, and the button has background: var(accent). What colour is the button?
4. Why :focus-visible and not :hover for the outline of the button?
5. How big should a button be on a phone?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·