Skip to content

Layout with CSS: Flexbox, Grid and responsive design

Exercise layout-css-annex.pdf · PDF

Open NoteBox on a big monitor. A 390 px strip sits in the middle of a 1920 px desert. Remove its max-width and you get the opposite: input boxes a metre wide. Which one is right? Neither.

The phone design is right for a phone. But how many screen widths will your app meet? All of them. So you do not design a page. You design a rule for how the page behaves at any width. In Figma you did this with auto layout, constraints and breakpoints. Today you write the same ideas by hand, and they turn out to be surprisingly few lines.

What you will learn

  • Choose between Flexbox (one direction) and Grid (two directions) for a region of a screen
  • Lay out a bar with display: flex, justify-content, align-items and gap
  • Build a two-column page and a self-adapting card grid with display: grid, fr and repeat(auto-fill, minmax())
  • Explain mobile-first and write a min-width media query at a breakpoint
  • Test a layout at several widths with the browser's device toolbar and on a real phone
  • The trap: "responsive" does not mean three designs, one per device

Shelf and bookcase

Put six books in a row on a desk. That is a shelf: one direction. You decide their order, the space between them, and whether they sit at the left, at the right or spread out.

Now picture a bookcase: shelves and columns, and every book has its own cell.

Flexbox lays out items in one direction, a row or a column. Grid lays them out in rows and columns at once.

Tool Use it when In Figma it was
Flexbox Things go in one direction: bars, toolbars, a stack of fields Auto layout, horizontal or vertical
Grid You need rows and columns: page regions, galleries of cards Layout grids, and auto layout with wrap

Both start the same way. You put display: flex or display: grid on the parent, and its children line up. The parent is the shelf. The children are the books.

Careful: Flexbox is not obsolete

"Grid is the new one, so Flexbox is old." No. They solve different problems and live together on almost every page. Flexbox reached stable browsers between 2012 and 2015. Grid shipped in Firefox, Chrome and Safari in March 2017. You will use both, often one inside the other.

Before you start: tidy NoteBox

The page is about to change shape, so simplify it first.

  • In src/routes/+layout.svelte, delete the whole <style> block. Its main rule was the 390 px column.
  • In src/routes/+page.svelte, remove the <h1>, the hint, the Undo bar, select, remove, undo and onKey.
  • Keep notes, query, visible, addNote and the two effects.

Deleting feels scary, but this is what Git is for. Run git log, pick a commit and look at it with git show: every deleted line is still there.

Flexbox: the bar

The top bar has the brand on the left and a counter on the right, on one line, 56 px high. Add it at the top of the page's markup:

<header class="bar">
    <span class="brand">NoteBox</span>
    <span class="count">{notes.length} notes</span>
</header>

And in the page's <style>:

.bar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    height: 56px;
    padding: 0 16px;
    border-bottom: 1px solid var(--nb-line);
}

.brand { font-size: 20px; font-weight: 700; }
.count { font-size: 13px; color: var(--nb-muted); }

Four properties on the parent do almost everything:

Property What it decides
flex-direction The direction of the shelf: row (the default) or column
justify-content Where the books sit along the shelf: flex-start, center, flex-end, space-between
align-items Where they sit across the shelf: stretch (the default), center, flex-start, flex-end
gap The space between books, never before the first or after the last

You can play with these live. In DevTools, click the flex badge next to <header> and change the values. Predict each result before you click.

You have been using all four without a name: NoteRow and NoteForm are flex containers.

Careful: gap is not margin

A margin belongs to each item and also applies at the edges. gap exists only between items.

Grid: the page regions

Under the bar there are two regions: the form and the notes. Wrap them:

<div class="layout">
    <aside>
        <h2>New note</h2>
        <NoteForm onsave={addNote} />
    </aside>

    <section>
        <h2>Notes</h2>
        <input type="search" placeholder="Search notes" aria-label="Search notes"
            bind:value={query} />
        <!-- the list goes here -->
    </section>
</div>
.layout {
    display: grid;
    grid-template-columns: 1fr;
    gap: 24px;
    padding: 16px;
}

h2 { margin: 0 0 16px; font-size: 18px; }

fr is a Grid unit: one share of the free space left after fixed sizes and gaps.

1fr is not one pixel and not one per cent. With a single column of 1fr, that column takes everything. One column is exactly what a phone needs, so this is already the phone layout.

Grid outside, Flexbox inside: the cards

