Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changing theme dynamically #8

Open
veeral-patel opened this issue Aug 13, 2019 · 31 comments
Open

Changing theme dynamically #8

veeral-patel opened this issue Aug 13, 2019 · 31 comments

Comments

@veeral-patel
Copy link

If I wanted to toggle my React app's theme by pressing a button in the app, without restarting my web server, would that be possible?

@peisenmann
Copy link

This is definitely a common concern for modern use cases. Users are more and more frequently coming to expect websites and apps to have a dark mode option that they can simply turn on or off.

@veeral-patel
Copy link
Author

Any thoughts on how we could achieve this in Ant @peisenmann?

As a workaround, you could create two different versions of your app (with different CSS), and deploy them at:

your-app.com (redirects to light.your-app.com)
light.your-app.com
dark.your-app.com

@peisenmann
Copy link

peisenmann commented Oct 3, 2019

@veeral-patel I haven't looked into it at all myself, so I don't know how it could possible work from the Ant side. A simple class at the highest level, like <div className="appRoot ant-dark">...</div> where everything inside the ant-dark region gets the dark theme could be adequate.

This would allow any number of themes to exist and be swappable at runtime in the browser with a simple class name change. I have no idea if that's feasible, but in lieu of a better solution, that could be adequate.

EDIT: I changed the wording of the suggestion above while keeping the concept.

@superadminfabian
Copy link

In case anyone is looking for a guide, I followed the below and have dynamic switching themes. Also gave the user control of individual colors (like primary).

https://medium.com/@mzohaib.qc/ant-design-live-theme-588233ea2bbc

You have to use LESS and add less.min.js to your index file. I also changed from the default react-scripts to rescripts. I had only ever used SCSS with create-react-app prior to this.

Medium difficulty, but worth it!

@veeral-patel
Copy link
Author

Check this out: @superadminfabian @peisenmann

https://github.com/gzgogo/antd-theme
https://github.com/gzgogo/antd-theme

@Yumcoder-dev
Copy link

https://github.com/YumcoderCom/antd-them-switch-cra

@MarkLyck
Copy link

How is this not in v4?

Please 🙏 ant design team, add a way to dynamically switch themes without the use of less variables.

@JoseRFelix
Copy link

Check out my article on how to dynamically switch between themes! Also, if you'd like to jump in right away to a package that can help you, you can check out react-css-theme-switcher.

@therakeshm
Copy link

therakeshm commented Sep 9, 2020

Check out my article on how to dynamically switch between themes! Also, if you'd like to jump in right away to a package that can help you, you can check out react-css-theme-switcher.

This is GEM! ✨ But one query tho, why do I have to run npx gulp less everytime I want to see the changes

can we hot reload it while the virtual dom changes?

@JoseRFelix
Copy link

Check out my article on how to dynamically switch between themes! Also, if you'd like to jump in right away to a package that can help you, you can check out react-css-theme-switcher.

This is GEM! ✨ But one query tho, why do I have to run npx gulp less everytime I want to see the changes

can we hot reload it while the virtual dom changes?

You could with Webpack! You'd have to create your own plugin though. However, I would discourage you from doing it since it will slow down your development build by a lot. If you don't want to use Gulp, you could create a custom script using vanilla Javascript and add a hook in your package.json that will execute it before starting the dev server or building for production.

The reason that I used Gulp is because of how well it handles file streams operations.

On the other hand, if you meant to change the theme on runtime in the browser, you could do it by following this article. I stopped using this solution because it was CPU demanding which is really bad for the user. Thankfully, I had predefined themes (light and dark) so I found a way to calculate the styles on build time instead of runtime, and just change between these generated styles.

@bobbui
Copy link

bobbui commented Sep 17, 2020

this is another good option to implement theme switcher https://github.com/mzohaibqc/antd-theme-webpack-plugin

@imCorfitz
Copy link

Check out my article on how to dynamically switch between themes! Also, if you'd like to jump in right away to a package that can help you, you can check out react-css-theme-switcher.

@JoseRFelix I found your article. Brilliant stuff. One thing missing was the ability to keep the selected dark mode. If I refreshed the browser, it would default to light again. I then found the https://github.com/donavon/use-dark-mode package, which keeps the selected theme in localstorage and it also supports the ability to detect if a Mac system is using dark mode, and chose that automatically. So I made a few changes to the gulpfile. See the Gist here: https://gist.github.com/imCorfitz/5a3724308a0b258a27f407ab659f91fd

