Semantic HTML – Practical Refactors

Turn div soup into resilient, accessible markup step by step

Course progress

Lesson 1 of 9 – Welcome & overview Lesson 2 of 9 – Refactor workflow: from “what is this?” to semantic markup Lesson 3 of 9 – Navigation bar refactors (legacy → semantic) Lesson 4 of 9 – Hero & main heading refactors Lesson 5 of 9 – Card grids and blog list refactors Lesson 6 of 9 – Form, label, and error state refactors Lesson 7 of 9 – Sidebars, asides, and layout containers Lesson 8 of 9 – Layout tables → semantic layout, and true data tables Lesson 9 of 9 – Mini project: refactor a whole page + checklist

Step 1

Welcome & how this tutorial works

This tutorial is a hands-on companion to "Semantic HTML that Survives Redesigns". Instead of focusing on theory, we’ll work through concrete examples:

  • Start with realistic "before" HTML (div soup, framework classes, layout hacks).
  • Refactor it into semantic, resilient markup.
  • Keep the original classes where possible so changes are incremental and safe.
  • Use a repeatable method you can apply in your own projects.

You can follow along in:

  • A throwaway index.html file.
  • An online playground (CodePen, JSFiddle, StackBlitz, etc.).
  • Your own project (copy/paste the examples into a sandbox branch).

Each lesson includes:

  • Before → After examples.
  • Explanations of why the refactor improves semantics and resilience.
  • A quick check question to reinforce the idea.
  • Occasional “interactive challenge” prompts you can try yourself.

You don't need to be perfect. The goal is to train your eye to spot:
"This is layout-driven/framework-shaped HTML" versus "This is semantic, framework-agnostic HTML".

Key terms we’ll use

  • Custom property – the underlying CSS feature (e.g. --color-bg).
  • Design token – a named value used across a design system.
  • Global token – a token defined in :root for site-wide use.
  • Component token – a token scoped to a component (e.g. .button).
Lesson 1 of 9
Step 2

A simple refactor method you can reuse

Why :root for tokens?

:root selects the root element of the document (the <html> element). It’s ideal for tokens because:

  • It’s only defined once per document.
  • Values cascade to the entire page.
  • You can override tokens later in more specific scopes.
:root {
  /* Colour tokens */
  --color-bg: #f8f3f0;
  --color-surface: #ffffff;
  --color-text: #1e2f26;
  --color-accent: #3c5d4a;

  /* Typography tokens */
  --font-family-base: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-family-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", monospace;
}

Using var() with fallbacks

var() reads a custom property. You can optionally provide a fallback:

