要素を作る

styled

styled-componentsからstyled(default)をインポートして使います。styled[htmlタグ名]にはそれぞれタグ付けテンプレート構文になっているのでここに CSS を書いていくだけでスタイル付けした要素を作ることができます。実際には

  1. 定義した CSS にstyled-componentsが適当なクラス名を振る
  2. それを実際に使っている要素のclassNameに指定する

といった感じのことをやっています。

以下はスタイル付けdiv要素を作っている例です。

import styled from 'styled-components';

const StyledDiv = styled.div`
  padding: 1em;
`;

<StyledDiv>foo</StyledDiv>

作られた要素はそのまま React コンポーネントとして使うことができます。

動的スタイル・動的属性

普通の React コンポーネントがpropsを渡せるようにstyled-componentsで作った要素にもpropsを扱える仕組みがあります。それにはタグ付けテンプレート構文の中に関数を埋め込みます。関数を埋め込むとそれにはpropsが渡ってくるのでそれを好きなように触って目的の値を返すようにします。

const MARGIN = '1em';

const StyledDiv = styled.div`
  padding: ${props => props.padding || 0};
  margin: ${MARGIN};
`;

<StyledDiv padding="1em">foo</StyledDiv>
// padding: 1em;
// margin: 1em;

ちなみに埋め込みには関数以外を指定するとそのまま出力されます。

属性の場合は、attrsというメソッドがあるのでこれでpropsを受け取るコールバックを定義します。以下はtitle属性値を少し編集してる例ですが、このように好きな属性値のオブジェクトを返すようにするといいです。

const StyledDiv = styled.div.attrs(props => {
  return {
    title: `edited ${props.title}`
  }
})`
  // ...
`;

共通スタイル

styled-componentsが提供しているcssヘルパーを使うことで共通部分を分けることができます。特にpropsを扱いたいときの共通部分の切り出しには必須です。

import styled, {css} from 'styled-components';

const common = css`
  padding: ${props => props.padding || 0};
`;

const StyledFooDiv = styled.div`
  ${common};
  margin: 1em;
`;

const StyledBarDiv = styled.div`
  ${common};
  margin: 2em;
`;

テーマ

親階層でThemeProviderコンポーネントを使うことでそれ以下のすべてのstyled要素のprops.themeに値を渡せます。全体を通して一貫した動的な値を渡すことができます。

import styled, {ThemeProvider} from 'styled-components';

const StyledDiv = styled.div`
  padding: ${props => props.theme.padding || 0};
`;

const App = () => (
  <StyledDiv />
);

