hperrin / svelte-material-ui

Svelte Material UI Components
https://sveltematerialui.com/
Apache License 2.0
3.32k stars 285 forks source link

How to switch themes? #248

Open robsch opened 3 years ago

robsch commented 3 years ago

Sorry for this possibly dumb question. According to the doc I have done this:

npm install --save-dev smui-theme
npx smui-theme template src/theme

This has created the two files src/theme/_smui-theme.scss and src/theme/dark/_smui-theme.scss.

Then added this to package.json and executed "prepare":

"prepare": "npm run smui-theme-light && npm run smui-theme-dark",
"smui-theme-light": "smui-theme compile build/smui.css -i src/theme",
"smui-theme-dark": "smui-theme compile build/smui-dark.css -i src/theme/dark",

This created the files build/smui.css and build/smui-dark.css.

Should I then link them both to index.html? Then one would override the other. What do you propose for switching the themes with a button? In a way as described here? Ideally I would store the last used setting in localStorage so that when the user re-opens the page the right theme gets applied immediately.

I don't user sapper or sveltekit.

robsch commented 3 years ago

Now I have also done what is described in Advanced Styling Method. So now only bundle.css is generated, instead of the two files before. That would mean that I link only this one css file in index.html.

However, the bundle.css seems to only include what is in src/theme/_smui-theme.scss, but not that from src/theme/dark/_smui-theme.scss. Why is that? Without it no switching would be possible...

marchamon commented 3 years ago

Here's what I did to switch easily between dark and light themes using the basic theming:

First, update your scss files this way:

@use 'sass:color';

@use '@material/theme/color-palette';

// Svelte Colors! (Dark Theme)
@use '@material/theme/index' as theme with (
  $primary: #ff3e00,
  $secondary: color.scale(#676778, $whiteness: -10%),
  $surface: color.adjust(color-palette.$grey-900, $blue: +4),
  $background: #000,
  $error: color-palette.$red-700
);

html,
body {
  height: 100%;
}

.dark-theme {
  height: 100%;
  background-color: #000;
  color: theme.$on-surface;

  a {
    color: #40b3ff;
  }
  a:visited {
    color: color.scale(#40b3ff, $lightness: -35%);
  }
}

The only thing I changed from the original scss file is that I put all the styling in the dark-theme class and the height of everything to 100% so that the component that'll use my new class will take the whole space. Do the same with the light theme.

Then you can run the prepare command and include both of yours style in the index.html. They do not override each other as they are guarded by the dark-theme and light-theme classes.

In you App.svelte, just add a parent container that set one of the classes based on a variable and you can easily switch between themes.

I hope that's helpful!

robsch commented 3 years ago

Thank you @marchamon. I think I've got your idea. But it seems that you originally have only one scss file, right? Since with the steps that I done (in my first post) creates two files: src/theme/_smui-theme.scss and src/theme/dark/_smui-theme.scss. From where did you get your single scss file?

The other thing that is not clear is how to set up rollup. Since I was dealing with two scss files I was not able to process them both via rollup (using rollup-plugin-postcss). I don't want to call 'prepare' seperatly, I'd like to use rollup.

Can you provide a rollup config as well?

marchamon commented 3 years ago

Thank you @marchamon. I think I've got your idea. But it seems that you originally have only one scss file, right? Since with the steps that I done (in my first post) creates two files: src/theme/_smui-theme.scss and src/theme/dark/_smui-theme.scss. From where did you get your single scss file?

I don't. I did the steps from the documentation and I have two separate scss files, just like yours. What I meant is that I used container classes in each scss file (.dark-theme in theme/dark/_smui-theme.scss and .light-theme in theme/_smui-theme.scss).

Then in your index.html file you still have to add both stylesheets:

<link rel="stylesheet" href="/build/smui.css" />
<link rel="stylesheet" href="/build/smui-dark.css" />

The other thing that is not clear is how to set up rollup. Since I was dealing with two scss files I was not able to process them both via rollup (using rollup-plugin-postcss). I don't want to call 'prepare' seperatly, I'd like to use rollup.

Can you provide a rollup config as well?

I don't really work on the themes right now so I didn't change the rollup config (yet). I guess what I would try is to add both paths to the themes (same config as the one given in the doc + path to the dark theme):

postcss({
      extract: true,
      minimize: true,
      use: [
        [
          "sass",
          {
            includePaths: ["./src/theme", "./src/theme/dark", "./node_modules"],
          },
        ],
      ],
    }),

I've never used rollup nor svelte before so it might not work!

robsch commented 3 years ago

Thanks again. Okay, you have those two files.

I tried setting the includePaths to both directories already. But I get only one bundle.css file in the build directory and it contains only the scss stuff of the first entry in the includePaths array (and of node_modules). The second entry seems to get skipped.

This is my current rollup configuration:

import svelte from 'rollup-plugin-svelte';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import livereload from 'rollup-plugin-livereload';
import { terser } from 'rollup-plugin-terser';
import postcss from 'rollup-plugin-postcss';
import replace from '@rollup/plugin-replace';
import dotenv from 'dotenv';

dotenv.config();

const production = !process.env.ROLLUP_WATCH;

function serve() {
    let server;

    function toExit() {
        if (server) server.kill(0);
    }

    return {
        writeBundle() {
            if (server) return;
            server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
                stdio: ['ignore', 'inherit', 'inherit'],
                shell: true
            });
            process.on('SIGTERM', toExit);
            process.on('exit', toExit);
        }
    };
}

