Theme Switcher — Practice Mode

Same project, but now YOU write the code. Fill in the blanks for new concepts. Write it yourself when you’ve seen the pattern before.

How this works:

Progress: 0/7 sections • 0%

A Understanding Theme Switching

Goal: Understand the three ingredients that make a theme switcher work.

The Big Picture

A theme switcher changes colors across your entire page in one move. Instead of rewriting every color when the user picks “dark mode,” you change a single attribute on the <html> element and let CSS do the rest.

The Three Ingredients

  • CSS Custom Properties — Variables like --color-surface that hold your colors.
  • A Dropdown UI — The menu that lets users pick Light, Dark, or System.
  • JavaScript — The glue. It listens for clicks, swaps the data-theme attribute, and saves the choice.

The Data Flow

When a user clicks “Dark”:

  1. JavaScript sets document.documentElement.dataset.theme = "dark"
  2. The <html> tag now has data-theme="dark"
  3. CSS rule [data-theme="dark"] activates and overrides the variables
  4. Every element using var(--color-surface) instantly updates
  5. JavaScript saves "dark" to localStorage

Your Starting HTML

Your CodePen already has this structure:

<div class="theme-dropdown">
  <button class="theme-trigger">
    <span>Theme</span>
  </button>
  <div class="theme-menu">
    <ul class="theme-list">
      <li><button class="color-theme-option" data-theme="system">OS default</button></li>
      <li><button class="color-theme-option" data-theme="light">Light</button></li>
      <li><button class="color-theme-option" data-theme="dark">Dark</button></li>
    </ul>
  </div>
</div>

<main class="card">
  <h1>Theme Toggle Demo</h1>
  <p>This text will change color when the theme switches.</p>
</main>

Reflection:

Before writing any CSS: look at the HTML. What do the data-theme attributes do? How will CSS and JS use them?

Auto-saved ✓

B CSS Custom Properties & Theme Colors

Goal: Define color variables and create light/dark rule sets.

Step 1: Define your default variables Fill in the blank

CSS custom properties (variables) always start with --. They go inside :root so they’re available everywhere. Fill in the variable names:

:root {
  __________________: #ffffff;
  __________________: #111111;
}
Hint: You need a variable for the page background (surface) and one for the text color.

Step 2: Apply variables to body Fill in the blank

Instead of hard-coding colors, use the var() function to reference your custom properties. Fill in the two values:

body {
  margin: 0;
  padding: 2rem;
  font-family: system-ui, sans-serif;
  background: __________________;
  color: __________________;
}
Hint: The syntax is var(--your-variable-name)

Step 3: Add the light theme rule Fill in the blank

CSS can target HTML attributes using bracket notation. Fill in the selector that matches when <html> has data-theme="light":

______________________ {
  --color-surface: #ffffff;
  --color-text: #111111;
}
Hint: Attribute selectors use square brackets: [attribute="value"]

Step 4: Add the dark theme rule Write it yourself

Same pattern as Step 3, but for the dark theme. Use these colors:

  • Surface: #1a1a1a
  • Text: #f0f0f0

Write the complete CSS rule for [data-theme="dark"] that overrides both variables.

Test it in DevTools

Open DevTools (F12). Find the <html> element and manually add data-theme="dark". Your colors should flip. Remove it to go back.

Reflection:

Why use CSS variables instead of writing separate stylesheets for light and dark?

Auto-saved ✓

C Styling the Dropdown Container

Goal: Style the trigger button and position the hidden menu below it.

Key properties to know:

  • position: relative — Creates a positioning context for child elements
  • position: absolute — Positions relative to nearest positioned ancestor
  • display: none — Completely hides an element
  • top: 100% — Places element right below its parent

Step 1: Position the dropdown wrapper Fill in the blank

The wrapper needs a specific position value so the menu (which will be absolute) knows where to anchor itself. What goes here?

.theme-dropdown {
  position: __________;
}
Hint: It’s not absolute, not fixed, not static. It creates a positioning context without moving the element itself.

Step 2: Style the trigger button Write it yourself

Style .theme-trigger to look like a clickable button. Include:

  • Black background, white text
  • Some padding (at least 0.5rem)
  • No default border
  • cursor: pointer

