Themeservice system settings

would it be possible to add to the light and dark mode the "system theme setting"?
simply put, the user may have chosen a mode in his personalization setting, thus using that selection would be great and useful. the setting would be in css:
@media (prefers-color-scheme: dark){
body {
background-color: #21242b;
}
}
with only that code, the theme would change once the user selects the dark or light mode for his system.
obviously you can define other than "body" color setups (like color, cards, li,...)
this is just a small suggestion that will add to the user experience without additional burden.

Hi @Fady1956,

This can't be easily done as a built-in because dark and light themes are defined in separate files. We would have to put everything in one file in order for it work just with a CSS selector.

Having said that there are two options to implement what you are after:

Use a helper CSS file or style

This CSS would @import the required CSS file depending on the CSS media query. It would be included instead of using RadzenTheme and the `ThemeService.

To implement this solution replace <RadzenTheme /> in your App.razor with this:

<style>
 @@import url(@($"_content/Radzen.Blazor/css/material-base.css?v={typeof(Radzen.Colors).Assembly.GetName().Version}")) (prefers-color-scheme: light);
 @@import url(@($"_content/Radzen.Blazor/css/material-dark-base.css?v={typeof(Radzen.Colors).Assembly.GetName().Version}")) (prefers-color-scheme: dark);
</style>

This code snippet will import the required CSS file depending on the user system settings. Using @@ is required because Blazor treats @ as a special symbol which needs to be escaped. This ?v={typeof(Radzen.Colors).Assembly.GetName().Version} implements cache busting and prevents the browser from loading a stale theme file after upgrading Radzen.Blazor.

Pros
  • Easier to implement.
Cons
  • It overrides RadzenTheme and ThemeService which means RadzenAppearanceToggle won't work.

Use a "landing" page

This approach involves having a landing page (with route "/") which runs the CSS query with JavaScript interop and if dark mode is requested sets the current theme and then redirects to the app.

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        // Use JS interop to run the CSS media query
        var darkMode = await JSRuntime.InvokeAsync<bool>("eval", "window.matchMedia('(prefers-color-scheme: dark)').matches");

        if (darkMode)
        {
            // Set the current theme if needed
            ThemeService.SetTheme("material-dark");
        }

        // Redirect to the real start page of the application
        NavigationManager.NavigateTo("/index");
    }
}
Pros
  • Still relies on ThemeService which means RadzenApperanceToggle would work as expected as well as everything else that could required `ThemeService.
Cons
  • Requires a new page and a new empty layout.

I am attaching two apps that implement both approaches.

SystemThemeApp.zip (263.2 KB)
SystemThemeCss.zip (258.7 KB)

2 Likes

As usual, Thank you Korchev for your assistance which is always appreciated.
However, I was thinking of a simpler approach than what you propose so generously.
OK, I want to keep the ThemeService (which is a good thing), but my proposal was that the ThemeService would propose a third option, i.e. Light, Dark & System.
Since Light & Dark are already there, what remains is in case the user selects the third option (System) the ThemeService would only check what is the user selected option and just go on with selecting either one of the existing Light and Dark.
Actually, I am in favor of using the system selected option at first as the prefered user option, then save his selection for future use.
Anyway, if that is not possible, then it is not possible :slight_smile:
Again thank you for your cooperation.

We can't have a "System" option as we don't have such a version of the theme. We have light and dark only. The media query is still needed to load the correct CSS file. If RadzenAppearanceToggle does the check you would always see a flicker in the case the "wrong" CSS file is loaded first.

Korchev,

yes I got the limitations, but still, it would have been nice to have that user experience to be available.
I understand your comments perfectly, but one can dream ...

I believe second version of the example does exactly what you are asking for. The user starts with the "system" theme (dark or light) preset.

yes it might do the trick, but it will not switch when the user changes this option from the settings.
so it might do the trick initially, but not afterwards.
Anyway, as I said: it cannot be done, then let us leave it at that.

The second demo does allow the user to change the appearance from RadzenAppearanceToggle - the user can change the theme even after it has been set. Both demos would reflect a system-wide change if the user refreshes their browser. Did you try any of them?

As I have already said before we can support this behavior only if we merge the light and dark CSS files into one. We are reluctant to this for various reasons:

  1. It is a breaking change if we decide to delete the "dark" version.
  2. It would almost double the theme size all users have to download as they will always download the light and dark version regardless which version they use.
  3. If we decide to keep the existing files would be a lot more effort for us to support.

The solutions I have suggested do not have any of those drawbacks.

korchev,
yes I tried your provided solutions.
what I am saying is that if the user change his selection in the system personalization (not through the app) it is not reflected automatically, while what I provided in my css will reflect automatically. try it and you will understand what I am saying. just use a normal html page adding to it my css and see what happens immediately if you switch in the system (doing nothing in the app) the change is immediately effected.

Yes, it will not change "automatically". I have explained at length why we won't implement this feature in my previous reply. Nothing more to add really. Doubling the CSS payload or our support effort isn't worth the trade off.

yes as I already said: I agree with your comment.

  1. I disagree with all these points. I propose the following, which addresses all three concerns:
  • ThemeService would stay the same: allowing switching between "material," "fluent," and "default" themes.

  • Each theme should support both "dark" and "light" variants. We can keep the "Material dark" theme, which simply doesn't support the "light" variant and won't switch to light based on system settings. => No breaking changes.

  • For a "unified theme" supporting both "dark" and "light" variants, the size won't double compared to other themes because most elements are the same. Only a small portion of theme-related color variables would be duplicated. Non-color elements (typography, padding, borders) are typically the same between variants.

  • Merging dark and light themes this way would result in less code, cleaner implementation, easier maintenance, and most importantly, compliance with web standards. For example today, there exist some inconsistency between "Material (Light)" and "Material Dark": the --rz-alert-icon-margin is 0 in the Light theme but 0.125rem in the Dark theme. Most likely there was some fix on the Light theme which is not port to the Dark theme. Merging the dark and light theme would prevent these kind of inconsistency and make the maintenance easier.

  • Switching theme cause page reloading, lost scrolling position, current element focus.. it is acceptable when Switching from Material to Fluent or a completely different theme. But not acceptable when Switching between different color palettes (dark, light).

  • What I said also apply for Text-direction (LTR, RTL).. As long as the theme use logical properties (inline, block, start, end) instead of physical properties (x, y, left/top, right/bottom) then switching Text-direction usually won't need page reloading, or any communication with the server.

  1. The "unified themes" should not only respond to the system-wide @media (prefers-color-scheme) but also respect the data-theme attribute of the parent element.
/* Default value */
:root {
    color-scheme: light;
    --rz-text-color: var(--rz-base-800); /*dim text color*/
}

/* 3. Prefers-color-scheme (lowest precedence) */
@media (prefers-color-scheme: dark) {
    :root {
        color-scheme: dark;
        --rz-text-color: var(--rz-base-50); /*bright text color*/
    }
}

/* 2. Data-theme attribute (higher precedence) */
[data-theme="light"] {
    color-scheme: light;
    --rz-text-color: var(--rz-base-800); /*dim text color*/
}

[data-theme="dark"] {
    color-scheme: dark;
    --rz-text-color: var(--rz-base-50); /*bright text color*/
}
  1. Looked at the codebases, I think that my idea is feasible, but:
  • It would require significant refactoring of the SCSS code.
  • It would also impact the theme editor in "Radzen Studio" to support defining colors for dark/light variants.

To prove that the "unified theme" size won't double, I created this radzen-material.css.

It is also a simpler solution for the original question. I think that it is better solution than all the above "workarounds". Here how it works:

my radzen-material.css converts the "Material" (or "Material Dark") theme into a "unified theme". You can use it as follows:

<head>
    <RadzenTheme Theme="material" @rendermode="InteractiveAuto" />  
    <link rel="stylesheet" href="@Assets['radzen-material.css']"/>
    ...
</head>

With this, the "Material" theme becomes reactive to both prefers-color-scheme (system-wide) and the data-theme attribute.

Using the data-theme attribute, you can even mix dark and light theme variants:

<div class="rz-p-12" data-theme="dark">  
    <RadzenButton>This button is always Dark</RadzenButton>  
</div>  
<div class="rz-p-12" data-theme="light">  
    <RadzenButton>This button is always Light</RadzenButton>  
</div>

Why is this significant?

The "Material" theme can support both "Dark" and "Light" variants by combining the base theme with my radzen-material.css (554 Bytes, not minified, not zipped).

  • The current Material-base.css (Light variant only) is 651.5 KB.
  • The Material Unified theme (supporting both Light and Dark variants) is approximately 651.5 KB + 554 B => only a 0.08% size increase, not a 200% increase.

Hi @Phu_Hiep_DUONG,

Thanks for your feedback and proof of concept work.

How would this work with the existing functionality that changes themes via ThemeService and RadzenAppearanceToggle? Right now RadzenAppearanceToggle changes the theme via the ThemeService. RadzenTheme listens for changes and includes the corresponding CSS file.

We should distinguish between "Theme Toggle" ("Material," "Fluent," "Humanistic," etc.) and "Color-Scheme Toggle" (Dark, Light). Currently, Radzen combines them. My proposal separates these two.

  • A "Theme Toggle" may cause page reloading, losing scroll positions or focus.
    • => Radzen's ThemeService should only handle "Theme Toggle" (or ThemePicker, ThemeChooser, etc.).
  • A "Color-Scheme Toggle" should be entirely client-side.
    • => The RadzenAppearanceToggle falls under "Color-Scheme Toggle" and should not use ThemeService.

Here's an implementation of RadzenAppearanceToggle, renamed ColorSchemeToggle to clarify the distinction:

  • For simplicity, I used an existing Web Component toggle.
    • My proposal follows Web Standards and best practices, so any similar Web Component should work.
    • Thanks to this Web Component, this ColorSchemeToggle supports "Dark," "Light," and "Auto" ("Auto" = "Dark" or "Light" based on system settings).
  • This demonstrates the concept; we can later "Blazorify" it into a proper Radzen component.
<HeadContent>
  <script src="https://unpkg.com/@@mahozad/theme-switch"></script>
</HeadContent>


ColorSchemeToggle.razor
------------------------
<theme-switch class="w-[24px] h-[24px]"></theme-switch>


ColorSchemeToggle.razor.css
---------------------------
theme-switch {  
    --theme-switch-icon-color: var(--rz-text-color);  
}


Then use it somewhere
-----------------
<ColorSchemeToggle />

I see a breaking change:

