Old-School Front-End / 2026 ・ No.05

Old-School Front-End

Part 5: CSS layout and phones, arranging with flex and grid and fitting any screen width

Published

The same claims list at three screen widths: PC, tablet and phone. On the PC the header is one row and the three summary boxes sit side by side. On the phone the summary boxes stack, while the heading and the New claim button stay side by side
This part's goal: one HTML file and one stylesheet that rearrange themselves for PC, tablet and phone. (The author's demo page, captured in Chromium in the cloud; the phone is Playwright's mobile emulation at 390×844. 17 October 2026. Device and browser frames were drawn in for illustration.)

An "old-school" front-end course: no AI doing it for you, every line typed by hand. Part 5 is about layout.
Last time we dressed up the claims list. Then we opened it on a phone… and it wasn't pretty.
This time we use flex (line things up), grid (put things in cells) and media queries (switch by screen width)
so that one HTML file and one stylesheet read well on both a PC and a phone.

1

This part's goal

AraiFurui… I showed the boss on my phone, and she asked where the Status column had gone.

FuruiI told you to read part 5 before opening it on a phone.

AraiI asked the AI to "make it mobile-friendly" and the CSS came back twice as long. Now I don't know where to start.

FuruiThere are really only three tools for arranging things. Learn those and mobile stops being scary.

You are here: part 5

1 Setup 2 HTML 3 Forms 4 CSS basics 5 Layout 6 Bootstrap 7 JS basics 8 DOM, events 9 Validation 10 Fetch 11 Sign-in 12 Approvals 13 Charts, print 14 jQuery 15 Finish You are here Setup HTML CSS JavaScript Real app Finish

First, where we stand. I opened the claims list on a phone, 390px wide.

Three phone screens side by side. 1 Without the meta viewport tag the whole page is shrunk and the text is unreadable. 2 With part 4's CSS, the table can't shrink enough: the page grows wider than the screen and the Status column is pushed off the right edge. 3 With part 5's CSS, the summary boxes stack and the table scrolls sideways
1 With the meta viewport tag removed. 2 Part 4's CSS as it was. 3 This part's CSS. (The author's demo page, captured in Chromium in the cloud; the phone is Playwright's mobile emulation at 390×844. 17 October 2026. Device and browser frames were drawn in for illustration.)

Number 2 is last part's CSS as it was. The table can't squeeze any narrower, so the page grows wider than the screen and the Status column ends up off the right edge. (In the Japanese and Chinese versions the table squeezes instead, and the status badges end up one character per line.) Number 1 also has the HTML's meta viewport tag removed. Number 3 is where we're headed. The HTML only gains two wrapper divs; CSS does the rest.

2

Know the phone screen: meta viewport

Remember this line we've had in the head since part 2?

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

Without it, a phone browser assumes "this page was made for PCs", lays it out as if the screen were 980px wide, then shrinks the result. Here is what I measured with the page open on a phone.

# Measured on a phone (390×844)
 no meta as in part 4 part 5
layout width 980px 468px 390px
zoom 0.398 1 1
table width 912px 443px 640px (box 358px, scrolls)
purpose column 334px 97px 205px
status badge height 27px 27px 27px

With and without meta viewport

No meta tag width=device-width 980px × 0.398 390px × 1 16px text shows at ~6.4px 16px text stays 16px

Without the meta tag the page is shrunk to 0.398 of its size, so 16px text comes out at about 6.4px. You can't read it without pinching to zoom. width=device-width means "lay out at the device's screen width" and initial-scale=1 means "start at full size". Mobile support starts with this one line.

Even with it, though, things break as in number 2. The meta tag only gets the page laid out at the phone's width. Making the content fit that width is CSS's job.

3

Three ways to arrange boxes

In part 4 we learned that every element is a box. Broadly, there are three ways to arrange those boxes.

Three ways to arrange boxes

Normal flow Stacks (the default) e.g. paragraphs flex One direction e.g. header row grid A grid of cells e.g. summary boxes block block a block main axis columns, rows first
  • Normal flow: what you get without asking. Block elements (p, h2, section and so on) stack vertically; inline elements (a, span and so on) flow inside text. Up to part 4, the claims list used almost nothing else
  • flex: put display: flex on the parent and its children line up in one direction. Great for a header's contents, or a heading next to a button
  • grid: put display: grid on the parent, decide the columns and rows first, then drop the children in. Great for cards or boxes in several columns

FuruiBefore flex and grid, we built columns with float, a feature meant for wrapping text around a picture.

AraiA text-wrapping trick, for the whole page?

FuruiYes. Columns never matched in height, and you needed a clean-up charm called clearfix. Today, just use flex and grid. Leave the old pain in the old days.

4

flex: lining things up

At PC width we want the header, stacked in three rows until now, to become a single row. That's a job for flex.

The header as flex