export default {
    input: 'src/main.js',
    output: {
        sourcemap: true,
        format: 'iife',
        name: 'app',
        file: 'public/build/bundle.js'
    },
    plugins: [
        replace({
            preventAssignment: true,
            'process.env.EOS_ENDPOINT': JSON.stringify(process.env.EOS_ENDPOINT),
        }),
        svelte({
            compilerOptions: {
                dev: !production
            }
        }),
        postcss({
            extract: true,
            minimize: true,
            use: [
                [
                    'sass',
                    {
                        includePaths: ['./src/theme', './src/theme/dark', './node_modules'],
                    },
                ],
            ],
        }),

        resolve({
            browser: true,
            dedupe: ['svelte']
        }),
        commonjs(),
        !production && serve(),
        !production && livereload('public'),
        production && terser()
    ],
    watch: {
        clearScreen: false
    }
};

Is there anything wrong? I'm still new to rollup, svelte, sass. I don't know what to do. But I assume that svelte-material-ui should work as expected. Interestingly, the guide about Theming was changed a time ago. There has been more instructions when you have a look at https://github.com/hperrin/svelte-material-ui/blob/bab2ff4f973bad462f9d4696333bd541f91987aa/THEMING.md. For some reason it was removed and now the guide misses some information.

miguelrueda commented 3 years ago

Hi, I'm working on the same feature, did you got a method to change between the themes? Do you know how can I affect the component styles based on the theme selected?

mcarraragh commented 3 years ago

Hi, I'm working on the same feature, did you got a method to change between the themes? Do you know how can I affect the component styles based on the theme selected?