const Root = () => (
  <ThemeProvider theme={{padding: '1em}}>
    <App />
  </ThemeProvider>
);

<Root />

ある要素を拡張した要素を作る

styledに対して既存の Styled なコンポーネントを渡すと、その要素のスタイルに加えて当たらなスタイルを当てた新しい要素を作れます。渡す時は関数の引数として渡します。

const BaseElement = styled.div`
  font-size: 14px;
`;

const ColorOrangeElement = styled(BaseElement)`
  color: orange;
`;

上記のColorOrangeElementは、

  • タグは<div>
  • スタイルはfont-size: 14px; color: orange;

な要素になっています。

もし<div>タグも変えたい場合は Styled なコンポーネントが持つwithComponentメソッドで変えることができます。例えば以下のようにすると<span>に変更できます。

const ColorOrangeElement = styled(
  BaseElement.withComponent('span')
)`
  color: orange;
`;

スコープ付き CSS のように使う

styledの引数に React コンポーネントを渡すとレンダリング時、そのプロパティに Styled Components がランダムに生成したclassNameが渡されます。

const Foo = props => {
  return <div className={props.className}>...</div>
};

const StyledFoo = styled(Foo)`
  color: orange;
`;
// ...

コンポーネントの構造が複雑な時、それら全てにclassNameをクラスに付与することで.scope.my-classNameのようなスコープを付きのセレクタでスタイル付けできるようになります。

またその時styledの中で.scope&と書くことができるので、実際には&.my-classNameと書くことができます。

const Foo = props => {
  return (
    <div className={`${props.className} first`}>
      <div className={`${props.className} second`}>
        <div className={`${props.className} third}`}>
          ...
        </div>
      </div>
    </div>
  );
};

const StyledFoo = styled(Foo)`
  &.first {
    color: orange;
  }

  &.second {
    width: 980px;
    margin: 0 auto;
  }

  &.third {
    display: inline-flex;
  }
`;

メディアクエリのヘルパーを作る

cssをラップするとメディアクエリが楽に書けるようになります。

css

この関数は、styled[element]テンプレートリテラルの中でさらに同じような書き方で書いたスタイルを埋め込みたい時につかいます。共通部分のスタイルを外部にまとめたいときとか便利なやつです。

import styled, {css} from 'styled-components';

const margin = css`
  ${props => props['data-margin'] || '10px'};
`;

const Div = styled.div`
  margin: ${margin};
`;
Edit styled-components の css

ラップしよう

テンプレートリテラル関数

以下はテンプレートリテラル関数の例です。戻り値はcssの戻り値と同じです。なので${desktop...}...はスタイル)という感じで使うことができます。しかもそのスタイルはメディアクエリで囲まれた状態になります。

const desktop = (first, ...interpolations) => css`
  @media (max-width: ${sizes[label]}px) {
    ${css(first, ...interpolations)}
  }
`;

まとめて定義

以下はオブジェクトの情報をまとめて、反復処理することでまとめて定義しています。これはmedia.desktopmedia.tabletといった形で使うことができます。

import {css} from 'styled-components';

const sizes = {
  desktop: 992,
  tablet: 768,
  phone: 576,
};

const media = Object.keys(sizes).reduce(
  (acc, label) => {
    acc[label] = (first, ...interpolations) => css`
      @media (max-width: ${sizes[label]}px) {
        ${css(first, ...interpolations)}
      }
    `;

    return acc;
  },
  {},
);

TypeScript

ちなみに TypeScript で書こうとするとこのようになります。

import {css, CSSObject, SimpleInterpolation} from 'styled-components';

const sizes: {[index: string]: number} = {
  desktop: 992,
  tablet: 768,
  phone: 576,
};

const media = Object.keys(sizes).reduce(
  (acc, label) => {
    acc[label] = (
      first: TemplateStringsArray | CSSObject,
      ...interpolations: SimpleInterpolation[]
    ) => css`
      @media (max-width: ${sizes[label]}px) {
        ${css(first, ...interpolations)}
      }
    `;

    return acc;
  },
  {} as {[index: string]: Function},
);
Edit メディアクエリのヘルパー

<body> や <html> などラップできない領域のタグのスタイルを書く

createGlobalStyleを使います。これはタグテンプレートになっていて、 CSS を書いて実行するとGlobalStyle(名前はなんでもいい)をコンポーネントを返します。これはレンダリング時にタグテンプレートに渡した CSS を<head>領域に追加してくれます。

createGlobalStylestyled-componentsから取り込めます。

import {createGlobalStyle} from 'styled-components';

あとはstyled.divのあとのようにスタイルを書くだけです。コンポーネントの中(children)には何も入れてはいけません。

const GlobalStyle = createGlobalStyle`
  html,
  body {
    height: 100%;
  }
`;

() => (
  <>
    <GlobalStyle />
    <ReduxProvider store={store}>
      <App />
    </ReduxProvider>
  </>
);

スナップショットテスト

スナップショットテストをする時は、そのままでは毎回ランダムなクラス名が設定され2回目以降毎回失敗するようになってしまいます。

Jest 環境ではjest-styled-componentsを使う事で、Styled Components が生成したクラス名を静的にレンダリングされた結果から取り除いてくれます。

使うにはまずこれをインストールし、

yarn add -D jest-styled-components

テストファイルでただ読み込みます。

import Foo from './foo-component';
import 'jest-styled-components';

test(...args);

スタイルテスト

jest-styled-componentsモジュールを使うことで「このコンポーネントはcolororangeで持ってるはず」をテストできます。

使うにはスナップショットの時などと同じくjest-styled-componentsをただインポートすれば、toHaveStyleRuleというマッチャーが使えるようになります。

ここでは@testing-library/reactと共に使い方を見ていきます。

使い方

toHaveStyleRulesは styled-components のルートとなる要素に対して使用するマッチャーです。

基本

@testing-library/reactを使っているので軽く説明すると、以下のコードは`、

  1. queryByRoleによって<button>要素を取得
  2. その要素に対してマッチャー使用

をしています。

import React from "react";
import styled from "styled-components";
import { render } from "@testing-library/react";
import "jest-styled-components";

const Button = styled.button`
  color: orange;
`;

test("the color of the component is orange", () => {
  const renderResult = render(<Button />);

  expect(
    renderResult.queryByRole("button")
  ).toHaveStyleRule("color", "orange");
});

<button>要素に対しての#toHaveStyleRule("color", "orange")によって、この要素がcolor: orangeというスタイルを持っている事がテストできます。そしてこのテストはパスします。

またもし、色の具体地は重要ではなくcolorを持っていることが重要というような場合、Jest のexpectオブジェクトのexpect.anyなどは使えます。 例えば、以下のようにcolorに何かしらの色値が入ってることをテストできます。

expect(renderResult.queryByRole("button")).toHaveStyleRule(
  "color",
  expect.any(String)
);

スコープ使用なコンポーネントでテスト

スコープを使用して1つのstyled-componentsで複数の要素に対してスタイル付けする事があります。上記のコードをスコープ使用な形に書き換えました。
こちらはスタイルをbutton { }というネスト構造で書いてます。

import React from "react";
import styled from "styled-components";
import { render } from "@testing-library/react";
import "jest-styled-components";

const Button = styled(({ className }) => {
  return <button className={className} />;
})`
  button {
    color: orange;
  }
`;

test("the color of the component is orange", () => {
  const renderResult = render(<Button />);

  expect(
    renderResult.queryByRole("button")
  ).toHaveStyleRule("color", "orange", {
    modifier: "button"
  });
});

この場合は3つ目の引数にmodifierを持ったオブジェクトを渡します。ここへセレクタを渡すことで、深いネストのセレクタの要素のスタイルでも問題なくテストできます。

メディアクエリ持ちのテスト

レスポンシブなスタイルにも対応してます。下記はスマホではcolor: blue、PC ではcolor: orangeであることをテストしてます。

import React from "react";
import styled from "styled-components";
import { render } from "@testing-library/react";
import "jest-styled-components";

const Button = styled(({ className }) => {
  return <button className={className} />;
})`
  @media (max-width: 767px) {
    button {
      color: blue;
    }
  }

  @media (min-width: 768px) {
    button {
      color: orange;
    }
  }
`;

test("the color of the component is orange", () => {
  const renderResult = render(<Button />);
  const button = renderResult.queryByRole("button");

  expect(button).toHaveStyleRule("color", "blue", {
    media: "(max-width: 767px)",
    modifier: "button"
  });

  expect(button).toHaveStyleRule("color", "orange", {
    media: "(min-width: 768px)",
    modifier: "button"
  });
});

3つ目の引数のオブジェクトにmediaというプロパティを増やし、値を確認したいメディアクエリと同じ値にするだけです。

複雑なコンポーネントでテスト

ここでいう複雑なというのは、ネストしたセレクタにstyled-componentsを使ってるようなケースを指してます。例えば、

const Span = styled.span``;

const Button = styled.button`
  ${Span} {
    color: orange;
  }
`;

のようなケースです。全体像は以下です。

import React from "react";
import styled, { css } from "styled-components";
import { render } from "@testing-library/react";
import "jest-styled-components";

const Span = styled.span``;

const Button = styled(({ className }) => {
  return (
    <button className={className}>
      <Span />
    </button>
  );
})`
  ${Span} {
    color: orange;
  }
`;

test("the color of the component is orange", () => {
  const renderResult = render(<Button />);
  const button = renderResult.queryByRole("button");

  expect(button).toHaveStyleRule("color", "orange", {
    modifier: css`
      ${Span}
    `
  });
});

このようなケースもスコープ使用なコンポーネントのルート要素からmodifierを通してテストできます。その場合はstyled-componentsが提供するcssTagged template を用いてセレクタを書きます。

リポジトリ

上記の内容はこのリポジトリをローカルにgit cloneし、yarn && yarn testと叩くことで実際にテストを走らせれます。