DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 964,423 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Cover image for Dynamic CSS themes with Elm
Hans Hoffman
Hans Hoffman

Posted on

Dynamic CSS themes with Elm

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!

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) and a .dark class for the overrides for when the theme is changed to "Solarized Dark".

: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;
}
Enter fullscreen mode Exit fullscreen mode

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

type alias Model =
    Bool

type Msg
    = ToggleTheme Bool
Enter fullscreen mode Exit fullscreen mode

Option 2:
Action β†’ Change theme

type Theme
    = SolarizedLight
    | SolarizedDark

type alias Model =
    Theme

type Msg
    = ChangeTheme Theme
Enter fullscreen mode Exit fullscreen mode

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.

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" ]
Enter fullscreen mode Exit fullscreen mode

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 β€” the smart way to interact with JS (Javascript-as-a-service). Thankfully this is all the vanilla JS we need.

app.ports.changeTheme.subscribe(theme => {
    if (theme === "") {
        document.body.classList.remove("dark");
    } else {
        document.body.classList.add(theme);
    }
})
Enter fullscreen mode Exit fullscreen mode

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

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

                SolarizedLight ->
                    ( SolarizedLight, changeTheme "" )
Enter fullscreen mode Exit fullscreen mode

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 logo hansjhoffman / elm-dynamic-css-theme

Use CSS3 variables to dynamically change the theme.

Top comments (0)

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.