elm-cssでstyled-component(CSS in JS)を超える(?)

8 min read読了の目安(約7800字

この記事は、CSS in Elmを実現するelm-cssの解説をしていく記事になります。

記事のターゲット

  • styled-components(CSS in JS)の良さがぶっちゃけよく分かっていない方
  • JavaScript/TypeScriptのフレームワークでstyled-componentは知っているが、Elmではどのような感じか気になる方
  • CSSでこのような苦しみを味わったことがある方
    • class名のマッピングが煩わしかったり、上手くスタイルが当たらなくてイライラしたことがある
    • パラメータによってスタイルを出し分けたい時に、上手く当たらなかったことがある
    • CSSの書き方をよく忘れる

※ もちろんstyled-componentだけではなく、Scoped CSSやemtion, Sassなども比較対象に含まれます。

記事はelm-cssに対して、以下のポイントで書かれています。

  • styled-components同様に嬉しいところ
  • styled-components以上に嬉しいところ
  • styled-componentsより微妙なところ

styled-components同様に嬉しいところ

elm-cssの基本

それでは早速、elm-cssのコードと共にstyled-components同様に嬉しいところを見ていきましょう。最初の例で嬉しいポイントは以下の二つです。

  • コンポーネントとスタイルのマッピングが必要ない
  • 変数を使える
  • ローカルスコープができる

view関数では、css関数にCSSで言うプロパティと値の組みであるStyleのリストを渡すことでスタイルを行います。idやclass属性で間接的にスタイルを適用(コンポーネントとスタイルのマッピング)するのではなく、直接的にスタイルを反映させることができます。

また、Theme型のようなレコードを作り開発者にわかりやすく変数の管理をすることができます。

type alias Theme =
    { border : { default : Color }
    , background : { default : Color }
    }


theme : Theme
theme =
    let
        green =
            rgb 0 255 0

        red =
            hex "ff0000"
    in
    { border = { default = green }
    , background = { default = red }
    }


view : Model -> Html Msg
view model =
    div
        [ css
            [ width <| px 100
            , height <| px 100
            , backgroundColor theme.background.default
            , border3 (px 5) dashed theme.border.default
            ]
        ]
        []

動かした例を見るとわかりますが、インラインに書いたように見えたスタイルは、実際にはclassが衝突しないようにランダムな文字列で生成されて間接的にスタイルが適用されています。

スタイルを別で定義し、ミックスインする

この例での嬉しいポイントは、以下の二つです。

  • Sassのようにミックスインができる
  • スタイルを個別で定義できる

次の例では、underlineOnHover関数でスタイルを定義します。複数のStyleは当然リストで定義されることになりますが、それらにbatch関数を適用することで一つのスタイルにまとめることができます。mixinP関数では、underlineOnHoverをミックスインして扱うことができています。同等のスタイルがあたったpタグnotMixinPは全く同じ結果になるはずです。

underlineOnHover : Style
underlineOnHover =
    Css.batch
        [ textDecoration none
        , hover
            [ textDecoration underline ]
        ]


mixinP =
    p
        [ css
            [ color (rgb 128 64 32)
            , underlineOnHover
            ]
        ]
        [ text "hoge" ]


notMixinP =
    p
        [ css
            [ color (rgb 128 64 32)
            , textDecoration none
            , hover
                [ textDecoration underline
                ]
            ]
        ]
        [ text "hoge" ]

動かした結果は興味深い結果となりました。同じスタイルの物は同じclassにまとめ上げられ、同じclassがそれぞれのpタグに適用されていました。

スタイルを持ったコンポーネントを定義する

この例で嬉しいポイントは、以下です。

  • スタイルを持ったコンポーネント(styled-component)を定義できる
  • スタイルを持ったコンポーネントのスタイルを上書きできる

今まで定義してきた要素は振る舞いを持たず、動的なアプリケーションとしては不十分な物でした。この例では、styled関数を使うことで、スタイルを持ったコンポーネントを新しく定義することができています。buttonタグは、新たにbtnタグとして生まれ変わりました。ReactやVueなどのコンポーネントとの差異として、タグ自体に状態は持たず振る舞いやスタイルは渡した引数によってのみ変化する純粋な関数になっているので、もっとカジュアルな物であると言う印象で大丈夫でしょう。つまりpropsだけを持ち副作用を持たないコンポーネントと同義です。

更に定義したコンポーネントを再びcss関数でスタイルを上書きすることもできます。

btn : List (Attribute msg) -> List (Html msg) -> Html msg
btn =
    styled button
        [ margin (px 12)
        , color (rgb 250 250 250)
        , hover
            [ backgroundColor theme.background.default
            , textDecoration underline
            ]
        ]


view : Model -> Html Msg
view model =
    div
        []
        [ btn [ onClick DoSomething ] [ text "clickME" ]
        , btn
            [ onClick DoSomething
            , css
                [ fontSize <| px 100
                ]
            ]
            [ text "Big" ]
        ]

以下のようにスタイルがあたっていることがわかります。

引数によってスタイルを変える

最後の例の嬉しいポイントは以下です。

  • 引数によってスタイルを切り替える

classを差し替えてスタイルを当てるのではなく、もっと直接的にスタイルを変更できます。btn関数にLargeSmallのような自作型のButtonSizeの値を渡すことでスタイルを分岐しています。

type ButtonSize
    = Large
    | Small


btn : ButtonSize -> List (Attribute msg) -> List (Html msg) -> Html msg
btn buttonSize =
    let
        size =
            case buttonSize of
                Large ->
                    batch [ width <| px 100, height <| px 100 ]

                Small ->
                    batch [ width <| px 50, height <| px 50 ]
    in
    styled button
        [ margin (px 12)
        , color (rgb 0 0 0)
        , size
        , hover
            [ backgroundColor theme.background.default
            , textDecoration underline
            ]
        ]


view : Model -> Html Msg
view model =
    div
        []
        [ btn Small [ onClick DoSomething ] [ text "Small" ]
        , btn Large [ onClick DoSomething ] [ text "Large" ]
        ]

擬似クラス&メディアクエリのネスト

hover, before, afterなどのスタイルはネストして書くことができるので、Sassと同様の表現力を持っています。

styled button
        [ margin (px 12)
        , color (rgb 0 0 0)
        , size
        , hover
            [ backgroundColor theme.background.default
            , textDecoration underline
            ]
        ]

メディアクエリなども使うことができます。

global
    [ media [ only screen [ Media.minWidth (px 300) ] ]
        [ footer [ Css.maxWidth (px 300) ] ]
    ]

スタイルの削除が容易

スタイルをコンポーネントに紐づけずに定義した場合には、どこからそのスタイルが使われているか簡単に参照することができます。仮にスタイルを削除しようとした場合には、どのコンポーネントがスタイルに依存していたかを分かった上で削除作業をすることができ、急にスタイルが崩れ始めると言う負の体験は大きく減らせそうです。

styled-components以上に嬉しいところ

それでは、elm-css(CSS in Elm)が持つ独自の嬉しさを見ていきましょう。実は今まで説明してきたコードにそれは現れていました。

型安全&エディタサポート

私はCSSを書く時にいつもプロパティの名前がわからなくなったり、borderの3つの値は何でどう言う順番で何を書けばいいのだろう? marginのこの値はなんだっけ?justify-contentにはどんな値を渡せたっけ?と混乱してしまいます。

Elmでは補完が効いたり、型に間違いがあればコンパイルエラーに、使い方がわからなくてもドキュメントを参照してサンプルコードが出てきます。非常にスムーズにスタイルを記述することができます。

スタイル表記に関して、これらはElmの他の文法と何ら変わることがありません。つまり、Elmのエディタプラグインが強化されれば同時にスタイルの書き方も同時に強化されることとなります。styled-componentはハイライトや補完のサポートが強いエディタ(IDE)と弱いエディタに分かれていたりする弱点があります。

引数によってスタイルを変えるで説明をしましたが、プロパティにカスタム型を使うことができます。Large, Smallの例では、文字列型のUnionと同様の表現ですが、値を持たせたりすることもできたり持たせた値のパターンマッチもコンパイラが面倒を見てくれて非常に強力です。

設定いらず

エディタサポートとも近いですが、スタイルを書くからといって特別なプラグインなどを入れる必要はありません。単にelm-cssのライブラリを導入するだけで使えてしまうのです。style-lintやeslintなどの設定は必要ないのです。

styled-componentsより微妙なところ

全てがstyled-componentより優れているわけではありません。

CSS表記そのまま書けるわけではない

styled-componentもそうですしJSXをサポートしているわけではないので、HTML, CSSとの表記の解離はあります。どうしても生のHTML, CSSを編集したいデザイナなどがチーム構成として決まっている場合は厳しいかもしれません。しかし、コンパイラの恩恵を得たり、エディタの恩恵を得るためのトレードオフなので多くの人にとっては、しっかり向き合うことでその利点を理解してもらえるのではないでしょうか?

const H2 = styled.h2`
    font-size: ${Size.FONT.LARGE}px;
`

vendor prefixesが付かない

styled-componentsは自動的にvendor prefixesを付けてくれるようですが、そのサポートはelm-cssにはないようです。vendor prefixesをつける必要があるものに対して、以下のようなスタイルを定義しなければなりません。何かしらの工夫をしないとせっかくの型安全の恩恵などは半減してしまいそうです。

legacyBorderRadius : String -> Style
legacyBorderRadius amount =
    Css.batch
        [ property "-moz-border-radius" amount
        , property "-webkit-border-top-left-radius" amount
        , property "-webkit-border-top-right-radius" amount
        , property "-webkit-border-bottom-right-radius" amount
        , property "-webkit-border-bottom-left-radius" amount
        , property "border-radius" amount
        ]

まとめ

elm-cssの説明を通して、styled-componentの良さ、elm-css(Elm)独自の良さ、微妙なところを挙げてみました。これを機に品質の高いスタイル管理を目指してみてはいかがでしょうか?