Write the .theme-trigger rule. The exact values are up to you — just make it look intentional, not default.

Step 3: Hide and position the menu Fill in the blank

The menu needs to: (1) start hidden, (2) sit below the trigger without pushing content around, (3) know where “below” is relative to the dropdown wrapper.

.theme-menu {
  display: ________;
  position: __________;
  top: ________;
  left: 0;
  background: #000;
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 4px;
  z-index: 10;
}
Hints: Blank 1 hides the element completely. Blank 2 removes it from normal flow and positions it relative to the nearest positioned ancestor. Blank 3 puts it at the bottom edge of its parent.

Step 4: Reset the list styles

Remove default bullets and spacing from the <ul>:

.theme-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

Reflection:

What would happen if you forgot position: relative on .theme-dropdown? Where would the menu appear?

Auto-saved ✓

D Styling the Menu Buttons

Goal: Style the option buttons and create the rule that reveals the menu.

Step 1: Style the option buttons

Each option needs flexbox for icon+text alignment and full-width click targets:

.color-theme-option {
  display: flex;
  align-items: center;
  column-gap: 0.5rem;
  width: 100%;
  padding: 0.5rem 0.75rem;
  margin: 0;
  font-size: 1rem;
  color: white;
  background-color: #000000;
  border: none;
  cursor: pointer;
}

Step 2: Add a hover state Write it yourself

Write a :hover rule for .color-theme-option that changes the background to #333.

Hint: The selector is .color-theme-option:hover

Step 3: The reveal rule Fill in the blank

When JavaScript adds a class to the dropdown wrapper, this CSS rule should show the menu. Fill in the missing class name and display value:

.theme-dropdown.______ .theme-menu {
  display: ________;
}
Hint: The class name describes the menu’s state. The display value is the opposite of none for block-level content.

Test it in DevTools

Find .theme-dropdown in the Elements panel. Manually add open to its class list. The menu should appear. Remove it and it disappears.

Reflection:

Why is toggling a CSS class better than directly setting style.display in JavaScript?

Auto-saved ✓

E JavaScript — Opening & Closing the Dropdown

Goal: Wire up JavaScript so clicking the trigger toggles the menu, and clicking elsewhere closes it.

Key concepts:

  • classList.toggle("open") — Adds the class if missing, removes it if present
  • event.stopPropagation() — Prevents the click from bubbling up to parent elements
  • element.contains(event.target) — Checks if a clicked element is inside another element

Step 1: Select your elements Fill in the blank

Use querySelector with the CSS class selectors to grab the dropdown wrapper and trigger button:

const dropdown = document.querySelector("________________");
const trigger = document.querySelector("________________");
Hint: These are CSS class selectors. Look at the HTML — what classes do the wrapper <div> and the <button> have?

Step 2: Toggle on trigger click Fill in the blank

When the trigger is clicked: (1) stop the event from bubbling, and (2) toggle the open class on the dropdown wrapper.

trigger.addEventListener("click", function (event) {
  event.__________________();
  dropdown.classList.__________("open");
});
Hints: Blank 1 stops the event from reaching parent elements (the word literally means “stop propagation”). Blank 2 is a classList method that adds a class if missing and removes it if present.

Why stopPropagation()?

Without it, the click bubbles up to the document listener (Step 3), which would immediately close the menu. The open and close would happen in the same tick — the menu would never visibly appear.

Step 3: Close when clicking outside Write it yourself

Add a "click" listener on document. Inside the handler, check if the click target is outside the dropdown. If it is, remove the "open" class.

Hints:
  • Use document.addEventListener("click", function (event) { ... })
  • Use dropdown.contains(event.target) to check if the click is inside
  • The ! operator flips the boolean — “if NOT inside”
  • Use classList.remove("open") (not toggle) to close

Test it

Click the button — menu opens. Click it again — menu closes. Click somewhere else — menu closes.

Common bug

If the menu opens and immediately closes, you probably forgot event.stopPropagation() in Step 2.

Reflection:

Could you use classList.toggle in the document listener instead of classList.remove? Why or why not?

Auto-saved ✓

F JavaScript — Loading & Applying Themes

