---
id: 687ad7be1ff45032ccf1d92f
title: Build a Theme Switcher
challengeType: 25
dashedName: lab-theme-switcher
demoType: onClick
---

# --description--

In this lab, you will build an app that switches between different themes. When you switch to a different theme, the background color will change and a different message will display.

**Objective:** Fulfill the user stories below and get all the tests to pass to complete the lab.

**User Stories:**

1. You should have a `button` element with the text `Switch Theme`.
2. Your `button` element should have the following attributes:
   - An `id` attribute set to `"theme-switcher-button"`.
   - An `aria-haspopup` attribute set to `"true"`.
   - An `aria-expanded` attribute set to `"false"`.
   - An `aria-controls` attribute set to `"theme-dropdown"`.
3. You should have a `ul` element with the following attributes:
   - An `id` attribute set to `"theme-dropdown"`.
   - A `role` attribute set to `"menu"`.
   - An `aria-labelledby` attribute set to `"theme-switcher-button"`.
   - A `hidden` attribute.
4. Your `ul` element should have at least two `li` elements with a `role` attribute set to `"menuitem"` and text of your choice representing a different theme.
5. Each of your `li` elements should have an `id` attribute that starts with `theme-` and ends with the theme you set for the `li` element text. For example, if one of your themes is `Light` then your `id` should be `theme-light`.
6. You should have an element with an `aria-live` attribute set to `"polite"`.
7. You should have a `themes` array with at least two objects that each contain a `name` and `message` property. The `name` will represent a different theme and the `message` will display when the theme switches. You are free to come up with `name` and `message` values of your choice but the `name` values should match the text content of your `li` elements.
8. When a user clicks on the `#theme-switcher-button`, the `#theme-dropdown` element should display the available themes.
9. When the `#theme-dropdown` element is displayed, the `aria-expanded` attribute should be set to `"true"` for the `button` element.
10. When the users clicks on the `#theme-switcher-button` while the `#theme-dropdown` element is displayed, the menu should be closed and the `hidden` attribute should be added and the `aria-expanded` attribute should be set to `"false"`.
11. When a user clicks on the `#theme-switcher-button` and selects a theme, the corresponding `theme-<name>` class should be added to the `body` element and the related theme message you set in the `themes` array should display in the `aria-live="polite"` element. 
12. When a user clicks on the `#theme-switcher-button` and selects a different theme than the current one, the `body` theme class and status message should update to match the new selection.

# --before-all--

```js
const resetThemeSwitch = () => {
  const themeDropdownBtn = document.getElementById("theme-switcher-button");
  const themeDropdownEl = document.getElementById("theme-dropdown");

  // Reset attributes due to earlier button clicks interfering with these tests
  themeDropdownEl.setAttribute("hidden", "");
  themeDropdownBtn.setAttribute("aria-expanded", "false");
}
```

# --hints--

You should have a `button` element.

```js
assert.exists(document.querySelector("button"));
```

Your `button` element should have the text `Switch Theme`.

```js
const btn = document.querySelector("button");
assert.equal(btn?.textContent.trim(), "Switch Theme");
```

Your `button` element should have an `id` attribute set to `"theme-switcher-button"`.

```js
const btn = document.querySelector("button");
assert.equal(btn?.getAttribute("id"), "theme-switcher-button");
```

Your `button` element should have an `aria-haspopup` attribute set to `"true"`. 

```js
const btn = document.querySelector("button");
assert.equal(btn?.getAttribute("aria-haspopup"), "true");
```

Your `button` element should have an `aria-expanded` attribute set to `"false"`. 

```js
const btn = document.querySelector("button");
assert.equal(btn?.getAttribute("aria-expanded"), "false");
```

Your `button` element should have an `aria-controls` attribute set to `"theme-dropdown"`.

```js
const btn = document.querySelector("button");
assert.equal(btn?.getAttribute("aria-controls"), "theme-dropdown");
```

You should have an `ul` element.

```js
assert.exists(document.querySelector("ul"));
```

Your `ul` element should have an `id` attribute set to `"theme-dropdown"`.

