htmlcsssveltesveltekitsvelte-component

Set `body` `background-color` programmatically in Svelte?


I'm making a site using Svelte and SvelteKit, and I'd like to use a predefined array of colors for styling in a normalized way-- if I change one of the values later, everywhere that styles using that value will be updated later.

// palette.js

export const grays = [
  'hsl(240 3.7% 10.6%)',
  'hsl(240 13% 18%)',
  ...
];
// routes/+page.svelte

<script>
  import { grays } from '$lib/palette.js';
</script>

<div style="background-color: {grays[0]};">
  <i>stuff</i>
</div>

My problem is I can't figure out which is the best way to apply one of my colors to <body>.

Setting CSS values in a <style> tag like this:

<style>
  :global(body) {
    background-color: {grays[0]};
  }
</style>

is not valid syntax for Svelte (and using :global rules generally seems to be unidiomatic).

My current options (that I can think of) seem to be:

  1. Set the document.body.style.backgroundColor in a <script>-- might cause flickering?
  2. Don't style <body> at all, style a big <div> with a custom CSS property that covers everything (as shown in the Svlte tutorial)-- probably my best option but seems a bit ugly / smelly / boilerplatey to do all the var(...) and style="--customvar={grays[0]};.

What should I do? Am I overthinking this?


Solution

  • You can set CSS variables on the <html> element/:root and they will automatically be inherited to the rest of the application. A good place is the style of the root layout (src/routes/+layout.svelte) or a stylesheet imported from said layout.

    <!-- +layout.svelte --->
    <slot />
    <style>
      :global(html) {
        --gray-1: ...;
        --gray-2: ...;
        --gray-3: ...;
        /* ... */
    
        background-color: var(--gray-1);
      }
    </style>
    

    <!-- +layout.svelte --->
    <script>
      import './site.css';
    </script>
    <slot />
    
    /* site.css */
    html {
      --gray-1: ...;
      --gray-2: ...;
      --gray-3: ...;
      /* ... */
    
      background-color: var(--gray-1);
    }
    

    If for some reason the source of truth has to be some piece of JS, the most reliable way is probably to inject the values on build. I would still work with CSS variables, though.

    You could e.g. generate the required CSS from the palette file via a custom Vite plugin.

    /** @import { PluginOption } from 'vite' */
    
    /**
     * A plugin that allows importing a generated CSS file from `@palette`.
     * @param {Record<string, string[]>} definitions
     *   Palette prefixes and the values for each palette.
     * @returns {PluginOption} The resulting plugin object.
     */
    export function palettes(definitions) {
      const prefixedId = '/@palette/variables.css';
    
      return {
        name: 'palette-injector',
        resolveId(source) {
          if (source == '@palette')
            return prefixedId;
        },
        load(id) {
          if (id != prefixedId)
            return null;
    
          const css = Object.entries(definitions)
            .flatMap(([prefix, values]) =>
              values.map((value, i) => `--${prefix}-${i + 1}: ${value};`)
            )
            .join('\n');
    
          return {
            code: `:root { ${css} }`,
            map: null,
          };
        },
      }
    }
    
    // vite.config.js
    import { defineConfig } from 'vite';
    import { sveltekit } from '@sveltejs/kit/vite';
    import { palettes } from './palettes-plugin';
    import { grays } from './src/lib/palette';
    
    export default defineConfig((cfg) => ({
      // ...
      plugins: [
        palettes({
          gray: grays,
        }),
        sveltekit(),
      ],
    }));
    
    <!-- +layout.svelte --->
    <script>
      import '@palette';
    </script>
    <slot />
    <style>
      :global(html) {
        background-color: var(--gray-1);
      }
    </style>