This way, I have the theme css files, and then I can use the useDarkMode() hook to toggle the global dark mode on or off.

@Nxtra
Copy link

Nxtra commented Mar 25, 2021

Check out my article on how to dynamically switch between themes! Also, if you'd like to jump in right away to a package that can help you, you can check out react-css-theme-switcher.

@JoseRFelix I found your article. Brilliant stuff. One thing missing was the ability to keep the selected dark mode. If I refreshed the browser, it would default to light again. I then found the https://github.com/donavon/use-dark-mode package, which keeps the selected theme in localstorage and it also supports the ability to detect if a Mac system is using dark mode, and chose that automatically. So I made a few changes to the gulpfile. See the Gist here: https://gist.github.com/imCorfitz/5a3724308a0b258a27f407ab659f91fd

This way, I have the theme css files, and then I can use the useDarkMode() hook to toggle the global dark mode on or off.

Ow man, that sound awesome.
However I can't get it done. I am unfamiliar with the whole gulp setup, let alone all the extra config it allows you to do.
Do you by any chance have a sandbox or repo we can check out?

@momesana
Copy link

momesana commented Mar 26, 2021

Given the size of Antd and the sheer amount of development time already invested in the less styles I find it pretty unlikely that Antd will switch to anything else any time soon. I think css variables would help a lot but switching to those is equally unlikely given that there are still browsers out there which don't support it (IE and many mobile browsers). Using the theme-switcher recommended here has a serious drawback. We have to hard-code the path of the precompiled theme CSS files inside our application thereby missing all the advantages that webpack provides us with, namely referencing the less source files instead from within the code and also being able to use our standard loader pipeline without having two different build-steps.

We had to support dynamic themes the other day and solved it by using style-loader with lazyStyleTags as injectType. We had tried a few other approaches that each came with their own tradeoffs before we settled on that. The following list describes all the approaches we've tried.

Solution 1: No module imports / link element in document head / via useEffect hook
Compile the antd less files independently to stand-alone css files either manually by using less --js or by defining separate entry points for each theme file in your webpack config. Subsequently compell the browser to dynamically load the style by adding a corresponding link element:

<link rel="stylesheet" href="<path-to-the-css-file-as-statically-delivered-by-the-browser>" />

Note, that the uri in href refers to the file as statically served by the backend-server. This requires you to manually manage the relation of the source (i.e the less files) and target files (i. e. the compiled css to be loaded) which is a major drawback. Each time you add or rename or move a theme, you have to update the webpack config file to reflect that. You also would have to separately adapt the front-end code to become aware of new themes by hardcoding their name/location in the sources. Apart from that the implementation is straight-forward: Upon changing the active theme (for example via a select) a useEffect hook that has the themeId as dependency will remove the previous theme link (if any) and create a new one (usually it will simply detach it, update the href and append it as a child again to force a reload).

Solution 2: No module imports / link element in body / using <link ... /> element instead of hooks
The above approach can be simplified by using React to render a local link element (i. e. in the body) instead of adding one to the header. We can thus go without the useEffect and let React do the dirty work:

return (
    <link rel="stylesheet" href="<path-to-the-css-file-as-statically-delivered-by-the-browser>" />
)

Again the major drawback of having to manage the relation of less and css files manually remains.

Solution 3: use style-loader and LazyStlyeTag / via useEffect hook
Finally we ended up using style-loader's lazy style tags. To that end we introduced a second webpack loader rule to only target less files ending with .theme.less. Those, then were not fed to the mini-css-extract-plugin but rather piped into style-loader.

{
// also make sure not to include these files in the other less loader rule lest they
// end up being bundled twice i. e. here and inside the default css bundle
test: /\.theme\.less$/i,
    use: [
        {
            loader: 'style-loader',
            options: { injectType: 'lazyStyleTag' },
        },
        'css-loader',
        {
            loader: 'less-loader',
            options: {
                lessOptions: {
                    env: mode,
                    javascriptEnabled: true,
                },
                sourceMap: true, 
            },
        },
    ],
}

The resulting css module has two functions, use() and unuse(). We import the module statically in our source file and create an object that has a mapping of themeIds to said module. A useEffect hook will then call theuse(), unuse() respectively whenever the themeId changes. The hook is being called like that in App.jsx:

...
// the hook below applies the theme corresponding to
// themeId which is the currently active theme
useAntdTheme(themeId);

return (
    <div>
        ...
    </div>
);

