styled-componentsが反映されないときは詳細度を調べてみよう

React + TypeScriptで作られたコードにstyled-componentsを適用する作業を進めている。基本はSCSSファイルで指定されたデザインをそのままstyled-componentsに移すだけなのだが、正しく移植したはずなのにデザインが乱れることが最近よく起こっていた。

//元のscssファイル
input.hoge {
    float: left;
    width: 50%;
    margin-right: 10px;
}
//styled-components
const Hoge = styled.input`
    float: left;
    width: 50%;
    margin-right: 10px;
`;

開発者ツールで見てみると、外部のCSSライブラリでもinput要素のデザインが指定されていた。

input[type=text] {
    width: 100%;
    ....
}

そして、SCSSファイルを使った場合は、SCSSファイル内で定義した width: 50% が正しく反映されるが、styled-componentsにした場合は外部CSSライブラリの width: 100% が反映されてしまうことがわかった。

これは、styled-componentsでは通常 .123abc のようにランダム生成されたクラス名1つによる指定として style タグをhtmlに挿入するためである。これに対し、今回のSCSSファイルでは input.hoge のように、クラス名のみならずinput要素も指定されていた。このため、styled-components化したことでCSS要素指定の詳細度が下がり、外部ライブラリの input[type=text] の方が優先されてしまっていた。
現にCSSの詳細度を計算できるサイトで調べてみると、外部CSSファイルとSCSSファイルの詳細度は同じ(->グローバルな外部CSSファイルよりも特定クラスが対象のSCSSファイルが優先される)なのに対し、styled-componentsの場合だけ詳細度が下がっていることがわかる。

f:id:Udomomo:20191106145130p:plain

対応策としては、まずstyled-components内で & を複数使い、クラスを重ねて指定する方法がある。この方法はstyled-componentsの公式ドキュメントにも載っている。

www.styled-components.com

//styled-components
const Hoge = styled.input`
    && {
        float: left;
        width: 50%;
        margin-right: 10px;
    }
`;

上記のようにすると、 & 1つごとにクラス名の指定1つに変換され、コンパイル後の style タグ内では .123abc.123abc のように、同じクラス名で二重に指定するようになる。& を3つ並べればクラス名を三重に指定できる。これはCSSの詳細度を上げるためによく使われるテクニックで、SCSSファイル内でもこの記法を使うことができる。

もう1つの方法として、ターゲットの要素の周囲にWrapper要素を作り、Wrapperのstyled-componentsの中で子要素としてターゲット要素のデザインを指定することもできる。

stackoverflow.com

ただ、マークアップ文書の中にデザイン目的のみの要素が混じってしまうので、個人的にはこの方法はあまり使いたくない。