Dynamic CSS themes with Elm
Tags: elm
Published: 2020-09-27 17:32:41

## Inspiration

I wanted to learn how to theme Single Page Applications (SPAs) apps using global CSS3 variables instead of CSS-in-JS since some design systems are built to be platform agnostic. I'm using Elm because it is my favorite hobby language until someone pays me use it professionally 😎

## Spec

If you have never done CSS themes before, no worries — it is surprisingly simple. We will have one button centered in the screen and the text should inform the user what theme will be activated should they click. There is no limit to the number of times the user can toggle the theme. The default theme is "Solarized Light". Typically your app would remember the users selection by storing it in a database or in local cache, but not this demo. Speaking of which, you can play with a live demo [here](https://dreamy-mcclintock-524f08.netlify.app/)! 

Note: be sure to turn off the Dark Reader Chrome extension if you have it.

## Code 

First, we need to setup our _default_ theme colors in the `:root` CSS pseudo-class (learn more [here](https://developer.mozilla.org/en-US/docs/Web/CSS/:root)) and a `.dark` class for the overrides for when the theme is changed to "Solarized Dark". 

```css
:root {
  --theme-background: #fdf6e3;
  --theme-selection-background: #ece7d5;
  --theme-foreground: #657a81;
  --theme-accent1: #2aa198;
  --theme-accent2: #b58900;
}

.dark {
  --theme-background: #002b36;
  --theme-selection-background: #073642;
  --theme-foreground: #b58900;
  --theme-accent1: #d33682;
  --theme-accent2: #268bd2;
}
```

Next, since Elm is responsible for rendering our HTML it needs to know what the current theme is. This _state_ is used to change the text in the render logic of the change theme button. So let's model our simple domain and decide on our ubiquitous language.

Option 1:
Action → _Toggle_ theme

```elm
type alias Model =
	Bool

type Msg
    = ToggleTheme Bool
```

Option 2:
Action → _Change_ theme

```elm
type Theme
    = SolarizedLight
    | SolarizedDark
	
type alias Model =
	Theme

type Msg
    = ChangeTheme Theme
```

Either option would work, but I'd argue that that a sum type is more elegant and extensible should you want to add more themes such as high contrast mode for the visually impaired, etc. So let's go with option 2 and create our button render logic.

```elm
themeButton : Model -> Html Msg
themeButton model =
    case model of
        SolarizedDark ->
            button [ class "theme-btn", onClick (ChangeTheme SolarizedLight) ]
                [ text "Toggle Light" ]

        SolarizedLight ->
            button [ class "theme-btn", onClick (ChangeTheme SolarizedDark) ]
                [ text "Toggle Dark" ]
```

The last bit relies on good old Javascript to manipulate the CSS classes on the DOM to reflect the current theme. The `.dark` class we created earlier needs to be added as an attribute to the `<body>` element so the color overrides will take effect. Elm does not allow us to directly manipulate the DOM (if you did not already know) so how do we do this?

Enter Elm [ports](https://guide.elm-lang.org/interop/ports.html) — the smart way to interact with JS (Javascript-as-a-service). Thankfully this is all the vanilla JS we need.

```javascript
app.ports.changeTheme.subscribe(theme => {
    if (theme === "") {
        document.body.classList.remove("dark");
    } else {
        document.body.classList.add(theme);
    }
})
```

One more bit... Our update function needs to broadcast the `changeTheme` message we just said we would listen for ↑ with the respective theme.

```elm
update : Msg -> Model -> ( Model, Cmd Msg )
update msg _ =
    case msg of
        ChangeTheme theme ->
            case theme of
                SolarizedDark ->
                    ( SolarizedDark, changeTheme "dark" )

                SolarizedLight ->
                    ( SolarizedLight, changeTheme "" )
```

## Conclusion

Other compile-to-Javascript languages such as ReasonReact have different philosophies about Javascript interopt. I'm not making an argument in favor of/against any right now, but hopefully this small example highlights how Elm pushes _untrusted_ code to the edges.

{% github hansjhoffman/elm-dynamic-css-theme no-readme %}