Was my first response (https://github.com/hperrin/svelte-material-ui/issues/248#issuecomment-846376293) not helpful to you? It worked like a charm for me. The idea is to:

If you put the theme class to the top level of your page, all the components will be impacted. You might have to change a few things to the CSS files I provided, but the whole thing relies on these guard classes.

miguelrueda commented 3 years ago

Sorry, my bad, I should have added more context:

Yes, your response was very helpful, I now have 2 different files for light and dark theme and I already linked them on my index.html file. When I refer to one or the other I have the styles applied on my elements.

My questions was more related to "switch theme" feature, I added a toggle button on my Component.svelte, when "on click" event occurs it will toggle class with "document.body.classList.toggle("dark-theme", someCondition)" but it only toggles the class on the element and the styles are not applied. Is this the correct way to toggle the class? Or should I do it in another way?

Another question, more related to development, I need to recompile the sass files every time I change something? There is no way to test the sass files during development?

Thanks for your response.

mcarraragh commented 3 years ago

That's not the way you should do it, you should use Svelte mechanisms.

In my App.svelte I have:

<script lang="ts">
  // @ts-check
  // ... Other stuff
  import { theme } from "@layout/stores.js";

  let currentTheme: string;
  theme.subscribe((newTheme) => (currentTheme = newTheme));

  // ... Other stuff

  router.start();
</script>

<main class="{currentTheme}-theme">
  // ... Other stuff
</main>

I used a store since my switch button isn't in the same component but on a separate header. The store looks like this:

import { writable } from "svelte/store";

export const theme = writable("dark");

In my header component, I have this:

<script lang="ts">
  // @ts-check
  import { theme } from "./stores.js";

  let currentTheme = "dark";

  function toggleTheme(): void {
    currentTheme = currentTheme === "light" ? "dark" : "light";
    theme.update(() => currentTheme);
  }

  //... Other stuff
</script>

If you have both the switch button and the theme class in the same component, you can skip the store-related content. Just use the toggleTheme function directly in your component.

Another question, more related to development, I need to recompile the sass files every time I change something? There is no way to test the sass files during development?

I guess you can configure Rollup to do this for you, but I couldn't say how since I never did and I'm not familiar with Rollup. I only did a small project to try Svelte features and see how it works. The only sass files I had were the theme files (and I didn't customize smui components so there wasn't many changes to try anyway), I did the rest of my styling in the style tag of the component.

miguelrueda commented 3 years ago

Thank you very much for your response, this approach will be very helpful, I'll try it and I will search about rollup configs. Regards.

HootDunk commented 3 years ago

I tried the above steps and the problem that I had with that approach is that the theme colors specified in the beginning of each .scss file:

@use '@material/theme/index' as theme with (
    $primary: #ff3e00,
    $secondary: color.scale(#676778, $whiteness: -10%),
    $surface: color.adjust(color-palette.$grey-900, $blue: +4),
    $background: #000,
    $error: color-palette.$red-700
);

are overridden by whichever css file is linked second in the html file. So even if you have the .dark-mode and .light-mode classes where you define colors for elements, these styles aren't properly applied to the actual Material UI components.

To solve this I combed through the dependency graph for Svelte-Material-UI and found a solution from the libraries author @hperrin. Here's a link to the repo https://github.com/hperrin/smui-example-sveltekit/

First make sure both of the generated CSS files are linked like the below example.

<link rel="stylesheet" href="/smui.css" media="(prefers-color-scheme: light)" />
<link rel="stylesheet" href="/smui-dark.css" media="screen and (prefers-color-scheme: dark)" />

The media attribute will set the theme based on the users OS settings which is a nice advantage to this solution. However, it's still beneficial to offer a toggle so that they can switch from the default if they desire. This can be done pretty simply with this function:

  let lightTheme =
    typeof window === 'undefined' ||
    window.matchMedia('(prefers-color-scheme: light)').matches;

  function switchTheme() {
    lightTheme = !lightTheme;
    let themeLink: HTMLLinkElement = document.head.querySelector('#theme');
    if (!themeLink) {
      themeLink = document.createElement('link');
      themeLink.rel = 'stylesheet';
      themeLink.id = 'theme';
    }
    themeLink.href = `/smui${lightTheme ? '' : '-dark'}.css`;
    document.head
      .querySelector('link[href="/smui-dark.css"]')
      .insertAdjacentElement('afterend', themeLink);
  }

All this does, is place the CSS file of the users desired theme last.

Heres the link to where you can find this function in the repo -> https://github.com/hperrin/smui-example-sveltekit/blob/master/src/routes/__layout.svelte

paoloricciuti commented 2 years ago

I tried the above steps and the problem that I had with that approach is that the theme colors specified in the beginning of each .scss file:

@use '@material/theme/index' as theme with (
    $primary: #ff3e00,
    $secondary: color.scale(#676778, $whiteness: -10%),
    $surface: color.adjust(color-palette.$grey-900, $blue: +4),
    $background: #000,
    $error: color-palette.$red-700
);

are overridden by whichever css file is linked second in the html file. So even if you have the .dark-mode and .light-mode classes where you define colors for elements, these styles aren't properly applied to the actual Material UI components.

To solve this I combed through the dependency graph for Svelte-Material-UI and found a solution from the libraries author @hperrin. Here's a link to the repo https://github.com/hperrin/smui-example-sveltekit/

First make sure both of the generated CSS files are linked like the below example.

<link rel="stylesheet" href="/smui.css" media="(prefers-color-scheme: light)" />
<link rel="stylesheet" href="/smui-dark.css" media="screen and (prefers-color-scheme: dark)" />

The media attribute will set the theme based on the users OS settings which is a nice advantage to this solution. However, it's still beneficial to offer a toggle so that they can switch from the default if they desire. This can be done pretty simply with this function:

  let lightTheme =
    typeof window === 'undefined' ||
    window.matchMedia('(prefers-color-scheme: light)').matches;

  function switchTheme() {
    lightTheme = !lightTheme;
    let themeLink: HTMLLinkElement = document.head.querySelector('#theme');
    if (!themeLink) {
      themeLink = document.createElement('link');
      themeLink.rel = 'stylesheet';
      themeLink.id = 'theme';
    }
    themeLink.href = `/smui${lightTheme ? '' : '-dark'}.css`;
    document.head
      .querySelector('link[href="/smui-dark.css"]')
      .insertAdjacentElement('afterend', themeLink);
  }

All this does, is place the CSS file of the users desired theme last.

Heres the link to where you can find this function in the repo -> https://github.com/hperrin/smui-example-sveltekit/blob/master/src/routes/__layout.svelte

Honestly this seems a bit too much imperative for a declarative framework.

A more elegant solution imho is to add a __layout page and use the tag to include the correct file inside recovering the variable from the store.

Also i think this should be better handled from the library itself. While it's very cool to have the correct css included with the media query it's not very practical when you have to do this kind of trickery to let the user change the theme despite is system preference.

HootDunk commented 2 years ago

Honestly this seems a bit too much imperative for a declarative framework.

Yeah I would agree. At the time of making that comment I hadn't yet stumbled on the svelte:head tag. A much more declarative way of doing this would be creating a component to manage the theme which has something like this included in the markup:

<svelte:head>
  {#if $darkModeStore}
    <link rel="stylesheet" href="/smui-dark.css" />
  {:else}
    <link rel="stylesheet" href="/smui.css" />
  {/if}
</svelte:head>

In the above case I am switching the linked style sheet based on the value of the darkmode store which is a boolean. In the component script tag you can check the users system preferences, check local storage for past saved preferences, etc... and update/set the value of the store based on that.

jdgamble555 commented 2 years ago

I put both css files in my header, then on my page I added this:

<link rel="stylesheet" id="theme" href="/smui{$darkMode ? '-dark' : ''}.css" />

where $darkMode is my boolean svelte store.

J

N00nDay commented 2 years ago

Toggle works fine the bigger issue I have is how to persist the users choice without having the default theme flicker first. I have it set up using a store | localStorage | css prefers-color-schema but with the default being light, the light theme will flicker before finally loading the dark theme. Very jaring experience for users.

https://user-images.githubusercontent.com/28783829/152662811-876c8a5a-8fdb-4ba5-9e10-b6eb14a3e333.mov

.

N00nDay commented 2 years ago

After doing some digging I just opted to wait for the top most layout to be mounted. If its mounted then show the content if not don't. This way the page only shows after the css has been assigned. It will give a few milliseconds delay but its better than the flicker.

ModischFabrications commented 2 years ago

Just coming through to drop a first impression: I was searching for a way to toggle dark mode, somehow I expected some presets to exist. I'm fine with the methods to create a custom theme, but I feel like some (sensible) defaults would make onboarding a lot easier. I personally picked up SMUI because I can't be trusted around CSS, I would've liked something that "just works" to switch modes, even if generic.

ModischFabrications commented 2 years ago

For anyone stumbling through: Use the theme builder to generate a default theme with dark mode support, it's much easier. No need for custom code or magic variables, it seems to work just fine out of the box.

Be aware that the documentation seems to have a mistake (https://github.com/hperrin/svelte-material-ui/issues/465) as of now.

michal-kapala commented 2 years ago

The idea is to:

* Have two separate files for the different themes. Each file contains a "guarding" class (`.light-theme` or `.dark-theme`), so that if you include both themes, the styles do not override each other

* Include both themes in the index.html

* Switch the theme by using either `.light-theme` or `.dark-theme` to the root of your page. For this, I used a variable and a button that changes that variable

If you put the theme class to the top level of your page, all the components will be impacted. You might have to change a few things to the CSS files I provided, but the whole thing relies on these guard classes.

Thanks for the solution, it goes pretty nice with Svelte's class directive.

One trick i can add is if you use dark as your wrapper class name you can make use of Tailwind's inline styling for dark mode simultaneously.

Lancer11211 commented 1 year ago

I'm struggling with the theming for many hours. I've this:

<svelte:head>
  {#if $isDarkMode}
    <link rel="stylesheet" href="smui-dark.css" />
  {:else}
    <link rel="stylesheet" href="smui.css" />
  {/if}
</svelte:head>

but it doesn't matter where I put the css files, svelte can't load it.

Edit: ahh, nevermind, wrong asset directory.

jdgamble555 commented 1 year ago

This is how I do it:

<link rel="stylesheet" id="theme" href="/smui{$isDarkMode ? '-dark' : ''}.css" />

J

Lancer11211 commented 1 year ago

This is how I do it:

<link rel="stylesheet" id="theme" href="/smui{$isDarkMode ? '-dark' : ''}.css" />

J

Thanks, that looks nice!

chinmay-inaza commented 1 year ago

This is the best answer that I have found online:

https://mvolkmann.github.io/blog/svelte/svelte-material-ui/?v=1.0.19

I converted the darkMode variable in this solution to it's own Store.

Lancer11211 commented 1 year ago

document.documentElement.setAttribute("data-theme", "..."); is the most flexible solution I've found.

chinmay-inaza commented 1 year ago

Could you please elaborate with a small example?

Lancer11211 commented 1 year ago
  let isDarkMode = $user.isDarkMode;
  user.subscribe((value: any) => {
    isDarkMode = value.isDarkMode;
    if(browser) {
      if(isDarkMode) {
        document.documentElement.setAttribute("data-theme", "customdark");
      } else {
        document.documentElement.setAttribute("data-theme", "customlight");
      }
    }
  });

like this.

MasturFYC commented 1 year ago

this is the way that has worked (using sveltekit) include both css on app.html, add id attribute like theme

<head>
...
    <link rel="stylesheet" href="/smui.css" media="(prefers-color-scheme: light)" />
    <link id="theme" rel="stylesheet" href="/smui-dark.css" media="screen and (prefers-color-scheme: dark)" />
...
</head>

create function auto mode theme

...
<script lang="ts">
    function autoModeWatcher(): void {
        const t = localStorage.getItem('theme') as string;

        if (t === 'light') {
            let themeLink: HTMLLinkElement | null = document.head.querySelector('#theme');

            if (themeLink) {
                themeLink.media="screen and (prefers-color-scheme: light)"
            }
        }
    }
</script>

add that function in svelte:head

<svelte:head>
    {@html `<script>${autoModeWatcher.toString()} autoModeWatcher();</script>`}
</svelte:head>

create switch in header of page or any other page

<script>
...
    function toggle_theme(): void {
        const t = localStorage.getItem('theme') as string;
        const newTheme = t === 'dark' ? 'light' : 'dark';

        let themeLink: HTMLLinkElement | null = document.head.querySelector('#theme');

        if (themeLink) {
            localStorage.setItem('theme', t);
            themeLink.media = `screen and (prefers-color-scheme: ${newTheme})`;
    }
    }
...
</script>