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;
}
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
Option 2:
Action → Change theme
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.
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 === "") {
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.
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.
Top comments (0)