```js
const ul = document.querySelector("ul");
assert.equal(ul?.getAttribute("id"), "theme-dropdown");
```

Your `ul` element should have an `role` attribute set to `"menu"`.

```js
const ul = document.querySelector("ul");
assert.equal(ul?.getAttribute("role"), "menu");
```

Your `ul` element should have an `aria-labelledby` attribute set to `"theme-switcher-button"`.

```js
const ul = document.querySelector("ul");
assert.equal(ul?.getAttribute("aria-labelledby"), "theme-switcher-button");
```

Your `ul` element should have a `hidden` attribute.

```js
const ul = document.querySelector("ul");
assert.isTrue(ul?.hasAttribute("hidden"));
```

Your `ul` element should have at least two `li` elements.

```js
const listItems = document.querySelectorAll("ul li");
assert.isAtLeast(listItems?.length, 2);
```

Each of your `li` elements should have a `role` attribute set to `"menuitem"`.

```js
const listItems = document.querySelectorAll("ul li");
assert.isNotEmpty(listItems);
listItems.forEach(li => {
  assert.equal(li?.getAttribute("role"), "menuitem");
})
```

Each of your `li` elements should have a piece of text that represents a theme.

```js
const listItems = document.querySelectorAll("ul li");
assert.isNotEmpty(listItems);
listItems.forEach(li => {
  const text = li?.textContent.trim();
  assert.isTrue(text.length > 0);
})
```

Each of your `li` elements should have an `id` attribute that starts with `theme-` and ends with the corresponding theme for that element. Make sure your `id` value is all lowercase.

```js
const listItems = document.querySelectorAll("ul li");
assert.isNotEmpty(listItems);
const listTitles = [...listItems].map(li => li?.textContent.trim().toLowerCase());

listItems.forEach((li, index) =>{
  assert.equal(li.getAttribute("id"), `theme-${listTitles[index]}`);
})
```

You should have an element with an `aria-live` attribute set to `"polite"`.

```js
assert.exists(document.querySelector('[aria-live="polite"]'));
```

You should have a `themes` array.

```js
assert.isArray(themes);
```

Your `themes` array should have at least two objects.

```js
assert.isAtLeast(themes?.length, 2);
themes.forEach(theme => {
  assert.isObject(theme);
});
```

Your `themes` array should have at least two objects each with a `name` and `message` property. 

```js
assert.isNotEmpty(themes);
themes?.forEach(theme => {
  assert.isObject(theme);
  assert.property(theme, "name");
  assert.property(theme, "message");
})
```

Your `name` and `message` property values should be strings that are not empty.

```js
assert.isNotEmpty(themes);
themes?.forEach(theme => {
  assert.isObject(theme);
  assert.isString(theme["name"]);
  assert.isNotEmpty(theme["name"]);
  assert.isString(theme["message"]);
  assert.isNotEmpty(theme["message"]);
})
```

Each of your `name` property values should match the text content of one of your `li` elements. Make sure these values are all in lowercase.

```js
const listItems = [...document.querySelectorAll("ul li")]?.map(li => li.textContent.toLowerCase().trim());
const namesArr = themes?.map(({name}) => name);

assert.equal(listItems.length, namesArr.length);
assert.isTrue(namesArr?.every(name => listItems.includes(name)));
```

When a user clicks on the `#theme-switcher-button` to display the theme options, the `hidden` attribute should be removed from the `#theme-dropdown` element.

```js
const themeDropdownBtn = document.getElementById("theme-switcher-button");
const themeDropdownEl = document.getElementById("theme-dropdown");

assert.isTrue(themeDropdownEl?.hasAttribute("hidden"));
themeDropdownBtn?.dispatchEvent(new Event("click"));
assert.isFalse(themeDropdownEl?.hasAttribute("hidden"));
```

When the `#theme-dropdown` element is displayed, the `aria-expanded` attribute should be set to `"true"` for the `button` element.

```js
resetThemeSwitch()
const themeDropdownBtn = document.getElementById("theme-switcher-button");

themeDropdownBtn?.dispatchEvent(new Event("click"));
assert.equal(themeDropdownBtn?.getAttribute("aria-expanded"), "true");
```

When the users clicks on the `#theme-switcher-button` while the `#theme-dropdown` element is displayed, the dropdown menu should be set back to `hidden`.

```js
resetThemeSwitch()
const themeDropdownBtn = document.getElementById("theme-switcher-button");
const themeDropdownEl = document.getElementById("theme-dropdown");

themeDropdownBtn?.dispatchEvent(new Event("click"));
assert.isFalse(themeDropdownEl?.hasAttribute("hidden"));
themeDropdownBtn?.dispatchEvent(new Event("click"));
assert.isTrue(themeDropdownEl?.hasAttribute("hidden"));
```

When the users clicks on the `#theme-switcher-button` while the `#theme-dropdown` element is displayed, the `aria-expanded` attribute should be set to `"false"`.

```js
resetThemeSwitch()
const themeDropdownBtn = document.getElementById("theme-switcher-button");

themeDropdownBtn?.dispatchEvent(new Event("click"));
assert.equal(themeDropdownBtn?.getAttribute("aria-expanded"), "true");
themeDropdownBtn?.dispatchEvent(new Event("click"));
assert.equal(themeDropdownBtn?.getAttribute("aria-expanded"), "false");
```

When a user clicks on the `#theme-switcher-button` and selects a theme, the corresponding `theme-<name>` class should be added to the `body` element.

```js
const themeDropdownBtn = document.getElementById("theme-switcher-button");
const listItems = document.querySelectorAll("#theme-dropdown li");

themeDropdownBtn.dispatchEvent(new Event("click"));

const clickedItem = listItems[1];
clickedItem?.dispatchEvent(new Event("click", {bubbles: true}));

const expectedClass = clickedItem?.id;
const actualClass = document.body.className;

assert.equal(actualClass, expectedClass);
```

When a user clicks the `#theme-switcher-button` and selects a theme, a `message` related to the selected `theme` from the `themes` array should be displayed in the `aria-live="polite"` element.


```js
const liveRegion = document.querySelector('[aria-live="polite"]');
const themeDropdownBtn = document.getElementById("theme-switcher-button");
const listItems = document.querySelectorAll("ul li");

themeDropdownBtn?.dispatchEvent(new Event("click"));
listItems[1]?.dispatchEvent(new Event("click", {bubbles: true}));

const selectedThemeName = listItems[1]?.textContent.toLowerCase().trim();
const selectedTheme = themes?.find(theme => theme.name === selectedThemeName);

assert.equal(liveRegion?.textContent, selectedTheme?.message);
```

When a user clicks on the `#theme-switcher-button` and selects two different themes, the `body` class and status message should update to match each selected theme.

```js
const liveRegion = document.querySelector('[aria-live="polite"]');
const themeDropdownBtn = document.getElementById("theme-switcher-button");
const listItems = document.querySelectorAll("#theme-dropdown li");

themeDropdownBtn?.dispatchEvent(new Event("click"));
listItems[0]?.dispatchEvent(new Event("click", { bubbles: true }));
const firstClass = document.body.className;
const firstMessage = liveRegion?.textContent;

themeDropdownBtn?.dispatchEvent(new Event("click"));
listItems[1]?.dispatchEvent(new Event("click", { bubbles: true }));
const secondClass = document.body.className;
const secondMessage = liveRegion?.textContent;

assert.equal(firstClass, listItems[0]?.id);
assert.equal(secondClass, listItems[1]?.id);
assert.notEqual(firstClass, secondClass);
assert.notEqual(firstMessage, secondMessage);
```

# --seed--

## --seed-contents--

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Theme Switcher</title>
  <link rel="stylesheet" href="./styles.css">
</head>
<body>

  <script src="./script.js"></script>
</body>
</html>
```

```css
body {
  margin: 0;
  font-family: sans-serif;
  transition: background 0.3s, color 0.3s;
}

ul {
  margin: 0;
  padding: 0;
}

li {
  list-style-type: none;
}