.page {
  background-color: var(--color-bg, #ffffff);
  color: var(--color-text, #222222);
}

pre {
  font-family: var(--font-family-mono, monospace);
}

The fallback is used when the token isn’t defined, which is handy when refactoring older CSS in stages.

Quick check: where do tokens live?

For global design tokens that should apply site-wide, which selector is usually the best home?

Lesson 2 of 9
Step 3

Refactoring navigation bars

Core colour tokens

Start with a small set of roles instead of hundreds of direct hex codes:

  • --color-bg – page background
  • --color-surface – card and panel background
  • --color-text – body text
  • --color-muted – secondary text
  • --color-accent – brand accent colour
  • --color-accent-soft – subtle accent background
:root {
  color-scheme: light dark;

  --color-bg: #f8f3f0;
  --color-surface: #ffffff;
  --color-text: #1e2f26;
  --color-muted: #4c6258;

  --color-accent: #3c5d4a;
  --color-accent-soft: #e0ece5;
}

Hooking into your dark mode toggle

If your site toggles a .dark-mode class on <body> (as this template does), you can flip the tokens there:

body.dark-mode {
  --color-bg: #070b09;
  --color-surface: #111a15;
  --color-text: #f5f5f5;
  --color-muted: #c6d1cc;

  --color-accent: #90c3a2;
  --color-accent-soft: #1b2a22;
}

All components that reference these tokens automatically adapt when dark mode is on.

Example: token-driven card

Card styles powered by tokens

Notice how the card never references raw hex colours, only tokens.

.token-card {
  background-color: var(--color-surface);
  color: var(--color-text);
  border-radius: var(--radius-md);
  border: 1px solid var(--color-accent-soft);
  box-shadow: var(--shadow-soft);
}

.token-card__eyebrow {
  color: var(--color-muted);
}

.token-card__cta {
  background-color: var(--color-accent);
  color: var(--color-on-accent, #ffffff);
}
Lesson 3 of 9
Step 4

Refactoring hero sections & page headings

Font family & weight tokens

First, define your stacks and common weights:

:root {
  --font-family-base: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-family-heading: "Segoe UI", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
  --font-family-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", monospace;

  --font-weight-regular: 400;
  --font-weight-medium: 500;
  --font-weight-bold: 700;
}

Type scale tokens

Next, define a simple scale. The exact values are less important than using the same ones everywhere:

:root {
  --font-size-100: 0.875rem; /* small text */
  --font-size-200: 1rem;     /* body */
  --font-size-300: 1.125rem; /* lead text */
  --font-size-400: 1.375rem; /* h3 */
  --font-size-500: 1.75rem;  /* h2 */
  --font-size-600: 2.25rem;  /* h1 */

  --line-height-tight: 1.2;
  --line-height-normal: 1.5;
  --line-height-relaxed: 1.7;
}

You can later combine these with clamp() for fluid type, but a fixed scale is a solid start.

Example: applying the scale

Heading and body text with tokens

Notice how each element references the same small set of tokens.

h1 {
  font-family: var(--font-family-heading);
  font-size: var(--font-size-600);
  line-height: var(--line-height-tight);
  font-weight: var(--font-weight-bold);
}

h2 {
  font-family: var(--font-family-heading);
  font-size: var(--font-size-500);
  line-height: var(--line-height-tight);
}

body {
  font-family: var(--font-family-base);
  font-size: var(--font-size-200);
  line-height: var(--line-height-normal);
}

Quick check: typography tokens

Which of these is the best name for a reusable body text size token?

Lesson 4 of 9
Step 5

Refactoring card grids & article lists

Defining a spacing scale

A simple 0–6 scale is often enough:

:root {
  --space-0: 0;
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
}

You can layer larger tokens on top later (e.g. --space-page).

Using spacing tokens in layout

Stack and cluster patterns with tokens

Spacing tokens work beautifully with layout utility classes.

.stack > * + * {
  margin-block-start: var(--space-3);
}

.cluster {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.card {
  padding: var(--space-4);
  border-radius: var(--radius-md);
}

Size tokens

You can also define common fixed sizes:

:root {
  --size-input-height: 2.5rem;
  --size-container-max: 70rem;
  --size-avatar-sm: 2rem;
  --size-avatar-md: 3rem;
}
Lesson 5 of 9
Step 6

Refactoring forms & errors

Radius tokens

Define a small radius scale:

:root {
  --radius-none: 0;
  --radius-sm: 0.15rem;
  --radius-md: 0.35rem;
  --radius-lg: 0.75rem;
  --radius-pill: 999px;
}

Border & shadow tokens

Pair radius tokens with borders and shadows:

:root {
  --border-width-hairline: 1px;
  --border-width-strong: 2px;
  --border-color-default: var(--color-accent-soft);

  --shadow-none: none;
  --shadow-soft: 0 2px 6px rgba(0, 0, 0, 0.08);
  --shadow-strong: 0 4px 14px rgba(0, 0, 0, 0.16);
}

Example: tokenised button

Button with radius & shadow tokens

All visual decisions are stored in tokens, so swapping style later is trivial.

.button {
  --button-radius: var(--radius-pill);
  --button-shadow: var(--shadow-soft);

  border-radius: var(--button-radius);
  box-shadow: var(--button-shadow);
  border: var(--border-width-hairline) solid transparent;
}

.button:focus-visible {
  outline: none;
  box-shadow:
    0 0 0 2px var(--color-bg),
    0 0 0 4px var(--color-accent);
}
Lesson 6 of 9
Step 7

Refactoring sidebars & layout helpers

Layering component tokens

Use global tokens inside component tokens, then reference only the component-level names in your rules:

.button {
  /* Component tokens */
  --button-bg: var(--color-accent);
  --button-bg-hover: color-mix(in srgb, var(--color-accent) 90%, #000 10%);
  --button-text: var(--color-on-accent, #ffffff);
  --button-padding-inline: var(--space-4);
  --button-padding-block: var(--space-2);

  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding-inline: var(--button-padding-inline);
  padding-block: var(--button-padding-block);
  border-radius: var(--radius-pill);
  background-color: var(--button-bg);
  color: var(--button-text);
}

Variants by overriding tokens

Primary vs secondary buttons via overrides

Each variant overrides only the component tokens it needs.

.button--secondary {
  --button-bg: transparent;
  --button-bg-hover: var(--color-accent-soft);
  --button-text: var(--color-accent);
  --button-border-color: var(--color-accent);

  border: var(--border-width-hairline) solid var(--button-border-color);
  background-color: var(--button-bg);
}

Contextual overrides

You can also override tokens at a higher level, like a hero section:

.hero {
  --color-accent: #dbb5b4;  /* dusty rose in this section */
}

.hero .button {
  /* Buttons inside .hero use the new accent automatically */
}
Lesson 7 of 9
Step 8

Refactoring tables & tabular data

Timing & motion tokens

Group your motion decisions together:

:root {
  --duration-fast: 120ms;
  --duration-normal: 200ms;
  --duration-slow: 320ms;

  --easing-standard: cubic-bezier(0.2, 0, 0.13, 1);
}
.button {
  transition:
    background-color var(--duration-normal) var(--easing-standard),
    box-shadow var(--duration-fast) var(--easing-standard);
}

Layer & breakpoint tokens

Z-index and width tokens help avoid magic numbers:

:root {
  --z-base: 0;
  --z-dropdown: 1000;
  --z-modal: 1100;

  --layout-max-width: 72rem;
}

Media query conditions can’t (yet) use var() everywhere, but you can still reuse breakpoint widths inside your rules:

@media (min-width: 48rem) {
  .layout {
    max-width: var(--layout-max-width);
    margin-inline: auto;
  }
}

Quick check: good token candidates

Which of these is least helpful as a token?

Lesson 8 of 9
Step 9

Mini project & semantic review checklist

Tokenised profile card example

A small layout driven entirely by tokens

The HTML doesn’t know anything about colours, fonts, or spacing. The CSS tokens carry all of that knowledge.

<article class="token-card">
  <p class="token-card__eyebrow">CSS design tokens</p>
  <h2 class="token-card__title">Build once, theme everywhere</h2>
  <p>Use :root and custom properties to define colours,
    spacing, type, and radius just once.</p>
  <button class="button">View tokens</button>
</article>
.token-card {
  padding: var(--space-4);
  border-radius: var(--radius-md);
  background-color: var(--color-surface);
  color: var(--color-text);
  box-shadow: var(--shadow-soft);
  max-width: 26rem;
}

.token-card__eyebrow {
  font-size: var(--font-size-100);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-muted);
  margin-block-end: var(--space-2);
}

.token-card__title {
  font-size: var(--font-size-400);
  margin-block-end: var(--space-3);
}

.token-card p {
  margin-block-end: var(--space-4);
}

Checklist: robust token system

  • Define tokens in :root for: colour, type, spacing, radius, shadows, motion, and layers.
  • Keep names neutral and reusable (e.g. --space-3, not --card-gap).
  • Use tokens everywhere instead of raw hex, pixel, or timing values.
  • Use component-level tokens to create variants without duplicating CSS.
  • Hook your light/dark theme toggle into token overrides, not per-component rules.
  • Introduce tokens gradually into legacy code using var() fallbacks.
Lesson 9 of 9