A row was fine for a narrow list. With more room, a note is better as a card. Replace the <ul> and its NoteRows:

<ul class="cards">
    {#each visible as note (note.id)}
        <li class="card">
            <h3>{note.title}</h3>
            <p>{note.body}</p>
            <span class="tag">{note.tag}</span>
        </li>
    {/each}
</ul>
.cards {
    display: grid;
    grid-template-columns: 1fr;
    gap: 16px;
    margin: 0;
    padding: 0;
    list-style: none;
}

.card {
    display: flex;
    flex-direction: column;
    gap: 8px;
    min-height: 120px;
    padding: 16px;
    border: 1px solid var(--nb-line);
    border-radius: var(--nb-radius);
}

.card h3, .card p { margin: 0; }
.card h3 { font-size: 16px; }
.card p { color: var(--nb-muted); }
.tag { margin-top: auto; font-size: 13px; }

The cards need text, so give each note in seed a body. Then, in DevTools → Application → Local Storage, delete the key notebox so the new seed is used.

Two details are worth stopping for:

  • The card is a shelf standing up. Why not Grid for the card? Because its content goes in one direction only.
  • margin-top: auto on the tag takes all the free space above it. The tag sticks to the bottom of the card, however short the text is. Try it with one long note and one short note.

Mobile-first and breakpoints

Press Ctrl+Shift+M with DevTools open. That is the device toolbar: it shows the page at an exact width you choose. Set it to Responsive and drag from 390 to 1280. Everything stretches. Now you have to say what should happen when there is room.

Mobile-first means the base CSS is for the narrowest screen, and you add only what changes as the screen gets wider.

A media query is CSS that applies only under a condition. A breakpoint is the width at which the layout changes.

NoteBox has two breakpoints: 768 and 1280.

flowchart LR
    B["<b>Base CSS</b><br>any width: one column,<br>padding 16"] -->|"at least 768 px"| M["<b>Add</b><br>two columns, padding 24,<br>as many card columns as fit"]
    M -->|"at least 1280 px"| L["<b>Add</b><br>stop at 1200 px, centred"]

At the end of the <style>:

@media (min-width: 768px) {
    .layout {
        grid-template-columns: 320px 1fr;
        padding: 24px;
    }

    .cards {
        grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    }
}

@media (min-width: 1280px) {
    .layout {
        max-width: 1200px;
        margin: 0 auto;
    }
}

Drag across 768: the form jumps to a 320 px column on the left, and the notes take the rest.

Now read the rule for the cards slowly, as a sentence: as many columns as fit, each at least 220 px, sharing the space equally. At 768 only one fits. Keep dragging: at about 850 a second column appears, and at about 1085 a third. You never said "two columns" or "three columns". You said what a card needs, and the grid counts.

The 1280 rule exists because lines of text a metre long are unreadable. Past that width, the content stops at 1200 px and centres itself.

Count the declarations inside the media queries: five. Everything else is shared by every screen.

Careful: min-width: 768px does not mean small screens

It means at least 768: that width and wider. You can also write desktop-first with max-width, and it works, but you end up undoing rules instead of adding them.

Careful: responsive does not mean three designs

There are no "three devices". There is a continuous width. Breakpoints go where your content breaks, not where a catalogue of phones says. NoteBox has two.

Three frames, one page

Look at the same page at 390, 768 and 1280 px, and list what is different.

The same at all three What changes
The bar Columns of .layout: 1 → 2
Colours, type sizes, radii Padding of .layout: 16 → 24
The inside of the form and of a card Card columns: 1 → as many as fit
Gaps: 24 between regions, 16 between cards Maximum width: none → 1200

A responsive page is one page with a short list of exceptions.

From Figma to CSS

You already know these ideas under other names:

In Figma In CSS
Auto layout, horizontal display: flex (row)
Auto layout, vertical display: flex; flex-direction: column
Gap between items gap
Padding of the frame padding
Alignment: space between justify-content: space-between
Child set to "Fill container" flex: 1 in a flex parent, 1fr in a grid
Child set to "Hug contents" No width: the default
Child with a fixed width width: 140px
Auto layout with wrap display: grid with repeat(auto-fill, minmax(...))
Breakpoints of a frame @media (min-width: ...)

Test on a real phone

Your screen is one width. "It works on my screen" proves very little. Stop the server and start it like this:

npm run dev -- --host

Vite prints a Network address such as http://192.168.x.x:5173. Open it on a phone on the same Wi-Fi and add a note with your thumb. Is Save easy to hit? It is 44 px high, the touch target you met in Apple's guidelines.

Some networks stop devices from reaching each other. If your phone cannot connect, try a phone hotspot, or rely on the device toolbar.

Why NoteBox ids use Date.now()

Over http://192.168... the browser refuses crypto.randomUUID(), because it needs HTTPS or localhost. That is why NoteBox's ids come from Date.now().

Your turn: the toolbar

The exercise in the handout adds a toolbar between the "Notes" heading and the cards: the search input, a select "All tags" and a button "Sort A-Z".

  • Under 480 px: the three controls stacked, each full width, gap 8.
  • From 480 px: one row. The input takes what is left (flex: 1), the select is 140 wide, the button 110.
  • All three are 44 high with the radius token, and 16 below the toolbar. They only have to look right; wiring them is optional.

If something fails, check these first:

What you see What to check
The toolbar does not line up Is display: flex on the toolbar, or on the input? Who is the shelf and who are the books?
On a phone the select is 140 px The widths are in the base CSS. Base means phone; widths go inside the media query
Many rules to cancel You wrote max-width queries. Start narrow and add
The input overflows the row It has width: 100% from before and flex: 1. Inside the query, use width: auto

Then try it in your own app: at least one flex bar, one grid and one breakpoint. To practise, play Flexbox Froggy and Grid Garden.

Key takeaways

  • Flexbox is a shelf: one direction. Grid is a bookcase: rows and columns. Real pages use both.
  • display goes on the parent; the children line up.
  • gap is the space between items. fr is one share of the free space.
  • repeat(auto-fill, minmax(220px, 1fr)) says what a card needs and lets the grid count the columns.
  • Mobile-first: the base CSS is the narrowest screen, and each min-width media query adds only what changes.
  • Breakpoints follow your content, not devices. Test with the device toolbar and a real phone.

Quick check

1. A shop page has a top bar with a logo on the left and a cart icon on the right. Which tool fits that bar best?
  • Grid, because it can place items in cells
  • Flexbox, because the items go in one direction
  • A media query, because the bar has two sides
  • Margins, because they push items apart

The bar is a single row, so it is a shelf: display: flex with justify-content: space-between. Grid is for rows and columns at once, and a media query only decides when CSS applies.

2. You want three buttons in a row. Where does display: flex go?
  • On each of the three buttons
  • On the first of the three buttons
  • On the element that contains the buttons
  • On the <body> of the whole page

display: flex goes on the parent, and its children line up. Putting it on a button only affects what is inside that button.

3. A grid has grid-template-columns: 320px 1fr and is 1000 px wide with no gap. How wide is the second column?
  • 1 px
  • 10 px
  • 320 px
  • 680 px

fr is one share of the free space left after fixed sizes and gaps. Here 1000 − 320 = 680, and 1fr is the only share, so it takes all of it.

4. A gallery uses repeat(auto-fill, minmax(200px, 1fr)) in a container 450 px wide, with no gap. How many columns do you get?
  • 1
  • 2
  • 3
  • 200

As many columns as fit, each at least 200 px: two fit in 450 px and a third would need 600. The two then share the space equally. You state what a card needs and the grid counts.

5. In mobile-first CSS, what does @media (min-width: 768px) apply to?
  • Only screens narrower than 768 px
  • Only screens exactly 768 px wide
  • Screens 768 px wide and wider
  • Only tablets, whatever their width is

min-width means "at least". In mobile-first the base CSS covers the narrowest screen, and the query adds what changes from that width upward. Queries know widths, not devices.

6. Where should a breakpoint go?
  • At the width of the most popular phone
  • At the width where your content stops working
  • At every width listed in a device catalogue
  • At the width of your own laptop screen

There are no three devices, only a continuous width. A breakpoint goes where your content breaks, which is why NoteBox needs only two.

7. A recipe site should show one recipe per row on a phone, and as many as fit on a laptop. Written mobile-first, where does repeat(auto-fill, minmax(260px, 1fr)) go?
  • In the base CSS, with a max-width query that cancels it
  • In a separate stylesheet, written for each model of laptop
  • On each recipe card, next to its padding and border
  • Inside a min-width query, with 1fr in the base CSS

Base means phone, so the base is one column. What only makes sense with more room goes inside a min-width media query, and it goes on the parent, because the parent is what lays out the cards.

Your page now fits any screen, but it is still one page with one address. Next, every screen of NoteBox gets its own URL: Routing and navigation.