The useAntdTheme hook is defined as follows:

...
export function useAntdTheme(themeId) { // themeId is the active (i. e. selected) theme
    useEffect(() => {
        const { styles } = themes[themeId]; // 2. retrieve the imported style module for current themeId
        styles?.use(); // 3. apply the styling
        return () => styles?.unuse(); // 1. unapply the previous styling (if any)
    }, [themeId]);
}

Last but not least: the mapping of theme Ids to style modules. In other words the themes object used in the above hook:

import compactStyles from '../themes/compact.theme.less';
import darkStyles from '../themes/dark.theme.less';
...
export const themes = {
   'compact': {
       id: 'compact', // used as value in the select
       displayName: 'Compact', // used as label in the select
       styles: compactStyles, // the style module
   },
   'dark': {
       id: 'dark',
       displayName: 'Dark',
       styles: darkStyles,
   },
};

Edit:
There is a running example on codesandbox and a corresponding github repository.

@peiwen-pts
Copy link

I tried to use your method @momesana , but in Umi.js only 'chainWebpack' is to change the original webpack settings. I am not familiar with the library so here's my code when I try to implement lazyStyleTag
:

// .umirc.ts
chainWebpack(config: Config) {
  config.module
      .rule('lazyless')
        .test(/\.lazy\.less$/)
        .oneOf('')
          .use('less-load')
            .loader('less-loader')
            .options({
              lessOptions: {
                env: 'development',
                javascriptEnabled: true,
              },
              sourceMap: true,
            })
            .end()
          .use('css-load')
            .after('less-loader')
            .loader('css-loader')
            .end()
          .use('style-load')
            .after('css-loader')
            .loader('style-loader')
            .options({ injectType: 'lazyStyleTag' })
}

and the error goes:
image

@momesana
Copy link

momesana commented Jun 7, 2021

I have never worked with Umj.js, @peiwen-pts so I can't say much without seeing the code or even better trying it out. From what I can see the files can't be found when webpack tries to build the bundle. Webpack here fails to find ./dark.lazy.less and light.lazy.less located in the same directory as ./src/layouts/components/TopMenuRightMostContent. Are the files present in the same directory? Has the ?modules query parameter been appended by webpack/umj.js or did you add them to the module path explicity when you imported the files?

In any case I will upload a repository today or in the coming days with the solution I proposed so you can try it out and hopefully figure out what the differences are that leads to the above failure.

@momesana
Copy link

momesana commented Jun 7, 2021

@peiwen-pts: here you go: https://github.com/momesana/dynamic-antd-theme-demo

Of interest are:

Here is a working example on codesandbox:
https://codesandbox.io/s/github/momesana/dynamic-antd-theme-demo

If you like, you can also checkout the repository yourself and run it with npm install followed by npm run dev which should give you the following on localhost, port 3000 if you select the Dark Compact theme:

select

or this if you go with the Light theme:

light

@peiwen-pts
Copy link

@momesana Thank you so much for the feedback. I temporarily solved the problem by employing Gulp: if anyone interested please check out this post by Jose Felix.

The thing might be of one's concern is that the css files might contain redundant code since Umi.js uses webpack to pack up original antd less files as well as the additional less files (which are to customise ant design components) into one file umi.css. In addition to this, Gulp packs up two more (dark and light) styles. So in the :
image
umi.css 400KB
light-theme.css 514KB
dark-theme.css 524KB

I would not know how to optimise this... It would be AWESOME if someone can point out where to start! THANKS!!!

@azinit
Copy link

azinit commented Aug 17, 2021

Still actual!

@smitroshin
Copy link

smitroshin commented Sep 1, 2021

@momesana

I use CRA + CRACO.

Your approach with lazyStyleTag doesn't work as a static build (with npm run build).

App crashes on the client side:

image
image

Issue is also discussed here: webpack-contrib/style-loader#81

@smitroshin
Copy link

@momesana how you deploy the app ?

@momesana
Copy link

momesana commented Sep 2, 2021

@smitroshin
I don't know how your setup looks like or what webpack config CRA uses. In order for loading/unloading (i. e. use()/unuse()) to work properly, you'll have to make sure that the themes are actually being processed by a less loader with a suitable lazyStyleTag configuration, like so: https://github.com/momesana/dynamic-antd-theme-demo/blob/1bfe8442fec1fcfbbad4295505342ec79bc0e82a/webpack.config.js#L45.
I can assure you that the code snippet I contributed also works in production mode. You can verify this on your own by pulling the latest changes from the repo and executing the two following commands from the project root folder:

