Code of the Day
AdvancedCSS Architecture

Lab — Refactor a Stylesheet

Take a tangled stylesheet full of IDs, !important overrides, and duplicated color values and refactor it to use cascade layers, design tokens, and consistent naming.

Lab · optionalWeb FoundationsAdvanced35 min
By the end of this lesson you will be able to:
  • Extract color values into semantic CSS custom properties
  • Introduce a spacing scale and replace magic-number margins
  • Set up cascade layers and eliminate !important declarations
  • Replace ID selectors used for styling with class selectors
  • Verify a refactor is safe by checking visual output is unchanged

The techniques in this module are straightforward to explain but only become natural through practice. This lab walks you through a realistic refactor: an existing stylesheet that accumulated the most common structural problems over time, fixed in four focused passes.

Work in your own editor. Copy the starting stylesheet, apply each part in sequence, and check the visual output against the original after each pass.

The starting stylesheet

Below is the stylesheet you will refactor. It is intentionally imperfect — every problem is real and common.

/* ==========================================================================
   STARTING STYLESHEET — do not submit this version
   ========================================================================== */

/* Reset */
* { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, sans-serif; }

/* Header */
#site-header {
  background-color: #3a7bd5;
  color: #ffffff;
  padding: 12px 24px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

#site-header .logo {
  font-size: 1.25rem;
  font-weight: 700;
  color: #ffffff;
  text-decoration: none;
}

#site-header nav a {
  color: #ffffff;
  text-decoration: none;
  margin-left: 16px;
  opacity: 0.85;
}

#site-header nav a:hover {
  opacity: 1;
}

/* Hero */
.hero {
  background-color: #eef3fb;
  padding: 48px 24px;
  text-align: center;
}

.hero h1 {
  font-size: 2rem;
  color: #1a1a2e;
  margin-bottom: 16px;
}

.hero p {
  font-size: 1.125rem;
  color: #555;
  max-width: 560px;
  margin: 0 auto 24px;
}

.hero .cta-button {
  background-color: #3a7bd5;
  color: #ffffff !important;
  padding: 12px 24px;
  border: none;
  border-radius: 4px;
  font-size: 1rem;
  cursor: pointer;
  text-decoration: none;
  display: inline-block;
}

.hero .cta-button:hover {
  background-color: #2a5fb0;
}

/* Cards */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 24px;
  padding: 32px 24px;
}

.card {
  background: #ffffff;
  border: 1px solid #d1d5e0;
  border-radius: 8px;
  padding: 20px;
}

.card h2 {
  font-size: 1.125rem;
  color: #1a1a2e;
  margin-top: 0;
  margin-bottom: 8px;
}

.card p {
  color: #555;
  font-size: 0.875rem;
  line-height: 1.5;
  margin-bottom: 16px;
}

.card a {
  color: #3a7bd5;
  font-weight: 600;
  text-decoration: none;
}

.card a:hover {
  color: #2a5fb0;
}

/* Featured card — override base card styles */
.card.featured {
  border-color: #3a7bd5 !important;
  background-color: #eef3fb !important;
}

/* Footer */
#site-footer {
  background-color: #1a1a2e;
  color: #e8eaf6;
  padding: 24px;
  text-align: center;
  font-size: 0.875rem;
}

#site-footer a {
  color: #93bbf8 !important;
  text-decoration: none;
}

Before you start, take stock of the problems:

  • The color #3a7bd5 appears 6 times; #1a1a2e appears 3 times; #ffffff appears 4 times; #eef3fb appears twice; #2a5fb0 appears twice.
  • !important appears 4 times.
  • #site-header and #site-footer are ID selectors used purely for styling.
  • Margin and padding values (8px, 12px, 16px, 20px, 24px, 32px, 48px) are literal numbers with no consistent scale.

Part 1 — Extract color tokens

Add a :root block at the top of the file (above the reset section) with all named color values. Then replace every hex literal in the rules below with the appropriate var() call.

Here is the token vocabulary to use:

:root {
  /* Primitive palette */
  --color-blue-300: #93bbf8;
  --color-blue-500: #3a7bd5;
  --color-blue-700: #2a5fb0;
  --color-blue-subtle: #eef3fb;
  --color-neutral-900: #1a1a2e;
  --color-neutral-600: #555;
  --color-neutral-200: #d1d5e0;
  --color-neutral-100: #e8eaf6;
  --color-white: #ffffff;

  /* Semantic tokens */
  --color-bg:          var(--color-white);
  --color-text:        var(--color-neutral-900);
  --color-text-muted:  var(--color-neutral-600);
  --color-border:      var(--color-neutral-200);
  --color-interactive: var(--color-blue-500);
  --color-interactive-hover: var(--color-blue-700);
  --color-interactive-subtle: var(--color-blue-subtle);
  --color-interactive-link: var(--color-blue-300);
  --color-surface-brand: var(--color-blue-500);
  --color-surface-dark: var(--color-neutral-900);
  --color-on-brand: var(--color-white);
  --color-on-dark: var(--color-neutral-100);
}

