For AI agents: the complete documentation index is available at https://mfeflow.github.io/mfeflow/llms.txt, the full documentation bundle is available at https://mfeflow.github.io/mfeflow/llms-full.txt, and this page is available as Markdown at https://mfeflow.github.io/mfeflow/features/theming.md.

theming

This page covers how to give your platform light and dark themes that all remotes follow, without each remote carrying theme code. The shell owns the choice, and remotes only read CSS variables.

What you get

The shell gets a provider, a theme picker, and a small script that applies the saved theme before the page paints. Remotes get a tokens object that points at the same variables. This feature is off by default.

Add it

pnpm mfeflow add theming

What changed in the shell and each remote:

apps/shell/src/theme/
├── themes.ts          # your light and dark tokens
├── Theme.tsx          # the provider that wraps the app
├── ThemeToggle.tsx    # the switch in the header
├── theme.css          # base variables
├── apply-stored.ts    # applies the saved theme at boot
└── head-script.ts     # the no-flash script for the head

apps/<remote>/src/theme/
└── tokens.ts          # CSS variable references for the remote

Three existing shell files are edited:

App.tsx            # wrapped in Theme
bootstrap.tsx      # gets a boot step
rsbuild.config.ts  # gets an import

If testing is installed, the stored theme, the toggle and the tokens get tests.

Use it

Define the tokens once in the shell. Each token becomes a --mf-<token> variable on the page's root element.

apps/shell/src/theme/themes.ts
export const themes = [
  defineTheme('light', {
    'color-bg': '#ffffff',
    'color-fg': '#111827',
    'color-primary': '#2563eb',
  }),
  defineTheme(
    'dark',
    { 'color-bg': '#0b1020', 'color-fg': '#e5e7eb', 'color-primary': '#60a5fa' },
    { colorScheme: 'dark' },
  ),
];

A remote then uses them in CSS:

.card {
  background: var(--mf-color-surface);
  color: var(--mf-color-fg);
}

Or in TypeScript, with fallbacks for when the remote runs on its own:

apps/cart/src/theme/tokens.ts
export const tokens = {
  bg: cssVar('color-bg', '#ffffff'),
  fg: cssVar('color-fg', '#111827'),
  surface: cssVar('color-surface', '#f3f4f6'),
  primary: cssVar('color-primary', '#2563eb'),
};

Switching themes rewrites the variables on the root element, so every remote changes at once with no re-render and no event passed between them.

Configure it

There are no install options. Everything is in themes.ts, which is yours to edit:

SettingDefaultWhat it does
themeslight, darkThe list of themes and their tokens; add tokens or themes freely
defaultThemelightThe theme used when nothing is stored and the system does not say dark
followSystemtrueUse the system dark-mode setting when no choice is stored

How it works

The choice is saved in the shell's storage under shell:theme. Two things apply it on load. A script generated by noFlashScript sits first in the shell's head, so the stored colours are on the page before any bundle loads. Then apply-stored.ts runs as the first boot step and does the same before the first component renders.

Both use the same order: a stored name that matches one of your themes, then the system dark preference if followSystem is on, then defaultTheme.

Gotchas

  • If your Content Security Policy blocks inline scripts, the head script is skipped. The boot step still applies the theme before the first render, but the page shows default colours until JavaScript has run.
  • A stored value that is not one of your themes, such as constructor, is ignored.
  • Token and theme names are lowercase letters, digits and dashes. Anything else throws when the theme is defined.

Reference