npm run build
npx http-server ./dist

The first command generates the javascript bundle inside the dist folder. The subsequent command starts http-server and serves said bundle. You'll see that it works just like the version run inside the webpack-dev-server.

@smitroshin
Copy link

@momesana
Thank you very much for you response.
I seen that your app is workable for production build.

Anyway, I didn't found any solution for my case. Seems like some configs from CRA or CRACO doesn't work well with lazyStyleTag.

There is repo with my case.
For the moment I can't spend more time for understating why the error above appear and I will try other ways for dynamic theming.

But maybe somebody will find a way :)

@vandercloak
Copy link

vandercloak commented Oct 10, 2021

@momesana Thanks for all the time you put into recording your findings here.

I was wondering if you had any advice on how your Solution 3 (or other solutions) could work with dynamic variables loaded from an api request at run time?

Ex. We have organizations with different theme settings. When a page is loaded, we fetch the org settings and retrieve a various theme settings (like primaryColor). Would your solution work for something like that? Maybe by generating a less file on the fly?

@vandercloak
Copy link

vandercloak commented Oct 18, 2021

It looks like 4.17.0-alpha.5 introduces support for css variables for overriding themes.

https://codesandbox.io/s/global-theme-antd-4-17-0-alpha-5-forked-ey3qq?file=/index.js:2290-2304

@Fiorello
Copy link

Fiorello commented Dec 7, 2021

It looks like 4.17.0-alpha.5 introduces support for css variables for overriding themes.

https://codesandbox.io/s/global-theme-antd-4-17-0-alpha-5-forked-ey3qq?file=/index.js:2290-2304

True, but it isn't the solution.
You can't switch from dark to light, instead you can set only

    primaryColor?: string;
    infoColor?: string;
    successColor?: string;
    processingColor?: string;
    errorColor?: string;
    warningColor?: string;

I'm using the craco-less solution with less.js to change theme at runtime, but i'd like to switch to css variables solution.
However, I think it is not yet stable enough

@momesana
Copy link

momesana commented Jan 2, 2022

I was wondering if you had any advice on how your Solution 3 (or other solutions) could work with dynamic variables loaded from an api request at run time?

Ex. We have organizations with different theme settings. When a page is loaded, we fetch the org settings and retrieve a various theme settings (like primaryColor). Would your solution work for something like that? Maybe by generating a less file on the fly?

The theme files are generated by webpack using the styled loader, so the benefit is that we can import the less file in our react application as an asset and this is then basically turned into a javascript module that can then be loaded (and the styles applied) when the app is running. That does however require that this module is generated during compile-time. In other words: you can dynamically load the themes at run-time but they need to already exist as [javascript] style modules.

In the scenario you described, theme parameters are obtained via an API during run-time. With the newest version of antd (i. e. >= 4.17.0), you can utilize the CSS variables to achieve what you outlined though unfortunately only a small subset of the variables is currently exposed by antd. Another way would be to return the name of the theme instead of the variables. That does of course preclude the ability to dynamically customize themes in detail, but allows you to provide a predefined set of themes and use the API to decide which one to use.

You can of course always look up what CSS rules are generated by antd when the less files are compiled to CSS and use specificity rules to override them by creating a corresponding style tag on the fly and attaching it to the DOM, overriding the variables via Javascript or use a CSS-in-JS solution (like createGlobalStyle in styled-components) . That's not an elegant solution though and also brittle as future changes to antd CSS selectors will break your styles. With the small portion of the less variables currently exposed as CSS variables this is probably still the only viable option if you need to be able to customize the individual themes itself.

@momesana
Copy link

momesana commented Jan 2, 2022

It looks like 4.17.0-alpha.5 introduces support for css variables for overriding themes.
https://codesandbox.io/s/global-theme-antd-4-17-0-alpha-5-forked-ey3qq?file=/index.js:2290-2304

True, but it isn't the solution. You can't switch from dark to light, instead you can set only

    primaryColor?: string;
    infoColor?: string;
    successColor?: string;
    processingColor?: string;
    errorColor?: string;
    warningColor?: string;

I'm using the craco-less solution with less.js to change theme at runtime, but i'd like to switch to css variables solution. However, I think it is not yet stable enough

