Cover image for Dynamic CSS themes with Elm

Dynamic CSS themes with Elm

hansjhoffman profile image Hans Hoffman ・3 min read


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 😎


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.


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;

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 =

type Msg
    = ToggleTheme Bool

Option 2:
Action → Change theme

type Theme
    = SolarizedLight
    | SolarizedDark

type alias Model =

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.

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 — 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 === "") {
    } else {

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 "" )


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.

Posted on by:

hansjhoffman profile

Hans Hoffman


I'm a Software Engineer, budding entrepreneur, dog lover, avid reader and a 81kg Olympic Weightlifter.


Editor guide