diff --git a/frontend/editor/src/core/components/shared/RainbowThemeProvider.tsx b/frontend/editor/src/core/components/shared/RainbowThemeProvider.tsx
deleted file mode 100644
index 0e6ba64a2d..0000000000
--- a/frontend/editor/src/core/components/shared/RainbowThemeProvider.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { createContext, useContext, useEffect, ReactNode } from "react";
-import { MantineProvider } from "@mantine/core";
-import { useRainbowTheme } from "@app/hooks/useRainbowTheme";
-import { mantineTheme } from "@app/theme/mantineTheme";
-import rainbowStyles from "@app/styles/rainbow.module.css";
-import { ToastProvider } from "@app/components/toast";
-import ToastRenderer from "@app/components/toast/ToastRenderer";
-import { ToastPortalBinder } from "@app/components/toast";
-import type { ThemeMode } from "@app/constants/theme";
-// SUI shared design-system tokens (used by @shared/components). Additive — its
-// var names don't clash with the editor's own theme.css. The effect below
-// bridges Mantine's color scheme to the `data-theme` attribute SUI keys on.
-import "@shared/tokens/tokens.css";
-
-interface RainbowThemeContextType {
- themeMode: ThemeMode;
- isRainbowMode: boolean;
- isToggleDisabled: boolean;
- toggleTheme: () => void;
- activateRainbow: () => void;
- deactivateRainbow: () => void;
-}
-
-const RainbowThemeContext = createContext
(null);
-
-export function useRainbowThemeContext() {
- const context = useContext(RainbowThemeContext);
- if (!context) {
- throw new Error(
- "useRainbowThemeContext must be used within RainbowThemeProvider",
- );
- }
- return context;
-}
-
-interface RainbowThemeProviderProps {
- children: ReactNode;
-}
-
-export function RainbowThemeProvider({ children }: RainbowThemeProviderProps) {
- const rainbowTheme = useRainbowTheme();
-
- // Determine the Mantine color scheme
- const mantineColorScheme =
- rainbowTheme.themeMode === "rainbow" ? "dark" : rainbowTheme.themeMode;
-
- // Bridge the resolved scheme to the `data-theme` attribute SUI's tokens.css
- // keys its dark palette on (the editor otherwise only sets Mantine's
- // data-mantine-color-scheme), so @shared/components theme correctly.
- useEffect(() => {
- document.documentElement.setAttribute(
- "data-theme",
- mantineColorScheme === "dark" ? "dark" : "light",
- );
- }, [mantineColorScheme]);
-
- return (
-
-
-
-
-
- {children}
-
-
-
-
-
- );
-}
diff --git a/frontend/editor/src/core/components/shared/ThemeProvider.tsx b/frontend/editor/src/core/components/shared/ThemeProvider.tsx
new file mode 100644
index 0000000000..2dda9b8d39
--- /dev/null
+++ b/frontend/editor/src/core/components/shared/ThemeProvider.tsx
@@ -0,0 +1,101 @@
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useMemo,
+ useState,
+ ReactNode,
+} from "react";
+import { MantineProvider } from "@mantine/core";
+import { useIsomorphicEffect } from "@mantine/hooks";
+import { usePreferences } from "@app/contexts/PreferencesContext";
+import { mantineTheme } from "@app/theme/mantineTheme";
+import { ToastProvider } from "@app/components/toast";
+import ToastRenderer from "@app/components/toast/ToastRenderer";
+import { ToastPortalBinder } from "@app/components/toast";
+import {
+ type ThemeMode,
+ getSystemTheme,
+ resolveColorScheme,
+} from "@app/constants/theme";
+// SUI shared design-system tokens (used by @shared/components); key on `data-theme`.
+import "@shared/tokens/tokens.css";
+
+interface ThemeContextType {
+ themeMode: ThemeMode;
+ setTheme: (mode: ThemeMode) => void;
+}
+
+const ThemeContext = createContext(null);
+
+export function useTheme() {
+ const context = useContext(ThemeContext);
+ if (!context) {
+ throw new Error("useTheme must be used within ThemeProvider");
+ }
+ return context;
+}
+
+interface ThemeProviderProps {
+ children: ReactNode;
+}
+
+export function ThemeProvider({ children }: ThemeProviderProps) {
+ // preferences.theme is the single source of truth.
+ const { preferences, updatePreference } = usePreferences();
+ const themeMode = preferences.theme;
+ const setTheme = useCallback(
+ (mode: ThemeMode) => updatePreference("theme", mode),
+ [updatePreference],
+ );
+
+ // Track the OS scheme so "system" updates live; only subscribe while on system.
+ // If matchMedia is unavailable we bail out and keep the seeded value, which
+ // getSystemTheme already defaults to light — so it never crashes.
+ const [systemScheme, setSystemScheme] = useState(getSystemTheme);
+ useIsomorphicEffect(() => {
+ if (themeMode !== "system") return;
+ if (
+ typeof window === "undefined" ||
+ typeof window.matchMedia !== "function"
+ ) {
+ return;
+ }
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
+ const update = () => setSystemScheme(media.matches ? "dark" : "light");
+ update();
+ media.addEventListener("change", update);
+ return () => media.removeEventListener("change", update);
+ }, [themeMode]);
+
+ // The theme preference resolved to a concrete light/dark scheme.
+ const colorScheme = resolveColorScheme(themeMode, systemScheme);
+
+ // Mantine drives `data-mantine-color-scheme`; mirror it to `data-theme` for SUI tokens.
+ useIsomorphicEffect(() => {
+ document.documentElement.setAttribute("data-theme", colorScheme);
+ }, [colorScheme]);
+
+ const value = useMemo(
+ () => ({ themeMode, setTheme }),
+ [themeMode, setTheme],
+ );
+
+ return (
+
+
+
+
+
+ {children}
+
+
+
+
+
+ );
+}
diff --git a/frontend/editor/src/core/components/shared/TopControls.tsx b/frontend/editor/src/core/components/shared/TopControls.tsx
index 573ee65afb..8fc61183a8 100644
--- a/frontend/editor/src/core/components/shared/TopControls.tsx
+++ b/frontend/editor/src/core/components/shared/TopControls.tsx
@@ -1,7 +1,5 @@
import React, { useState, useCallback, useMemo } from "react";
import { SegmentedControl, Loader } from "@mantine/core";
-import { useRainbowThemeContext } from "@app/components/shared/RainbowThemeProvider";
-import rainbowStyles from "@app/styles/rainbow.module.css";
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
import GridViewIcon from "@mui/icons-material/GridView";
import FolderIcon from "@mui/icons-material/Folder";
@@ -148,7 +146,6 @@ const TopControls = ({
onFileSelect,
onFileRemove,
}: TopControlsProps) => {
- const { isRainbowMode } = useRainbowThemeContext();
const [switchingTo, setSwitchingTo] = useState(null);
const pageEditorState = usePageEditorDropdownState();
@@ -213,7 +210,6 @@ const TopControls = ({
onChange={handleViewChange}
color="blue"
fullWidth
- className={isRainbowMode ? rainbowStyles.rainbowSegmentedControl : ""}
style={{
transition: "all 0.2s ease",
opacity: switchingTo ? 0.8 : 1,
diff --git a/frontend/editor/src/core/components/shared/config/configSections/GeneralSection.tsx b/frontend/editor/src/core/components/shared/config/configSections/GeneralSection.tsx
index f6343f5ec5..d3383df0c7 100644
--- a/frontend/editor/src/core/components/shared/config/configSections/GeneralSection.tsx
+++ b/frontend/editor/src/core/components/shared/config/configSections/GeneralSection.tsx
@@ -19,8 +19,9 @@ import {
import { useTranslation } from "react-i18next";
import { usePreferences } from "@app/contexts/PreferencesContext";
import { useAppConfig } from "@app/contexts/AppConfigContext";
-import { useRainbowThemeContext } from "@app/components/shared/RainbowThemeProvider";
+import { useTheme } from "@app/components/shared/ThemeProvider";
import LanguageSelector from "@app/components/shared/LanguageSelector";
+import type { ThemeMode } from "@app/constants/theme";
import type { ToolPanelMode } from "@app/constants/toolPanel";
import type {
StartupView,
@@ -85,7 +86,7 @@ const GeneralSection: React.FC = ({
const { t } = useTranslation();
const { preferences, updatePreference } = usePreferences();
const { config } = useAppConfig();
- const { toggleTheme, themeMode } = useRainbowThemeContext();
+ const { setTheme, themeMode } = useTheme();
const [fileLimitInput, setFileLimitInput] = useState(
preferences.autoUnzipFileLimit,
);
@@ -499,15 +500,13 @@ const GeneralSection: React.FC = ({
{t(
"settings.general.themeDescription",
- "Switch between light and dark mode",
+ "Choose light, dark, or follow your system",
)}