Goal: Wire up each theme button, save the choice, and restore it on reload.

Step 1: Select the theme buttons Write it yourself

Select the three theme buttons using attribute selectors. You need three const declarations using querySelector.

Hints:
  • Attribute selector syntax: button[data-theme='value']
  • Name them systemBtn, lightBtn, darkBtn
  • The values to match are 'system', 'light', and 'dark'

Step 2: Wire up the Light button Fill in the blank

When clicked, this handler needs to: set the data-theme attribute on <html>, save to localStorage, and close the dropdown.

lightBtn.addEventListener("click", function () {
  document.___________________.dataset.theme = "light";
  localStorage.___________("theme", "light");
  dropdown.classList.remove("open");
});
Hints: Blank 1 is the property that gives you the <html> element (it literally means “document element”). Blank 2 is the localStorage method that saves a key-value pair.

Breaking this down

  • document.documentElement — The <html> element
  • .dataset.theme = "light" — Sets data-theme="light" on it
  • localStorage.setItem("theme", "light") — Saves the string under the key "theme"

Step 3: Wire up the Dark button Write it yourself

Same exact pattern as the light button. Just change the three instances of "light" to "dark".

Step 4: Wire up the System button Fill in the blank

This button saves "system" and then checks the OS preference to decide light or dark:

systemBtn.addEventListener("click", function () {
  localStorage.setItem("theme", "system");
  const osDark = window.____________________________________________;
  if (osDark) {
    document.documentElement.dataset.theme = "dark";
  } else {
    document.documentElement.dataset.theme = "light";
  }
  dropdown.classList.remove("open");
});
Hint: window.matchMedia() takes a media query string like CSS uses. You want to check (prefers-color-scheme: dark) and read the .matches property.

Step 5: Load the saved theme on page load Write it yourself

This is the boss fight. Write code that runs at the top of your JS file. It should:

  1. Read the saved theme from localStorage (key is "theme")
  2. If there’s no saved value, OR if it’s "system": check the OS preference with matchMedia and set the theme accordingly
  3. Otherwise: set data-theme to whatever was saved
Hints:
  • localStorage.getItem("theme") returns null if nothing is saved
  • !saved is truthy when saved is null
  • You can combine conditions: if (!saved || saved === "system")
  • Reuse the matchMedia pattern from Step 4

Final test

  1. Click “Dark” — page goes dark
  2. Refresh the page — it stays dark
  3. Click “Light” — page goes light
  4. Click “OS default” — matches your system setting
  5. Refresh again — persists correctly

You built a theme switcher!

The full loop works: user clicks → JS sets the attribute → CSS swaps variables → localStorage remembers.

Reflection:

The “system” option saves "system" to localStorage, not the resolved "dark" or "light". Why does that matter? What breaks if you save the resolved value?

Auto-saved ✓

G Final Step: Document Your Thinking

Goal: Add your reflections to your code as comments.

Before you finish:

  1. Click the "Copy All My Responses" button below
  2. Open your CodePen’s JavaScript panel
  3. Paste your responses at the top as a comment block:
/*
  MY REFLECTIONS
  ==============
  [Paste your copied responses here]
*/

Why? Your reflections are part of your learning. Adding them as comments shows your thinking process and helps you remember what you learned when you revisit this code later.

Congratulations!

You built a fully functional theme switcher — and you wrote most of the code yourself.

  • Define and override CSS custom properties with :root and attribute selectors
  • Build a dropdown pattern: trigger button + absolutely positioned menu
  • Toggle visibility with a CSS class (.open) controlled by JavaScript
  • Close dropdowns on outside click using event bubbling
  • Persist user preferences with localStorage
  • Read OS dark mode preference with matchMedia

What you built:

  • CSS variables that swap all colors in one attribute change
  • A dropdown menu using the relative/absolute positioning pattern
  • Three theme options including OS-aware “system” mode
  • Persistence across page reloads with localStorage

Where to go from here:

  • Add more variables: borders, shadows, accent colors, link colors
  • Add transitions: transition: background-color 0.3s, color 0.3s for smooth changes
  • Keyboard accessibility: Arrow keys to navigate, Escape to close
  • Active indicator: Highlight which theme is currently selected