People would suddenly start seeing a different theme than the one they have selected (the one specified by their end user's system settings). How can one say "my app should always use a dark theme regardless of the user's OS settings"? This is currently supported and we don't want to lose it. The CSS from your gist would apply the light theme (which is what the default is) and be dark only if prefers-color-scheme matches. No existing Radzen.Blazor user would know about the data-theme attribute.

The file from your gist is not 554B - it is 43KB. Also doubling in size is not 200% increase but 100% :slight_smile:
I have exaggerated the change in size which is a mistake. I completely forgot that the themes use CSS variables now. The main issue is that using a specific theme for all users would now require a new custom attribute (data-theme="dark") unless I am missing something from your implementation.

There are no breaking changes, because my proposal doesn't remove anything existed. I suggest to add:

  • A new theme called "Unified Material"
  • A new component called ColorSchemeToggle.
  • Everything else remains unchanged.

For users adopting the "Unified Material" theme, the existing RadzenAppearanceToggle won't work; they must use ColorSchemeToggle instead.

Pros and cons of "Unified Material" vs. existing "Material (Light)" and "Material Dark" themes:

  • Cons:
    • The CSS for "Unified Material" is larger than the two existing themes.
      • Note: Thanks for correcting my incorrect estimation. The combined size of the two files was a worst-case approximation. Properly merging them should result in a smaller total size. After minification and gzip, the size difference might become insignificant on production.
  • Pros:
    • Switching between Dark/Light with ColorSchemeToggle is smoother (no page reloads, lost focus, or scroll position).
    • Supports automatic Dark/Light theme changing following system settings.
    • Supports forcing Dark/Light themes across the entire application or specific page sections (e.g., some parts can be Dark, others Light, or follow system settings).
    • Dark/Light theme switching can be fully client-side, with optional server-side management using Blazor reactivity.

The ColorSchemeToggle component uses the data-theme attribute to override OS settings:

<html data-theme="dark">

or

<html data-theme="light">

Initial data-theme values can be set from cookies or local storage (on page loading); if unset, it defaults to OS settings.

Users don’t need to know about this data-theme attribute unless they want to force specific page sections to a different scheme, e.g., <div data-theme="light">.

Maintenance Consideration

Maintaining all three themes ("Unified Material," "Material (Light)," and "Material Dark") could be a burden unless we refactor the SCSS code to separate color management. By sharing most of the code and differentiating only color variables, I think that maintainability would improve after the refactor.

Hopefully, everybody might migrate to "Unified Material" and we can decommission "RadzenAppearanceToggle", "Material (Light)" and (Material Dark)

Adding a new theme is less than ideal:

  • More themes to maintain.
  • Hard to explain why such a theme exists.

This is also hard to explain. ColorSchemeToggle is identical in functionality to RadzenAppearanceToggle. Tying the usage of a component to a particular set of "unified" themes is even harder to explain.

That's never going to happen I am afraid. I've told you why - some people want their app to always look dark or light regardless of the user settings. This is why we can't just decommission the existing themes.

All in all implementing your suggestion leads to either a maintenance and support (having more themes and special components that do not work with all themes) or breaking changes. Unless we find a way around that we can't implement your suggestion.

To avoid having to explain the differences between ColorSchemeToggle and RadzenAppearanceToggle, we can merge them:

RadzenAppearanceToggle could adopt the logic of ColorSchemeToggle when the current theme is "Unified"—i.e., when RadzenAppearanceToggle.CurrentDarkTheme == RadzenAppearanceToggle.CurrentLightTheme.

For peoples want their app to always appear dark regardless of user settings, they can still use the "Unified Material" theme and simply add <html data-theme="dark"> in App.razor.

Existing users won’t lose anything when migrating to the "Unified Theme". It provides more functionality (as outlined in the "Pros" section above), not less..

The additional theme was introduced to prevent breaking changes and to ease the transition.
Yes, it adds temporary overhead, but in the next major Radzen version (where breaking changes are allowed), only the unified themes would remain—reducing the number of themes to maintain.

The real challenge isn't user-side migration but rather:

  • Radzen Studio Theme Editor: Users will no longer set a single value like "Sort item color = #ffffff". They’ll need to configure both dark and light variants. Alternatively, Radzen Studio could auto-generate dark variants by inverting light values.

  • Refactoring SCSS: Current styles need to be restructured to separate color management.

In summary, introducing the Unified theme is entirely feasible and beneficial for long run, if we're willing to invest the initial effort and are motivated to do so.

They will lose the existing behavior unless they apply data-theme="darik" which nobody would know and would come ask for support.

This is indeed going be a problem and would need additional unplanned for us development.

I am afraid we can't allocate the resources to make this change. Not to mention the two-step implementation (maintaining two sets of themes) and then the additional change required to keep the existing behavior (<html data-theme="dark">). Users that want this behavior can either use the workaround I've suggested earlier or the CSS from your gist.

1 Like