body > header { display: flex; } .site nav .user gap margin-left: auto main axis justify-content cross axis align-items: center display: flex on the parent lines the children up along the main axis margin-left: auto means "give me all the space on my left"
body > header {
  display: flex;          /* line the children up */
  flex-wrap: wrap;        /* wrap if they don't fit */
  align-items: center;    /* centre them vertically */
  gap: 8px 32px;          /* 8px between rows, 32px between items */
}
.user {
  margin: 0 0 0 auto;     /* take all the space on the left, go far right */
}

Flex has two directions. The direction the children line up in is the main axis (horizontal by default); the one at right angles is the cross axis (vertical). justify-content arranges things along the main axis and align-items lines them up on the cross axis. I measured where the children land for different justify-content values on a test page.

justify-content and where the children land (measured)

justify-content: left edges (px, measured) flex-start 0 88 176 flex-end 224 312 400 center 112 200 288 space-between 0 200 400 margin-left: auto 0 88 400 space-between pins both ends and shares the rest; auto pushes just one

The margin-left: auto on the header is a classic flex trick. Give a flex child an auto margin and that margin swallows all the leftover space, pushing just that child to the far right. "Logo and nav on the left, user name alone on the right" takes one line.

The heading-and-button row is flex too. We wrapped it in div class="page-head" and split it to both ends with justify-content: space-between. This row stays side by side even on phones: the h1 and the button are short enough to fit in one row (and flex-wrap folds them if they ever don't).

5

grid: putting things in cells

Last time the summary boxes used inline-block and width: 220px. With the width fixed, a PC leaves empty space on the right, and a phone stacks them one per line with a gap on the right as well. Let's swap that for grid.

.summary {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 12px;
}

Line two looks like a spell, but read it from the inside out and it's simple.

  • minmax(200px, 1fr): each column is "at least 200px, at most 1fr". fr is a grid-only unit meaning "a share of the leftover width"; all 1fr means equal shares
  • repeat(auto-fit, …): repeat that column "as many times as fit". The browser works out how many 200px columns the width allows

I changed the screen width and measured the actual columns.

auto-fit: number and width of columns (measured)

repeat(auto-fit, minmax(200px, 1fr)) 390px screen (358px content): 1 col. 358 600px screen (568px content): 2 col. 278 278 768px screen (720px content): 3 col. 232 232 232 1024px screen (912px content): 3 col. 296 296 296 0px Same 1024px with auto-fill: the 4th column stays empty 219 219 219 219 As many columns as fit (200px min), extra shared by 1fr auto-fit folds empty columns to 0; auto-fill keeps them

One column on a phone, two at 600px, three at 768px, without a single media query. At 1024px there's room for four, but there are only three boxes, so auto-fit folds the empty fourth column down to 0px and the three boxes stretch across the row. With auto-fill the empty column stays, leaving a column-sized gap on the right. Here we chose auto-fit, so the boxes fill the row.

POINT

With grid you state only the minimum width and leave the number of columns to the browser. That's far shorter than spelling out "1 column on phones, 2 on tablets, 3 on PCs", and it copes with widths you never thought of.

6

Let only the table scroll

A table's value lies in its rows and columns lining up. Squash it and it becomes unreadable; let it push the page wider and the whole page wobbles sideways, as in number 2. So we wrap the table in a div and let only that wrapper scroll sideways.

<div class="table-wrap" tabindex="0" role="region" aria-label="All claims">
  <table> … </table>
</div>
.table-wrap {
  overflow-x: auto;      /* if it overflows, scroll inside this box */
}
table {
  width: 100%;
  min-width: 640px;      /* don't squash below this */
}
.amount, td time, .status {
  white-space: nowrap;   /* never wrap amounts, dates or statuses (really one line in each rule) */
}
Two crops of the table on the phone. 1 Before scrolling, No., Date and Purpose are visible. 2 After swiping the table sideways, the Amount and Status columns appear. The page itself doesn't move
1 Before scrolling. 2 After swiping just the table. The page stays at 390px; only the table moves. (The author's demo page, captured in Chromium in the cloud; the phone is Playwright's mobile emulation at 390×844. 17 October 2026. Device and browser frames were drawn in for illustration.)
  • min-width: 640px: the table's minimum width. On narrower screens the table stops shrinking and overflows inside the wrapper, which then scrolls
  • white-space: nowrap: keeps dates, amounts and badges in one piece, so "2026-10-" and "16" never end up on separate lines
  • tabindex="0": lets the wrapper take keyboard focus. Once focused, the arrow keys scroll it sideways, no mouse needed
  • role="region" and aria-label: anything focusable needs a name. This tells screen readers it's a region called "All claims"
7

Media queries: switching by width

We only want the header in one row on wide screens such as PCs. On a phone, the logo, nav and user name would be crammed together. Switching CSS by screen conditions like this is what media queries do.

/* everything up to here applies to every screen (written for phones) */
body > header { background: var(--brand); padding: 12px 24px; }

@media (min-width: 768px) {
  /* added only when the screen is 768px or wider */
  body > header { display: flex; … }
}

Here we write for phones first and add rules for wider screens with min-width, an approach called mobile first. The narrowest screens get the fewest rules, which keeps things simple. I stepped the screen width 1px at a time and noted where the look changes.

# Screen width stepped 1px at a time from 300px to 1300px: where things change
 300px+   1 summary column, table scrolls, header stacked
 444px+   2 summary columns
 656px+   3 summary columns
 672px+   table fits its box (no sideways scroll)
 768px+   media query kicks in: header in one row (display: flex)

Screen width and where things change (measured)

320 444 656 768 1024 1280 screen width (px) Shared styles (written for phones first) @media (min-width: 768px) added from here 1 col. 2 col. 3 col. table scrolls 672 Blue and dotted parts change on their own, no media query needed

The interesting part: the media query only switches the header and main's padding. The number of summary columns (grid) and the table scrolling (overflow) change on their own with the available width. Treat media queries as a last resort, and reach first for layouts that stretch and shrink naturally.

AraiIs 768px some kind of rule? An iPad's width?

FuruiPick breakpoints by device name and you'll be chasing every new model. Let the content decide. 768px is simply about where our logo, nav and name sit comfortably in one row.

8

The finished CSS

The HTML changes (new lines in bold):

  <main>
    <div class="page-head">
      <h1>My claims</h1>
      <p><a class="button" href="form.html">+ New claim</a></p>
    </div>

    <section>
      <h2>This month</h2>
      <ul class="summary">
        <li>Claims this month: 4</li>
        <li>Awaiting approval: <strong>¥32,400</strong></li>
        <li>Sent back: 1</li>
      </ul>
    </section>

    <section>
      <h2>All claims</h2>
      <div class="table-wrap" tabindex="0" role="region" aria-label="All claims">
        <table>
          <caption>Claims for October 2026 (newest first)</caption>
          <thead>
          …
        </table>
      </div>
    </section>

And the whole of css/style.css. Lines in bold changed or were added since part 4.

/* ===== 1. Ground rules (colour variables, box sizing, text) ===== */
:root {
  --ink: #1f2b2a;
  --muted: #5b6b75;
  --brand: #1f4e6b;
  --accent: #c0392b;
  --line: #d7dee5;
  --bg: #f4f6f9;
}
*, *::before, *::after {
  box-sizing: border-box;
}
body {
  margin: 0;
  font-family: system-ui, "Segoe UI", Roboto, "Noto Sans", sans-serif;
  line-height: 1.6;
  color: var(--ink);
  background: var(--bg);
}

/* ===== 2. Header ===== */
body > header {
  background: var(--brand);
  color: #fff;
  padding: 12px 24px;
}
body > header a {
  color: #fff;
  text-decoration: none;
}
.site {
  margin: 0;
  font-size: 1.125rem;
  font-weight: 700;
}
.site img {
  vertical-align: middle;
}
nav ul {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
}
nav li {
  display: inline-block;
  margin-right: 16px;
}
nav a:hover,
nav a:focus-visible {
  text-decoration: underline;
}
.user {
  margin: 4px 0 0;
  font-size: 0.875rem;
  opacity: 0.85;
}

/* ===== 3. Main content and headings ===== */
main {
  max-width: 960px;
  margin: 0 auto;
  padding: 16px;
}
h1 {
  margin: 0;
  font-size: 1.5rem;
}
/* Heading and button on opposite sides */
.page-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.page-head p {
  margin: 0;
}
h2 {
  margin: 32px 0 12px;
  padding-left: 10px;
  border-left: 4px solid var(--accent);
  font-size: 1.125rem;
}

/* A link that looks like a button */
.button {
  display: inline-block;
  padding: 8px 16px;
  border-radius: 6px;
  background: var(--accent);
  color: #fff;
  font-weight: 700;
  text-decoration: none;
}
.button:hover {
  background: #a93226;
}
.button:focus-visible {
  outline: 3px solid #f5b041;
  outline-offset: 2px;
}

/* Summary: a grid, as many across as fit (at least 200px) */
.summary {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 12px;
  list-style: none;
  margin: 0;
  padding: 0;
}
.summary li {
  padding: 12px 16px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: #fff;
}
.summary strong {
  display: block;
  color: var(--accent);
  font-size: 1.25rem;
}

/* ===== 4. Table ===== */
/* Table: on narrow screens, only the table scrolls sideways */
.table-wrap {
  overflow-x: auto;
}
.table-wrap:focus-visible {
  outline: 3px solid #f5b041;
}
table {
  width: 100%;
  min-width: 640px;
  border-collapse: collapse;
  border: 1px solid var(--line);
  background: #fff;
}
caption {
  padding-bottom: 8px;
  color: var(--muted);
  font-size: 0.875rem;
  text-align: left;
}
th,
td {
  padding: 10px 12px;
  border-bottom: 1px solid var(--line);
  text-align: left;
}
thead th {
  background: #eef2f6;
  color: var(--muted);
  font-size: 0.875rem;
}
tbody tr:nth-child(even) {
  background: #fafbfc;
}
tbody tr:hover {
  background: #fdf2e9;
}
.amount {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
td time {
  white-space: nowrap;
}

/* Status badges (keep the words; never colour alone) */
.status {
  display: inline-block;
  padding: 2px 10px;
  border: 1px solid;
  border-radius: 999px;
  font-size: 0.8125rem;
  font-weight: 700;
  white-space: nowrap;
}
.status-submitted { color: #8a5a00; background: #fff4d6; }
.status-approved  { color: #1d6b3a; background: #e3f4e9; }
.status-returned  { color: #a93226; background: #fdecea; }
.status-paid      { color: #4d5b66; background: #eceff2; }

/* ===== 5. Footer ===== */
body > footer {
  padding: 24px;
  color: var(--muted);
  text-align: center;
}

/* ===== 6. Screens 768px and wider (tablets, PCs) ===== */
@media (min-width: 768px) {
  /* Header in one row */
  body > header {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px 32px;
  }
  nav ul {
    margin: 0;
  }
  /* Push the user name to the far right */
  .user {
    margin: 0 0 0 auto;
  }
  main {
    padding: 24px;
  }
}
The claims list at PC width with red boxes and numbers: 1 header (flex), 2 user name (margin-left: auto), 3 heading and button row (flex), 4 summary boxes (grid), 5 table wrapper (scrolls sideways)
Where layout was added: 1 body > header, 2 .user, 3 .page-head, 4 .summary, 5 .table-wrap. (The author's demo page, captured in Chromium in the cloud; the phone is Playwright's mobile emulation at 390×844. 17 October 2026. Device and browser frames were drawn in for illustration.)
  • main's padding: 16px on phones, 24px from 768px. Narrow screens get slightly tighter margins too
  • width and inline-block removed from .summary li: the grid columns decide the box widths, so the children get no width. Give them one and they fight the grid
  • h1 margin set to 0: the vertical position of heading and button is left to align-items: center on .page-head

AraiThe AI's "mobile version" had five media queries, overriding the same rules again and again.

FuruiOurs has one, because grid and overflow handle the stretching. Short CSS is easy to find your way around when it needs fixing.

9

How to check

You don't have to send the page to a real phone every time. Chrome DevTools has device mode.

  • Opening it: open DevTools and click the phone-and-tablet icon at the top left (the device toolbar). The shortcut is Ctrl+Shift+M on Windows and Cmd+Shift+M on Mac
  • Drag the width: drag the edge of the screen to change the width and find where things break. Try narrow widths such as 320px too
  • Check in Styles: confirm that rules inside the media query apply at that width (no strikethrough)
NOTE

Device mode is a handy approximation, not a real phone. Check fonts and the feel of touch on a real device. And when an AI has made something "mobile-friendly" for you, drag the width from 320px to 1280px and look for breakage with your own eyes. That's the old-school inspection.

§ — On one page, and what is next

  1. Mobile support starts with meta viewport. Without it, the page is laid out for a 980px screen and shrunk.
  2. There are three ways to arrange boxes: normal flow, flex and grid. One row means flex; cells mean grid.
  3. Flex has a main axis and a cross axis. Arrange with justify-content and align-items; push to the right with margin-left: auto.
  4. Grid's repeat(auto-fit, minmax(200px, 1fr)) changes the number of columns without media queries. Tables scroll inside their own wrapper.
  5. With media queries, write for phones first and add with min-width. Let the content, not the device, choose the breakpoints.

AraiMy hand-written CSS is half the size of the AI's. But do I really write buttons and tables by hand like this every time…?

FuruiGood question. Next time is Bootstrap: the toolbox people built because they were tired of writing it all again. Now that you know what's inside, the tool won't end up using you.

References

The page photos show the author's demo pages, captured on 17 October 2026 in Chromium (Playwright) in the cloud. The phone and tablet are Playwright's mobile emulation (isMobile), not real devices. Segoe UI isn't available there, so the screenshots fall back to other system fonts. Layout widths and zoom, table and column widths, badge heights, flex child positions, grid column widths and the widths where things switch are real values measured in the same Chromium (they shift slightly with text width). index.html and style.css were checked with the Nu Html Checker (vnu) and showed no errors. The characters are fictional.

Old-School Front-End, part 5: CSS layout and phones / 2026.10

← Back to all posts