#status {
  text-align: center;
  min-height: 20px;
}
```

```js
```

# --solutions--

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Theme Switcher</title>
  <link rel="stylesheet" href="./styles.css">
</head>
<body class="theme-light">  
  <button
    class="dropdown-btn"
    id="theme-switcher-button"
    aria-haspopup="true"
    aria-expanded="false"
    aria-controls="theme-dropdown"
  >
    Switch Theme
  </button>

  <ul
    class="dropdown-content"
    id="theme-dropdown"
    role="menu"
    aria-labelledby="theme-switcher-button"
    hidden
  >
    <li class="dropdown-item" id="theme-light" role="menuitem">Light</li>
    <li class="dropdown-item" id="theme-dark" role="menuitem">Dark</li>
    <li class="dropdown-item" id="theme-ocean" role="menuitem">Ocean</li>
    <li class="dropdown-item" id="theme-nord" role="menuitem">Nord</li>
  </ul>
  
  <div id="status" aria-live="polite"></div>
  
  <script src="./script.js"></script>
</body>
</html>
```

```css
body {
  margin: 0;
  font-family: sans-serif;
  transition: background 0.3s, color 0.3s;
}

ul {
  margin: 0;
  padding: 0;
}

li {
  list-style-type: none;
}

.dropdown-btn {
  position: relative;
  padding: 10px 20px;
  border: none;
  cursor: pointer;
}

.dropdown-content {
  position: absolute;
  background-color: #ffffff;
  min-width: 160px;
  box-shadow: 0px 4px 8px rgba(0,0,0,0.2);
  z-index: 1;
}

.dropdown-item {
  width: 100%;
  padding: 12px;
  color: #000000;
  background-color: #ffffff;
  cursor: pointer;
}

.dropdown-item:hover {
  background-color: #f1f1f1;
}

#status {
  text-align: center;
  min-height: 20px;
}

/* Themes */
.theme-light {
  background-color: #ffffff;
  color: #000000;
}

.theme-light .dropdown-btn {
  background-color: #d3b3aa;
  color: #000000;
}

.theme-dark {
  background-color: #121212;
  color: #ffffff;
}

.theme-dark .dropdown-btn {
  background-color: #4f4f4f;
  color: #ffffff;
}

.theme-ocean {
  background-color: #0077be;
  color: #e0f7fa;
}

.theme-ocean .dropdown-btn {
  background-color: #193e51;
  color: #ffffff;
}

.theme-nord {
  background-color: #2e3440;
  color: #d8dee9;
}

.theme-nord .dropdown-btn {
  background-color: #596680;
  color: #ffffff;
}
```

```js
const themes = [
  {
    name: "light",
    message: "Hello sunshine — Light theme is on!"
  },
  {
    name: "dark",
    message: "The night is yours — Dark theme is on!"
  },
  {
    name: "nord",
    message: "The frost has settled - Nord theme is on!"
  },
  {
    name: "ocean",
    message: "Blue skies and high tides — Ocean theme is on!"
  },
];

const setTheme = (theme) => {
  document.body.className = "";
  document.body.classList.add(`theme-${theme.name}`);
};

const updateStatus = (theme) => {
  const status = document.getElementById("status");
  status.textContent = theme.message;
};

document.addEventListener("DOMContentLoaded", () => {
  themes.forEach((theme) => {
    const button = document.getElementById(`theme-${theme.name}`);
    
    if (button) {
      button.addEventListener("click", () => {
        setTheme(theme);
        updateStatus(theme);
        closeMenu();
      });
    }
  });

  const toggleButton = document.getElementById("theme-switcher-button");
  const menu = document.getElementById("theme-dropdown");

  toggleButton.addEventListener("click", () => {
    const isExpanded = toggleButton.getAttribute("aria-expanded") === "true";
    if (isExpanded) {
      closeMenu();
    } else {
      openMenu();
    }
  });

  function closeMenu() {
    menu.setAttribute("hidden", "");
    toggleButton.setAttribute("aria-expanded", "false");
  }

  function openMenu() {
    menu.removeAttribute("hidden");
    toggleButton.setAttribute("aria-expanded", "true");
  }

  // Close the menu when clicking outside
  document.addEventListener("click", (e) => {
    if (!toggleButton.contains(e.target) && !menu.contains(e.target)) {
      closeMenu();
    }
  });
});
```
