Where does the theme toggle (.toggle-switch) in MFE (Learner Dashboard) come from?

Hi everyone,

I am working with Open edX Sumac 3 and noticed a difference in how the theme toggle button is implemented between the classic LMS (Indigo theme) and the MFEs.

  • In the LMS (Tutor Indigo), the toggle appears inside a <div id="toggle-switch">…</div>.

  • In the Learner Dashboard MFE (e.g. https://apps.edx.io/learner-dashboard/), the theme toggle appears inside a <div class="toggle-switch">…</div>.

I have already checked both openedx/frontend-component-header and edx/frontend-component-header-edx, but neither of these repositories includes a ThemeToggle or ToggleSwitch component.

So my questions are:

  1. Which repository actually contains the code for the .toggle-switch button in the MFEs?

  2. Is this implemented as part of the core Open edX MFEs, or is it usually injected via the Frontend Plugin Framework (slots/branding overrides)?

  3. If it’s a plugin, could anyone point me to an example repo or implementation pattern that adds the theme toggle in MFEs?

Thanks in advance for any pointers!

Hi @lexuandaibn123
If you are referring specifically to the Dark Mode theme toggle, then as far as I’m aware this particular toggle is exclusive to the Indigo theme: Code search results · GitHub

I can find some reference to a darkmode in frontend platform which might be helpful, but I have not directly used this myself so can’t provide any real-world feedback on it’s implementation, though the docs do appear fairly comprehensive at least…

1 Like

Hi @lexuandaibn123 and welcome. Do you need more assistance with this issue?

I’m still looking into this and could use a pointer.

While debugging, I found the MFE theme toggle is actually rendered by a component named ThemeToggleButton. Here’s the exact snippet I’m seeing at runtime:

import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import Cookies from 'universal-cookie';
import { Icon } from '@openedx/paragon';
import { WbSunny, Nightlight } from '@openedx/paragon/icons';

const themeCookie = 'indigo-toggle-dark';
const themeCookieExpiry = 90; // days

const ThemeToggleButton = () => {
  const cookies = new Cookies();
  const isThemeToggleEnabled = getConfig().INDIGO_ENABLE_DARK_TOGGLE;

  const getCookieExpiry = () => {
    const today = new Date();
    return new Date(today.getFullYear(), today.getMonth(), today.getDate() + themeCookieExpiry);
  };

  const getCookieOptions = (serverURL) => ({ domain: serverURL.hostname, path: '/', expires: getCookieExpiry() });

  const addDarkThemeToIframes = () => {
    const iframes = document.getElementsByTagName('iframe');
    const iframesLength = iframes.length;
    if (iframesLength > 0) {
      Array.from({ length: iframesLength }).forEach((_, ind) => {
        const style = document.createElement('style');
        style.textContent = `
          body{
            background-color: #0D0D0E;
            color: #ccc;
          }
          a {color: #ccc;}
          a:hover{color: #d3d3d3;}
          `;
        if (iframes[ind].contentDocument) { iframes[ind].contentDocument.head.appendChild(style); }
      });
    }
  };

  const removeDarkThemeFromiframes = () => {
    const iframes = document.getElementsByTagName('iframe');
    const iframesLength = iframes.length;

    Array.from({ length: iframesLength }).forEach((_, ind) => {
      if (iframes[ind].contentDocument) {
        const iframeHead = iframes[ind].contentDocument.head;
        const styleTag = Array.from(iframeHead.querySelectorAll('style')).find(
          (style) => style.textContent.includes('background-color: #0D0D0E;') && style.textContent.includes('color: #ccc;'),
        );
        if (styleTag) {
          styleTag.remove();
        }
      }
    });
  };

  const onToggleTheme = () => {
    const serverURL = new URL(getConfig().LMS_BASE_URL);
    let theme = '';

    if (cookies.get(themeCookie) === 'dark') {
      document.body.classList.remove('indigo-dark-theme');
      removeDarkThemeFromiframes();
      theme = 'light';
    } else {
      document.body.classList.add('indigo-dark-theme');
      addDarkThemeToIframes();
      theme = 'dark';
    }
    cookies.set(themeCookie, theme, getCookieOptions(serverURL));

    const learningMFEUnitIframe = document.getElementById('unit-iframe');
    if (learningMFEUnitIframe) {
      learningMFEUnitIframe.contentWindow.postMessage({ 'indigo-toggle-dark': theme }, serverURL.origin);
    }
  };

  if (!isThemeToggleEnabled) {
    return <div />;
  }

  return (
    <div className="theme-toggle-button">
      <div className="light-theme-icon"><Icon src={WbSunny} /></div>
      <div className="toggle-switch">
        <label htmlFor="theme-toggle-checkbox" className="switch">
          <input id="theme-toggle-checkbox" defaultChecked={cookies.get(themeCookie) === 'dark'} onChange={onToggleTheme} type="checkbox" />
          <span className="slider round" />
        </label>
      </div>
      <div className="dark-theme-icon"><Icon src={Nightlight} /></div>
    </div>
  );
};

export default ThemeToggleButton;

What’s confusing is I can’t find this component (or anything similar) in either openedx/frontend-component-header or edx/frontend-component-header. It relies on INDIGO_ENABLE_DARK_TOGGLE and the indigo-toggle-dark cookie, which lines up with Tutor Indigo’s build-time config patch here:

I cloned and customized Tutor Indigo and rebuilt the MFEs, but the toggle’s behavior didn’t change—so it seems this button isn’t coming from the Tutor Indigo source directly. It looks more like it’s shipped via a prebuilt package or injected via the header/slots system.

@tutor-maintainers could someone assist with this Indigo question?

It is indeed coming from the patch you’re mentioning, and it goes to the env.config.jsx file at the root of each MFE.

Are you sure your custom indigo plugin is indeed being used?

Have you tried rebuilding the MFE image with --no-cache?