elm christmas

elm-css

โ†Previous postNext post โ†’

Your Elm code is awesome. Don't leave your CSS behind!

A 5 min read written by
Jonas Berdal

Writing code in Elm is nice. The compiler takes care of us. If something is wrong we get nice error messages, and if our program compiles it also runs. If we remove or rename a function the compiler will take us through all places we need to go to complete the changes. All of this comes from the great static type system.

As we build our web applications we use CSS for styling. CSS was a great innovation at the time of its creation, but experience has shown that as the amount of CSS grows it gets harder and harder to maintain. In the worst cases it can reach the point where even minor refactorings can be a scary experience as its hard to be sure the changes don't have unintended consequences. This has lead to the creation of libraries and tools to improve on vanilla CSS. Another approach is to do CSS in JS.

In the Elm world we have great libraries that let us do CSS in Elm. My team at NSB has been using elm-css, a library by Richard Feldman, for the last year. The experience has been very good. It extends the awesome type system of Elm to CSS, allowing us to write styles that are maintainable.

Why is it hard to make CSS typesafe?

It turns out that this is not straight forward. In CSS properties can take different types of values. Lets look at the margin property as an example:

margin: 30px
margin: auto

The corresponding elm-css looks like this :

margin (px 30)
margin auto

This means that the margin function must take an argument that can either be a length value or the auto value. One way to model this is by using custom types:

type Input
    = Px Float
    | Auto

margin : Input -> Style

The problem with this is that there exists other properties like cursor that has partially overlapping sets of legal input values:

cursor pointer
cursor auto

It is not possible to expand the Input type and use Input as argument to cursor as this would allow illegal CSS:

type Input
    = Px Float
    | Auto
    | Pointer

cursor : Input -> Style
margin : Input -> Style

--Illegal CSS
cursor (px 10)
margin pointer

To solve this with custom types we would have to create a separate Input type for each possible combination of input values. This would mean that we would have to prefix the values to avoid namespace conflicts, and that we would have to remember exactly which value combination each property accepts. Not a very nice solution.

Luckily there is a better way

Elm-css has luckily found a better way of applying types to CSS. It involves extensible records, which we wrote about yesterday, and a clever trick. Lets look at a simplificatian of the actual implementation of the px and auto functions:

px : Float ->
    { value : String
    , numericValue : Float
    , units : PxUnits
    , lengthOrAuto : Compatible
    ...
    }
px numericValue =
    { value = String.fromFloat numericValue ++ "px"
    , numericValue = numericValue
    , units = PxUnits
    , lengthOrAuto = Compatible
    ...
    }


auto :
    { value : String
    , lengthOrAuto : Compatible
    , cursor : Compatible
    ...
    }
auto =
    { value = "auto"
    , lengthOrAuto = Compatible
    , cursor = Compatible
    ...
    }

As we can see, these functions create records that symbolises CSS values. These records have a value field which contains the string that will be used to generate the inline css. This is the same for all elm-css functions that creates CSS values. The px record also contains a numericValue and units field. These are used for CSS calculations. The intersting part here is the fields of type Compatible. These encode the information about which CSS property functions we can use our CSS value records. Lets look at the actual type signatures of the margin and cursor functions.

margin : LengthOrAuto compatible -> Style

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

cursor : Cursor compatible -> Style

type alias Cursor compatible =
    { compatible | value : String, cursor : Compatible }

By using extensible record types in the signature we limit the argument records to those that are compatible with the property. Both the px record and the auto record are compatible with the margin function, but only the auto record is compatible with the cursor function. However, you might have noticed that this means we need a Compatible field for all different legal combinations in the CSS spec. Because of this some of the value records are quite large:

auto :
    { lengthOrAuto : Compatible
    , overflow : Compatible
    , textRendering : Compatible
    , flexBasis : Compatible
    , lengthOrNumberOrAutoOrNoneOrContent : Compatible
    , alignItemsOrAuto : Compatible
    , justifyContentOrAuto : Compatible
    , cursor : Compatible
    , value : String
    , lengthOrAutoOrCoverOrContain : Compatible
    , intOrAuto : Compatible
    , pointerEvents : Compatible
    , touchAction : Compatible
    , tableLayout : Compatible
    }

This is the cost of making CSS typesafe. The great thing is that this mess is mostly hidden away from the user of the library. It does however lead to documentation that can be hard to read and error messages that you need a bit of experience/knowledge to decipher:

"Elm-css error message"

This should of course not scare anyone away from trying elm-css. In my experience it really is a game changer to work with. In addition, there is work on elm-css that will heavily improve the error messages and documentation.

Of course there are other problems with implementing the entire CSS spec in Elm, like overloaded values, variable number of arguments to a property and more. If you want to learn more about how elm-css has solved these issues I recommend that you start with the links right below ๐ŸŽ…

โ†Previous postNext post โ†’