Deciding when to apply a theme is the duty of the developer. This is not something antd could or should do. Besides countless existing react hooks for that, a custom hook that achieves this can be realized in a few lines of code. Changing the value of the variables is also trivial. The only problem is that only a small subset of the variables is currently exposed.

@momesana
Copy link

momesana commented Jan 2, 2022

As of now the latest available version of antd is 4.18.2. Running grep -oh '\-\-ant[0-9a-zA-Z-]*' ./node_modules/antd/dist/antd.variable.css | sort | uniq to extract the available set of currently exposed antd css variables yielded the following output:

--antd-wave-shadow-color
--ant-error-color
--ant-error-color-active
--ant-error-color-deprecated-bg
--ant-error-color-deprecated-border
--ant-error-color-hover
--ant-error-color-outline
--ant-info-color
--ant-info-color-deprecated-bg
--ant-info-color-deprecated-border
--ant-primary-1
--ant-primary-2
--ant-primary-3
--ant-primary-4
--ant-primary-5
--ant-primary-6
--ant-primary-7
--ant-primary-color
--ant-primary-color-active
--ant-primary-color-active-deprecated-d-02
--ant-primary-color-active-deprecated-f-30
--ant-primary-color-deprecated-f-12
--ant-primary-color-deprecated-l-20
--ant-primary-color-deprecated-l-35
--ant-primary-color-deprecated-pure
--ant-primary-color-deprecated-t-20
--ant-primary-color-deprecated-t-50
--ant-primary-color-hover
--ant-primary-color-outline
--ant-success-color
--ant-success-color-active
--ant-success-color-deprecated-bg
--ant-success-color-deprecated-border
--ant-success-color-hover
--ant-success-color-outline
--ant-warning-color
--ant-warning-color-active
--ant-warning-color-deprecated-bg
--ant-warning-color-deprecated-border
--ant-warning-color-hover
--ant-warning-color-outline

That is just a small subset of the available less variables and notably the background colors are entirely absent. That's not nearly enough to support a dark theme and only lends itself for minor tweaks like for example changing the primary color. Hopefully more variables will be exposed in the future.

see: ant-design/ant-design#33534

@kicks321
Copy link

kicks321 commented Apr 8, 2022

Hey, so.... I created a working example of how to get this to work with a custom provider wrapped around the Config provider

//// ThemeProvider
`import React from 'react';
import { ConfigProvider } from 'antd';
import { Theme } from '@/types';
import { ConfigProviderProps } from 'antd/lib/config-provider';

interface ThemeProviderProps extends ConfigProviderProps {
children?: React.ReactNode;
theme?: Theme;
}

const getColorScheme = (mode: Theme | undefined) => {
if (mode === Theme.DARK) {
return {
primaryColor: '#ff4d4f',
errorColor: '#ff4d4f',
warningColor: '#faad14',
successColor: '#52c41a',
infoColor: '#1890ff',
};
}

return {
primaryColor: '#1890ff',
errorColor: '#ff4d4f',
warningColor: '#faad14',
successColor: '#52c41a',
infoColor: '#1890ff',
};
};

interface IThemeContext {
newTheme: Theme;
toggleDark?: () => void;
}

const defaultState = {
newTheme: Theme.LIGHT,
};

export const ThemeContext = React.createContext(defaultState);

const ThemeProvider: React.FC = ({ theme, children, ...props }) => {
const [newTheme, setTheme] = React.useState(Theme.DARK);

const toggleDark = () => {
if (newTheme === Theme.LIGHT) {
setTheme(Theme.DARK);
} else {
setTheme(Theme.LIGHT);
}
};

React.useMemo(() => ConfigProvider.config({ theme: getColorScheme(newTheme) }), [newTheme]);

return (
<ThemeContext.Provider value={{ newTheme, toggleDark }}>
<ConfigProvider {...props}>{children}
</ThemeContext.Provider>
);
};

export default ThemeProvider;
`

//// Page.tsx - Consuming the Context
`import { Button, Spin, Switch } from 'antd';
import React, { useContext } from 'react';
import { ThemeContext } from '@/providers/theme.provider';

const PageView = () => {
const { toggleDark } = useContext(ThemeContext);
return (
<div
style={{
justifyContent: 'center',
display: 'flex',
alignItems: 'center',
height: '100%',
flexDirection: 'column',
}}>



);
};

export default PageView;`

@kicks321
Copy link

kicks321 commented Apr 8, 2022

The key is to utilize useMemo in the ConfigProvider.config() options. Specifically only calling when the global theme prop changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests