An Argument for Elm/Html

What's the best HTML/CSS library in Elm? mdgriffith/elm-ui? rtfeldman/elm-css? Both improve upon elm/html. In this article I will show why elm/html is not only a good choice but maybe even the best of the three. TLDR; elm/html is simpler and more flexible.

I will first start by making an argument against Elm-UI. Next I'll show why Elm-CSS isn't helping, and finally I will present a small trick to make elm/html shine (It's your IDE). The article ends with an acknowledgment, that the presented approach isn't the holy grail. I invite you to also participate in the discussion over on discourse.

Elm-ui: Better than HTML. Or is it?

Elm-UI is a new way to layout HTML and CSS, combining both into one coherent experience.

Element.el [Element.centerY] (Element.text "Im centered!")

Now, here is a Question: How do you center an element vertically without using Elm-UI?

I admit, I'd forgotten myself after using Elm-UI for a while. But that's the problem. The more we use Elm-UI, the harder it is to work without it.

But sometimes we have to. For example, when using web-components.

Html.node "fancy-card" [] [Html.text "Title"]
    |> Element.html
    |> Element.el [Element.Font.size 64 ]

The issue comes when we try to replace the text inside with Elm-UI.

let
    content =
      Element.layoutWith {options : [Element.noStaticStyleSheet]} []
        (Element.text "Title")
in
Html.node "fancy-card" [] [content]
    |> Element.html
    |> Element.el [Element.Font.size 64 ]

If we run this code, we'll see that Element.Font.size 64 isn't effecting the content. This is a side effect of the magic that makes Elm-UI work. Here's an Ellie to see for yourself.

Elm-UI might be nice to use, but it can't be combined with web components. if we need web components, using elm/html is our only option.

Elm-CSS : Typed CSS. But at what cost?

Elm-CSS allows us to write type safe CSS. It isn't as pleasant as Elm-UI, but it's still amazing. It also plays nice with elm/html.

Html.Styled.div 
    [ Html.Styled.Attributes.css
        [ Css.displayFlex
        , Css.alignContent Css.center
        ] 
    ] 
    [ Html.Styled.text "Im centered!"
    ] 
    |> Html.Styled.toUnstyled

The compiler tells us if the produced CSS isn't correct. For example, let's say we forgot to add Css.px.

Css.height 100 --correct solution: Css.height (Css.px 100)

The compiler correctly points out that this isn't correct.

The 1st argument to `height` is not what I expect:

19|         , Css.height 100
                         ^^^
This argument is a number of type:

    number

But `height` needs the 1st argument to be:

    Css.LengthOrAuto compatible

Ok, so let's check the type definition of LengthOrAuto.

type alias LengthOrAuto compatible =
    { compatible | value : String, lengthOrAuto : Compatible }

Don't worry if you haven't seen such a type before. It's some black magic called extendable records. In order for Elm-CSS to be type safe, it has to use some advanced type trickery. The compiler error isn't actually helpful at all. We have to check the mdn web docs to see all possible values of for height (like 100px).

Ok, but reading the mdn docs for height, we see that height actually supports more values, for example: height: fit-content.

Css.height Css.fitContent

It's valid CSS, but Elm-CSS actually does not like that.

The 1st argument to `height` is not what I expect:

19|         , Css.height Css.fitContent
                         ^^^^^^^^^^^^^^
This `fitContent` value is a:

    Css.MinMaxDimension {}

But `height` needs the 1st argument to be:

    { compatible | lengthOrAuto : Css.Compatible, value : String }

That's because it's a new CSS feature added in 2024 and not yet supported in Elm-CSS. So we'll have to wait until a new version of Elm-CSS comes out. It's btw. a major change, so it might take some time. Until then, the compiler will tell us that it isn't valid CSS, even though it's already supported on all major browsers.

While Elm-CSS checks if our CSS is syntactically correct, is can't help us fix our errors. At the end of the day, it's a question of taste: Are we willing to trade type safety for good compiler errors? Is plain old elm/html really that bad?

Elm/Html: Old but Good

Choosing Elm/html over Elm-UI feels like going back to the Stone Age. But let's step back for a moment and look at the problems that we are actually trying to solve.

  • Better primitives. (Element.el, Element.centerY)

  • Avoid typos in your CSS. (height: 100)

My argument is that it's actually not hard to just write those primitives ourselves.

At its core Element.el is a flexbox div with a single child.

el : List (Attribute msg) -> Html msg -> Html msg
el attrs content =
    Html.div
        (Html.Attributes.style "display" "flex"
            :: attrs
        )
        [ content ]

Using Flexbox we can translate Element.centerY into align-content: center.

centerY = Html.Attributes.style "align-content" "center"

Sadly, it's not that easy.

When combined with flex-direction: column our solution will not align horizontally, but vertically instead. In that case, we want to use justify-content: center.

But the nice thing is, everyone is using Flexbox.

Quickly googling or checking a guide for flexbox will tell us how to center things. We even have a little toolbox directly in our Chrome DevTools to play around with.

Nowadays you even have a little toolbox directly in your chrome devtool to play around with all possiblities

So my argument for simpler primitives is: Write them yourself and let flexbox help you write better CSS.

Let your IDE help you avoid typos

How can we prevent typos without using elm-CSS?

widthPx : Float -> Attribute msg
widthPx float =
    Attribute.style "width" (String.fromFloat float ++ "px")

widthPx 100 --(No need for Css.px)

Writing this function doesn't take long, but it will save so much time. And if our project uses rem instead, then we can easily copy and adapt the code.

We can do the same for justify-content and align-content.

justifyContentCenter : Attribute msg
justifyContentCenter =
    Html.Attributes.style "justify-content" "center"

alignContentCenter : Attribute msg
alignCententCenter =
    Html.Attributes.style "align-content" "center"

Our IDE will help us find these functions. Typing “center” in VS Code will suggest them in a dropdown. So it's actually not that really useful.

Using suggestions in our favor, we can go even further and add more variants.

justifyContent : String -> Attribute msg
justifyContent =
    Html.Attributes.style "justify-content"

justifyContentCenter = justifyContent "center"
justifyContentFlexStart = justifyContent "flex-start"
justifyContentFlexEnd = justifyContent "flex-end"

Now, when typing “justifyContent” it will propose those functions. We can just select the correct one and press enter.

It's not type safety. But it keeps typos out of our CSS.

All these functions can live in a single file. It can live directly in our repository, or we can publish it if we think that the community would benefit from it. Different people will want different primitives — so my file will be different to your file.

I am using this approach for two years now, and I haven't looked back.

Edit:
The discussion that followed lead into me publishing elm-html-style. It's an autogenerated file containing all CSS functions with variants. Might be worth checking out.

Conclusion

In this article, I have presented my arguments for a simpler solution. Both Elm-UI and Elm-CSS aren't bad, they are actually really great. But the benefits come with a cost.

I know that my arguments aren't waterproof. Both Elm-UI and Elm-CSS use stylesheets, and there are things that we can't do without using a stylesheet (like styling button). I have an answer for that, but It will have to wait for now. Subscribe if you don't want to miss it.

Thanks for reading this far. What's your opinion. Are you doing something similar? Do you have a better solution? Perhaps one of my arguments is wrong?

Let's discuss it over at Elm Discourse.