Vue Fes Japan 2019 公式サイトから学ぶ CSS コーディングの知見

この記事は [CSS Advent Calendar 2019](https://qiita.com/advent-calendar/2019/css) の 20日目の記事です。

Vue Fes Japan 2019 が開催中止になってしまい、大変残念だったのですが... :cry:
（私は Vue.js 日本ユーザーグループのコアスタッフで、今回は Web サイト制作班として少しコミットしていたのでした）

先日公式サイトのソースコードが無事公開されました :tada: 
そこで、Web サイト制作で得た CSS の知見がとてもためになったので、いくつかご紹介していきたいと思います。

## Vue Fes Japan 2019 公式サイト

公式サイト： https://vuefes.jp/2019/
GitHub： https://github.com/vuejs-jp/vuefes-2019

2019 の公式サイトも、[2018 の公式サイト](https://vuefes.jp/2018/)と同様に、Nuxt.js の[静的ファイルの生成](https://ja.nuxtjs.org/guide/#%E9%9D%99%E7%9A%84%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E7%94%9F%E6%88%90)機能を使って作成しています。

### CSS の構成について

- [normalize.css](https://www.npmjs.com/package/normalize.css) を使っている
- SCSS を使っている
- [グローバルな SCSS](https://ja.nuxtjs.org/guide/assets) を利用している
    - さらに、[Nuxt Style Resources](https://github.com/nuxt-community/style-resources-module) を利用して、SCSS の変数をグローバルで共有している
-  各コンポーネントでは [Scoped CSS](https://vue-loader-v14.vuejs.org/ja/features/scoped-css.html) を利用している

このように、「グローバルな CSS」と「閉じられた CSS（Scoped CSS）」で構成されています。

ちなみにこのプロジェクトでは UI フレームワークや CSS フレームワークは使っていません。

### コーディングするときにとても嬉しかったポイント

Web サイトデザイン主担当の沖さん（@448jp）が、**Web サイトの仕様**をドキュメント化して共有してくれました。

<img width="810" alt="スクリーンショット 2019-12-18 5.04.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236385/0848c781-9b4b-a09c-c3f0-9d90a0b54f23.png">

これがとても詳細まで定義されており、ブレークポイントごとの余白や、フォント・色などを **CSS プロパティで指定**してくださったおかげで、悩まずにすばやく実装することができました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">HTML/CSSを書く人はエスパーではないので、PSDやXDで配置されたオブジェクトのマージンがpxなのか、%なのか、vwなのかは読み解けない。ましてやブレークポイントごとに変わる場合はなお。デザイナーは見た目だけ作って終わりではなく、デザインの意図と仕様を実装者に伝えるところまで仕事としたい。</p>&mdash; 沖 良矢™ (@448jp) <a href="https://twitter.com/448jp/status/1194460426211229696?ref_src=twsrc%5Etfw">November 13, 2019</a></blockquote>

つよい... :muscle: 

私は普段デザインカンプをもらってコーディングをするとき、ブレークポイント付近のデザインをエスパーしてコーディングしたり、仕様を自分で決めたりしていたのですが、そうするとデザイナーさんと齟齬が生まれたり手戻りが発生したりして、開発に余計な時間がかかってしまうことがあります。

沖さんのようなデザイナーがいる環境というのは、なかなか恵まれていると思うのですが、Web サイトをデザインする人は詳細な仕様を明文化することで、実装側がとても助かるということを知ってもらえると良いなと思いました :pray:

## CSS コーディングの知見

### グローバルな CSS の設計について

- [vuefes-2019/src/assets/stylesheets at master · vuejs-jp/vuefes-2019](https://github.com/vuejs-jp/vuefes-2019/tree/master/src/assets/stylesheets)

CSS の各レイヤーをディレクトリやファイルごとに分類することで、どんな意味を持った CSS をどこに配置するか、共同開発する際にメンバーと認識が合うようになります。

- foundation
    - colors.scss ... 色の変数
    - reset.scss ... normalize.css によってブラウザごとの差異を整えた後に、さらにリセットしたいブラウザのデフォルトのスタイル
    - typography.scss ... タイポグラフィのスタイル
    - variables.scss ... 共通化して使える変数
- main.scss ... 各 SCSS ファイルを import しているファイル
- media_queries.scss ... メディアクエリによって要素を表示させるか否かを制御できるクラス
- utilities.scss ... 各画面で共通して使えるクラス

また、[foundation](https://github.com/vuejs-jp/vuefes-2019/tree/master/src/assets/stylesheets/foundation) 配下の CSS を開発初期の段階で固めておくことで、CSS フレームワークを使わずとも統一感のとれた CSS を書いていくことができます。

### ブレークポイントの名称は `sp` `pc` ではなく `sm` `md` `lg` を使う

私は今まで、ブレークポイントの切り替えで使う変数名の表記には `sp` `pc` を使っていたのですが、この 2パターンだと中間層のサイズのブレークポイントを書きたい際に困ることがありました。

ブレークポイントの名前をサイズの名前にすると、中間層のサイズも柔軟に表すことができるようになるのでオススメです。

https://github.com/vuejs-jp/vuefes-2019/blob/master/src/assets/stylesheets/foundation/variables.scss#L1-L4

```scss
$layout-breakpoint--is-small: 768px !default;
$layout-breakpoint--is-small-up: 769px !default;
$layout-breakpoint--is-medium: 980px !default;
$layout-breakpoint--is-medium-up: 981px !default;
```

### `sm` （モバイル）サイズの要素の指定を相対指定にする

モバイル端末の違いによって、文字やコンテンツの幅が大きく変わってしまう問題については、サイズを相対指定（`vw`）にすることで解決しています。

<img width="410" alt="スクリーンショット 2019-12-18 5.47.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236385/a932a3f8-761e-a1d4-4217-9711ada433a8.png">

<img width="340" alt="スクリーンショット 2019-12-18 5.48.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236385/1954ab28-1b1c-bf03-69f2-913232a8e97e.png">

文字やコンテンツの幅を相対指定にすることで、iPad と iPhone SE というような画面幅が違う端末でも、画崩れを起こさずに表現することができます。

ちなみにデスクトップサイズ以上の文字については、固定で指定しています（さすがにラップトップと大きいサイズのモニターでは幅が違いすぎるため）

### 計算が簡単なカードリストレイアウトの実装方法

[SPONSORS](https://vuefes.jp/2019/#the-sponsor-list-section) セクションでは、スポンサープランごとに 1列に並ぶカードの幅を変える必要がありました。

- ゴールドは 1列 4カード、シルバーは 1列 5カード... というように並ぶ
- それぞれ 20px のガター（余白）がついている

<img width="784" alt="スクリーンショット 2019-12-18 6.44.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236385/bedb51e8-2294-57e4-3765-e22aad77017f.png">

当初、これを実装するために考えていたこととしては、

- スポンサープランごとのカードの幅を計算する
- カードの `margin-right` と `margin-bottom` に 20px を設定する
- 右端に来る n 番目のカードは、 `margin-right: 0;` を設定する
  - n 番目の n の値は、スポンサープランごとに変わる

というようなことを書こうとしましたが、計算と実装が複雑になるため、別の方法をとることにしました。

計算が簡単になる方法として、**ネガティブマージンと `calc()` を使うやり方**があります。

```vue:template
    <ul
      v-for="sponsorPlan in sponsorPlansHavingSponsors"
      :key="sponsorPlan.plan"
    >
      <li class="sponsor-group" :class="sponsorPlan.plan">
        <h3 class="sponsor-plan">
          {{ sponsorPlan.name }}
        </h3>

        <ul>
          <li
            v-for="sponsor in sponsorsByPlan(sponsorPlan.plan)"
            :key="sponsor.sys.id"
            class="sponsor"
          >
            <!-- ... -->
          </li>
        </ul>
      </li>
    </ul>
```

```scss:scss
.sponsor-group {
  ul {
    display: flex; // flexbox で実装する
    flex-wrap: wrap; // アイテムがはみ出したときに折り返すようにする
    justify-content: center; // 中央揃えにする
    width: calc(100% + 20px); // 💡幅を左右のガター分プラスする
    margin: -10px; // 💡ネガティブマージンを使って余白を広めにとる
  }
  .sponsor {
    margin: 10px; // 💡ガターの半分の幅を指定する（要素が隣り合うとガターの幅になる）
    width: calc((100% / 5) - 20px); // 普通サイズ（シルバー）の Sponsor バナーの横幅
  }
}
.sponsor-group.bronze {
  .sponsor {
    width: calc((100% / 6) - 20px); // ブロンズのバナーのときの横幅
  }
}
.sponsor-group.gold,
.sponsor-group.special {
  .sponsor {
    width: calc((100% / 4) - 20px); // ゴールド、スペシャルのバナーのときの横幅
  }
}
```

まず `.sponsor-group` にネガティブマージンを使って、ガターの幅分だけ要素の幅を広めに指定します。

次に、カードの上下左右にガター分の `margin` を指定します（このときガターの半分の幅を指定することで、カードが隣り合うとガターの幅になります）

そして、カードの幅を `calc()` で計算して、flexbox で要素を並べると、簡単にカードリストが出来上がります。

ちなみにカードの幅の `calc((100% / 5) - 20px);` という部分は、

`(sponsor-group の幅 / 1列に入るカードの数) - ガターの幅`

という計算式になります。

こうするとことで、 `1列に入るカードの数` を変更すればカードの幅が変わるので、とても計算が簡単になります。

[vuefes-2019/TheSponsorListSection.vue at master · vuejs-jp/vuefes-2019](https://github.com/vuejs-jp/vuefes-2019/blob/master/src/components/TheSponsorListSection.vue)

↑のコードは TheSponsorListSection の簡略版ですが、コードだけではネガティブマージンをとっているあたりが分かりにくいと思うので、CodePen を用意しました。

<p class="codepen" data-height="265" data-theme-id="default" data-default-tab="html,result" data-user="ryamakuchi" data-slug-hash="LYExRpa" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="CSS coding for card list layout">
  <span>See the Pen <a href="https://codepen.io/ryamakuchi/pen/LYExRpa">
  CSS coding for card list layout</a> by rry (<a href="https://codepen.io/ryamakuchi">@ryamakuchi</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

https://codepen.io/ryamakuchi/pen/LYExRpa

- group
    - ネガティブマージンを `width` を使って、全体的にガターの幅分を広めにとる
- card
    - 上下左右にガターをつけて、`calc()` で幅ぴったりにする

当初考えていた、左側と下にガターをつける方法と比べると、右端に来る n 番目のカードのスタイルの上書きをするということがなくなるため、計算が簡単になります。

補足：ちなみに `calc()` は、IE 10 / IE 11 では[一部うまく機能しない場合](https://caniuse.com/#search=calc)もあるため、実装するときは気をつけてください。

## まとめ

CSS の設計をきちんと行うことで、フレームワークを使うことなく、メンテナンスしやすい＆共同開発しやすいサイト作成ができました。

もし CSS / UI フレームワークを使うことを検討しているのであれば、

[create-nuxt-app で利用できる UI フレームワークを比較する - Qiita](https://qiita.com/ryamakuchi/items/8448d4f8e697f273d16c)

も、あわせてご覧いただければ幸いです。

メリークリスマス！ :christmas_tree: