Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/6716.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added `default_color_mode` to `rx.Config` (`"system"`, `"light"`, or `"dark"`, also settable via `REFLEX_DEFAULT_COLOR_MODE`), so apps can set the initial color mode — and use the built-in color mode switcher and `rx.color_mode_cond` — without pulling in the large Radix themes CSS. The value drives both the compiled `ThemeProvider` default and the pre-hydration preload script, so there is no flash of the wrong theme on first paint. An explicit `rx.theme(appearance=...)` still takes precedence.
1 change: 1 addition & 0 deletions packages/reflex-base/news/6716.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added `default_color_mode` to `rx.Config` (`"system"`, `"light"`, or `"dark"`, also settable via `REFLEX_DEFAULT_COLOR_MODE`) and moved the shared `LiteralColorMode` type and color-mode string constants into `reflex_base.constants`. This lets apps set the initial color mode without depending on the Radix themes appearance prop.
Original file line number Diff line number Diff line change
@@ -1,24 +1,8 @@
import { useTheme } from "$/utils/react-theme";
import { createElement, useEffect } from "react";
import { ColorModeContext, defaultColorMode } from "$/utils/context";
import { useEffect } from "react";

export default function RadixThemesColorModeProvider({ children }) {
const { theme, resolvedTheme, setTheme } = useTheme();

const toggleColorMode = () => {
setTheme(resolvedTheme === "light" ? "dark" : "light");
};

const setColorMode = (mode) => {
const allowedModes = ["light", "dark", "system"];
if (!allowedModes.includes(mode)) {
console.error(
`Invalid color mode "${mode}". Defaulting to "${defaultColorMode}".`,
);
mode = defaultColorMode;
}
setTheme(mode);
};
const { resolvedTheme } = useTheme();

useEffect(() => {
const radixRoot = document.querySelector(
Expand All @@ -30,16 +14,5 @@ export default function RadixThemesColorModeProvider({ children }) {
}
}, [resolvedTheme]);

return createElement(
ColorModeContext.Provider,
{
value: {
rawColorMode: theme,
resolvedColorMode: resolvedTheme,
toggleColorMode,
setColorMode,
},
},
children,
);
return children;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {
useMemo,
} from "react";

import { isDevMode, defaultColorMode } from "$/utils/context";
import { isDevMode, defaultColorMode, ColorModeContext } from "$/utils/context";

const allowedModes = ["light", "dark", "system"];

const ThemeContext = createContext({
theme: defaultColorMode,
Expand All @@ -23,6 +25,25 @@ export function ThemeProvider({ children, defaultTheme = "system" }) {
);
const [isInitialized, setIsInitialized] = useState(false);

const setColorMode = (mode) => {
if (!allowedModes.includes(mode)) {
console.error(
`Invalid color mode "${mode}". Defaulting to "${defaultColorMode}".`,
);
mode = defaultColorMode;
}
setTheme(mode);
};

const resolvedTheme = useMemo(
() => (theme === "system" ? systemTheme : theme),
[theme, systemTheme],
);

const toggleColorMode = () => {
setColorMode(resolvedTheme === "light" ? "dark" : "light");
};

const firstRender = useRef(true);

useEffect(() => {
Expand All @@ -36,23 +57,20 @@ export function ThemeProvider({ children, defaultTheme = "system" }) {
const lastCompiledTheme = localStorage.getItem("last_compiled_theme");
if (lastCompiledTheme !== defaultColorMode) {
// on app startup, make sure the application color mode is persisted correctly.
setColorMode(defaultColorMode);
localStorage.setItem("last_compiled_theme", defaultColorMode);
localStorage.setItem("theme", defaultColorMode);
setIsInitialized(true);
return;
}
}

// Load saved theme from localStorage
const savedTheme = localStorage.getItem("theme") || defaultTheme;
setTheme(savedTheme);
setColorMode(savedTheme);
setIsInitialized(true);
});

const resolvedTheme = useMemo(
() => (theme === "system" ? systemTheme : theme),
[theme, systemTheme],
);

useEffect(() => {
// Set up media query for system preference detection
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
Expand Down Expand Up @@ -90,7 +108,18 @@ export function ThemeProvider({ children, defaultTheme = "system" }) {
return createElement(
ThemeContext.Provider,
{ value: { theme, resolvedTheme, setTheme } },
children,
createElement(
ColorModeContext.Provider,
{
value: {
rawColorMode: theme,
resolvedColorMode: resolvedTheme,
toggleColorMode,
setColorMode,
},
},
children,
),
);
}

Expand Down
13 changes: 12 additions & 1 deletion packages/reflex-base/src/reflex_base/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from typing import TYPE_CHECKING, Annotated, Any, ClassVar, Literal

from reflex_base import constants
from reflex_base.constants.base import LogLevel
from reflex_base.constants.base import LiteralColorMode, LogLevel
from reflex_base.environment import EnvironmentVariables as EnvironmentVariables
from reflex_base.environment import EnvVar as EnvVar
from reflex_base.environment import (
Expand Down Expand Up @@ -177,6 +177,7 @@ class BaseConfig:
redis_token_expiration: Token expiration time for redis state manager.
env_file: Path to file containing key-values pairs to override in the environment; Dotenv format.
state_auto_setters: Whether to automatically create setters for state base vars.
default_color_mode: The default color mode for the app: "system" (follow the OS preference), "light", or "dark". Applies to the built-in color mode switcher and `color_mode_cond` without requiring a radix theme.
show_built_with_reflex: Whether to display the sticky "Built with Reflex" badge on all pages.
is_reflex_cloud: Whether the app is running in the reflex cloud environment.
extra_overlay_function: Extra overlay function to run after the app is built. Formatted such that `from path_0.path_1... import path[-1]`, and calling it with no arguments would work. For example, "reflex_components_moment.moment".
Expand Down Expand Up @@ -251,6 +252,8 @@ class BaseConfig:

state_auto_setters: bool = False

default_color_mode: LiteralColorMode = "system"

show_built_with_reflex: bool | None = None

is_reflex_cloud: bool = False
Expand Down Expand Up @@ -409,6 +412,14 @@ def _post_init(self, **kwargs):
msg = f"{self._prefixes[0]}REDIS_URL is required when using the redis state manager."
raise ConfigError(msg)

allowed_color_modes = constants.LiteralColorMode.__args__
if self.default_color_mode not in allowed_color_modes:
msg = (
f"default_color_mode must be one of "
f"{allowed_color_modes}, but got {self.default_color_mode!r}."
)
raise ConfigError(msg)

def _normalize_plugins(self):
"""Normalize ``plugins`` entries to Plugin instances.

Expand Down
8 changes: 8 additions & 0 deletions packages/reflex-base/src/reflex_base/constants/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,22 @@
from .base import (
APP_HARNESS_FLAG,
COOKIES,
DARK_COLOR_MODE,
IS_LINUX,
IS_MACOS,
IS_WINDOWS,
LIGHT_COLOR_MODE,
LOCAL_STORAGE,
POLLING_MAX_HTTP_BUFFER_SIZE,
PYTEST_CURRENT_TEST,
REFLEX_VAR_CLOSING_TAG,
REFLEX_VAR_OPENING_TAG,
SESSION_STORAGE,
SYSTEM_COLOR_MODE,
ColorMode,
Dirs,
Env,
LiteralColorMode,
LogLevel,
Ping,
ReactRouter,
Expand Down Expand Up @@ -68,9 +72,11 @@
"ALEMBIC_CONFIG",
"APP_HARNESS_FLAG",
"COOKIES",
"DARK_COLOR_MODE",
"IS_LINUX",
"IS_MACOS",
"IS_WINDOWS",
"LIGHT_COLOR_MODE",
"LOCAL_STORAGE",
"NOCOMPILE_FILE",
"POLLING_MAX_HTTP_BUFFER_SIZE",
Expand All @@ -83,6 +89,7 @@
"ROUTE_NOT_FOUND",
"SESSION_STORAGE",
"SETTER_PREFIX",
"SYSTEM_COLOR_MODE",
"AgentsMd",
"Bun",
"ColorMode",
Expand All @@ -103,6 +110,7 @@
"GitIgnore",
"Hooks",
"Imports",
"LiteralColorMode",
"LogLevel",
"MemoizationDisposition",
"MemoizationMode",
Expand Down
6 changes: 6 additions & 0 deletions packages/reflex-base/src/reflex_base/constants/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,12 @@ class ReactRouter(Javascript):


# Color mode variables
SYSTEM_COLOR_MODE: str = "system"
LIGHT_COLOR_MODE: str = "light"
DARK_COLOR_MODE: str = "dark"
LiteralColorMode = Literal["system", "light", "dark"]


class ColorMode(SimpleNamespace):
"""Constants related to ColorMode."""

Expand Down
11 changes: 5 additions & 6 deletions packages/reflex-base/src/reflex_base/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
from __future__ import annotations

from collections.abc import Mapping
from typing import Any, Literal
from typing import Any

from reflex_base import constants
from reflex_base.breakpoints import Breakpoints, breakpoints_values
from reflex_base.constants.base import DARK_COLOR_MODE as DARK_COLOR_MODE
from reflex_base.constants.base import LIGHT_COLOR_MODE as LIGHT_COLOR_MODE
from reflex_base.constants.base import SYSTEM_COLOR_MODE as SYSTEM_COLOR_MODE
from reflex_base.constants.base import LiteralColorMode as LiteralColorMode
from reflex_base.event import EventChain, EventHandler, EventSpec, run_script
from reflex_base.utils import format
from reflex_base.utils.exceptions import ReflexError
Expand All @@ -17,11 +21,6 @@
from reflex_base.vars.function import FunctionVar
from reflex_base.vars.object import ObjectVar

SYSTEM_COLOR_MODE: str = "system"
LIGHT_COLOR_MODE: str = "light"
DARK_COLOR_MODE: str = "dark"
LiteralColorMode = Literal["system", "light", "dark"]

# Reference the global ColorModeContext
color_mode_imports = {
f"$/{constants.Dirs.CONTEXTS_PATH}": [ImportVar(tag="ColorModeContext")],
Expand Down
45 changes: 38 additions & 7 deletions reflex/compiler/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@
from reflex_base.constants.state import FIELD_MARKER
from reflex_base.environment import environment
from reflex_base.plugins import CompileContext, CompilerHooks, PageContext, Plugin
from reflex_base.style import SYSTEM_COLOR_MODE
from reflex_base.utils import memo_paths
from reflex_base.utils.exceptions import ReflexError
from reflex_base.utils.format import to_title_case
from reflex_base.utils.imports import ABSOLUTE_IMPORT_PREFIXES, ImportVar
from reflex_base.vars.base import LiteralVar, Var
from reflex_base.vars.sequence import LiteralStringVar
from reflex_components_core.base.app_wrap import AppWrap
from reflex_components_core.base.fragment import Fragment
from reflex_components_radix.plugin import RadixThemesPlugin
Expand Down Expand Up @@ -176,6 +176,30 @@ def _compile_theme(theme: str) -> str:
return templates.theme_template(theme=theme)


def _resolve_default_color_mode(theme: Component | None) -> str:
"""Resolve the app's compile-time default color mode.

An explicit theme appearance ("light"/"dark") takes precedence over the
``Config.default_color_mode`` option; "inherit", no appearance, or a
non-literal appearance Var falls back to the config value.

Args:
theme: The top-level app theme, if any.

Returns:
One of "system", "light", or "dark".
"""
appearance = getattr(theme, "appearance", None)
if appearance is not None:
appearance_var = LiteralVar.create(appearance)
if (
isinstance(appearance_var, LiteralStringVar)
and appearance_var._var_value != "inherit"
):
return appearance_var._var_value
return get_config().default_color_mode


def _compile_contexts(state: type[BaseState] | None, theme: Component | None) -> str:
"""Compile the initial state and contexts.

Expand All @@ -186,22 +210,20 @@ def _compile_contexts(state: type[BaseState] | None, theme: Component | None) ->
Returns:
The compiled context file.
"""
appearance = getattr(theme, "appearance", None)
if appearance is None or str(LiteralVar.create(appearance)) == '"inherit"':
appearance = LiteralVar.create(SYSTEM_COLOR_MODE)
default_color_mode = str(LiteralVar.create(_resolve_default_color_mode(theme)))

return (
templates.context_template(
initial_state=utils.compile_state(state),
state_name=state.get_name(),
client_storage=utils.compile_client_storage(state),
is_dev_mode=not is_prod_mode(),
default_color_mode=str(appearance),
default_color_mode=default_color_mode,
)
if state
else templates.context_template(
is_dev_mode=not is_prod_mode(),
default_color_mode=str(appearance),
default_color_mode=default_color_mode,
)
)

Expand Down Expand Up @@ -595,13 +617,16 @@ def compile_document_root(
head_components: list[Component],
html_lang: str | None = None,
html_custom_attrs: dict[str, Var | Any] | None = None,
default_color_mode: str = "system",
) -> tuple[str, str]:
"""Compile the document root.

Args:
head_components: The components to include in the head.
html_lang: The language of the document, will be added to the html root element.
html_custom_attrs: custom attributes added to the html root element.
default_color_mode: The color mode applied before hydration when no theme
is saved in the browser.

Returns:
The path and code of the compiled document root.
Expand All @@ -613,7 +638,10 @@ def compile_document_root(

# Create the document root.
document_root = utils.create_document_root(
head_components, html_lang=html_lang, html_custom_attrs=html_custom_attrs
head_components,
html_lang=html_lang,
html_custom_attrs=html_custom_attrs,
default_color_mode=default_color_mode,
)

# Compile the document root.
Expand Down Expand Up @@ -1269,6 +1297,9 @@ def compile_app(
if app.html_custom_attrs
else {"suppressHydrationWarning": True}
),
default_color_mode=_resolve_default_color_mode(
radix_themes_plugin.get_theme()
),
)
)
progress.advance(task)
Expand Down
Loading
Loading