After this pass, no hex value should appear in any rule block. Every color should come from a var() call.

Check: the visual output should be identical to the original.

Part 2 — Introduce a spacing scale

The spacing values in use (8, 12, 16, 20, 24, 32, 48 px) map cleanly onto a 0.25rem base unit scale. Add spacing tokens to the :root block:

:root {
  /* ... color tokens from Part 1 ... */

  /* Spacing scale (base unit: 0.25rem = 4px) */
  --space-2:  0.5rem;   /*  8px */
  --space-3:  0.75rem;  /* 12px */
  --space-4:  1rem;     /* 16px */
  --space-5:  1.25rem;  /* 20px */
  --space-6:  1.5rem;   /* 24px */
  --space-8:  2rem;     /* 32px */
  --space-12: 3rem;     /* 48px */
}

Then replace every pixel value used for padding, margin, and gap with the corresponding token. Leave font-size and border-radius as-is for now — those need their own scales which are outside this lab's scope.

Check: pixel-perfect identical output.

Part 3 — Add cascade layers and remove !important

Wrap the existing rule sections in named layers. The goal is to make !important unnecessary by ensuring utilities and state variants live in a higher layer than base component styles.

@layer reset, tokens, base, components, utilities;

/* Move the :root tokens block here: */
@layer tokens {
  :root {
    /* ... all tokens from Parts 1 and 2 ... */
  }
}

@layer reset {
  * { box-sizing: border-box; }
  body { margin: 0; font-family: system-ui, sans-serif; }
}

@layer base {
  /* header, hero, card-grid, card, footer base styles */
}

@layer components {
  /* .cta-button, .card.featured */
}

@layer utilities {
  /* nothing yet — reserved */
}

Now remove every !important from the rule blocks. Because .card.featured is in @layer components and .card is also in @layer components, you do need to ensure .card.featured appears after .card within that layer — or use slightly more specific selectors to make the intent clear. The point is that no !important flag is needed.

When two rules in the same layer tie on specificity and source order, the later one wins — just like unlayered CSS. Layer order controls inter-layer priority; within a layer, normal cascade rules apply. So move .card.featured after .card in @layer components and you are done.

Check: featured cards still have the blue border and tinted background; CTA buttons are still white text; footer links are still light blue.

Part 4 — Replace ID selectors with class selectors

#site-header and #site-footer are ID selectors used only for styling. They carry unnecessarily high specificity and break the layer model (an ID selector in a lower layer can still beat a class selector in a higher layer if the specificity is high enough — this is the one situation where specificity pierces the layer boundary).

Update the HTML:

<!-- Before -->
<header id="site-header">…</header>
<footer id="site-footer">…</footer>

<!-- After -->
<header id="site-header" class="site-header">…</header>
<footer id="site-footer" class="site-footer">…</footer>

Keep the id attributes — they may be used as anchor targets or JavaScript hooks. Add class attributes alongside them. Then update the CSS rules:

/* Replace all #site-header rules with .site-header */
/* Replace all #site-footer rules with .site-footer */

Check: header and footer are unchanged visually.

Self-review checklist

Run through these before considering the refactor complete:

  • No hex color values remain in rule blocks (all moved to :root tokens)
  • No magic-number px spacing values in padding, margin, or gap rules
  • No !important declarations anywhere in the stylesheet
  • No ID selectors used for styling (IDs may remain in HTML for JS/anchor use)
  • All layers declared at the top in priority order: reset, tokens, base, components, utilities
  • Visual output is indistinguishable from the starting stylesheet in all states (hover, featured card, footer links)

"Visual output is identical" requires actual browser testing — open both versions side by side or use a screenshot diffing tool. It is easy to introduce subtle regressions when changing token names or layer assignments that only become visible in a specific hover state or screen size.

Going further

Once the refactor is stable, consider these next steps on your own:

  1. Add a dark-mode theme: create a [data-theme="dark"] block in @layer tokens that redefines the semantic color tokens. Toggle data-theme on <html> via a button and confirm that the entire page switches without any component-level CSS changes.

  2. Add a --font-size-* scale and a --radius-* scale to the tokens layer, then replace remaining literal values for those properties.

  3. Rename the component classes to follow BEM: site-header__logo, site-header__nav, card__title, card__body, card__link, card--featured. This makes the relationship between elements explicit in the class name itself.

Finished reading? Mark it complete to track your progress.

On this page