Using Skeleton UI's dark mode toggle programmatically from within other components

There’s a whole ton about developing in the real world that I didn’t learn until I got to work with other developers. One of those lessons is relevant here: Never be afraid to dive into the source code of the open software that your software depends on. If you think a function call should be working, but it isn’t, you can — and should — go look at what it’s doing with your input!

I’ve known from first-hand experience that the folks behind the Skeleton Svelte UI library have not been shy about making sure any state that Skeleton components need/use is managed in Svelte stores. They expose this quite often in the documentation, however, I don’t see any mention in the dox about how to work with the user’s dark mode state programmatically – i.e., whether the user has dark mode on or off.

I was thinking about different ways I could handle changing between two different backgrounds depending on whether dark mode or light mode was on, which led to wondering whether I could check the user’s dark mode choice from Javascript code in case someone asked me to do something beyond visual changes – think something like having a chat bot greet the user differently based on whether they have dark mode on or off. To do this, I’d have to figure out where the <Lightswitch /> component was storing this value.

Sure enough, Skeleton stores this value with one of its own Local Storage Stores. Colocated with almost every one of its Svelte components is a Typescript file that exports functions that are exclusive to that Svelte component. This lightswitch.ts file is one that supports the Lightswitch component, which we can see is much more robust than it seems on the surface!

If you checked out that code, you could see on line 3 that the component imports a whole bunch of exports from that file. One of those tasty chunks of Typescript is a Local Storage Store called modeCurrent, and it simply stores a boolean value: true or false.

We can see the function is imported on line 5, then right below that we have a few stores defined:

// Stores ---
// TRUE: light, FALSE: dark

/** Store: OS Preference Mode */
export const modeOsPrefers = localStorageStore<boolean>('modeOsPrefers', false);
/** Store: User Preference Mode */
export const modeUserPrefers = localStorageStore<boolean | undefined>('modeUserPrefers', undefined);
/** Store: Current Mode State */
export const modeCurrent = localStorageStore<boolean>('modeCurrent', false);

The major key 🔑 is the export keyword: it’s exporting the stores after they’ve been created, meaning we can import them elsewhere. The keyword alone isn’t what makes the next code line happen, though – it will get re-exported at the package level for that, otherwise we’d have to import from the lightSwitch.ts file directly.

So in some random text component – nested quite a few levels deep into the onion – I imported this store for my own usage:

import { modeCurrent } from '@skeletonlabs/skeleton'

This example is redundant, as I’ll get into at the end, but from here, you could do a reactive statement to create a class to slap on the component:

<script>
  // we access a store's value with $storeName:
  $: bgClass = $modeCurrent ? 'graph-paper' : 'circuit-board'
</script>

If you haven’t seen this before – in the code above I use a ternary operator to have { bgClass } evaluate to graph-paper when it’s light mode, and circuit-board when dark mode is on.

We could also use a derived store to calculate derivative values based on light/dark:

<script>
  import { modeCurrent } from '@skeletonlabs/skeleton'
  import { derived } from 'svelte/store'

  const welcomeMessage = derived(modeCurrent, ($isDarkMode)=>{
    // process the value and return the bg class or other variable you need to do the job
    return $isDarkMode ? 'Hello, light mode!' : 'Pssst... hey, over here'
  })
</script>

<div class="">
  <h2>{ $welcomeMessage }</h2>
  <slot />
</div>

Then you would, as always, read your derived store’s value elsewhere with the dollar sign syntax: $bg

but wait… couldn’t I just use the dark: prefix?

Yes. You’d really only want to do this if you wanted to do dynamic (ie, with Javascript) things in response to the user’s choice. You wouldn’t normally want to do JS things in response to dark mode, especially in a Tailwind-enabled app. You’d typically just be changing colors.

In typical usage of dark mode within Skeleton and/or Tailwind (which Skeleton is built upon), There will be a .dark class on the body for you to use in your CSS rules to do dark-only styles, and there’s a dark: prefix you can use with your classes to indicate a class is dark-mode only.

<script>
  // doing other things up here for my component that have nothing to do with dark mode
</script>

<div class="text-slate-500 bg-white dark:text-white dark:bg-slate-900">
  <p>my content</p>
</div>

<style lang="postcss">
  /* we could also do it from component styles, using @apply */
  div {
    @apply text-slate-500 bg-white dark:text-white dark:bg-slate-900;
  }
</style>

©2024 Joe Castelli