LIFULL Creators Blog

「株式会社LIFULL(ライフル)」の社員によるブログです。

清く正しく「サービス共通ヘッダ・フッタ」を実装する

フロントエンドエンジニアの嶌田です。今回が LIFULL Creators Blog への初めての投稿です。

「サービス共通ヘッダ・フッタ」は、ただのヘッダ・フッタではありません。ソースコードはいくつものサイトやサービスで使いまわされます。組込み先が持っている CSS によっては表示が崩れてしまうかもしれません。ブレークポイントやコンテンツの幅がそろわないかもしれません。サービス共通で使えるヘッダ・フッタには相応の強さや柔軟さが求められます。

この記事では、LIFULL HOME'S のサービス共通のレスポンシブ版ヘッダ・フッタを実装するために動員した「強く・堅牢に実装するためのノウハウ」を紹介します。

共通ヘッダー・フッターの画面キャプチャー

どこにでも組み込めるように実装する

共通ヘッダ・フッタをどんなコードベースに入れても崩れることなく表示されることは求められる品質の一つです。サービスごとに適用されている CSS がわからない中で、これを完璧に実装することは困難ですが、極力崩れにくい実装なら可能です。

重複しないクラス名ルールを設定する

クラス名が重複すると意図しないスタイルが当たってしまいます。たとえば次のような CSS と HTML があると、.container の要素には意図しないスタイルが適用されてしまいます。

/* 既存の CSS */
.container {
  color: red;
}

/* ヘッダー・フッターの CSS */
.header .container {
  ...
}
<header class="header">
  <div class="container">
    <!-- 文字色は赤になる -->
  </div>
</header>

この問題を避けるには以下に留意するとよいでしょう。

  • BEM の考え方を取り入れる
  • ユニークなブロック名にする

BEM は言わずと知れた CSS 命名規則で、一定の UI のまとまりを Block とし、Block を構成する各要素を Element として扱います。Element は必ず接頭辞として Block 名を含むため、冗長ながらも重複を避けたクラス名を付けることができます。

.header {
  ...
}
.header__container {
  ...
}

これだけだと、.header というクラス名がサービス既存の CSS で使われている可能性を捨てきれません。そのため Block 名自体のユニークさも必要です。Block 名の具体性を高めるために複数の単語を使ったり、接頭辞を付けたりして重複しない Block 名を考えましょう。

/* 複数の単語を使う */
.responsive-header { ... }

/* 共通パーツをあらわす cmn- 接頭辞を付ける */
.cmn-header { ... }

/* 使われていない大文字小文字ルールにする */
.Header { ... }

弊社のサービスの場合、大文字始まりの単語でクラス名をつけているサービスはなさそうだったので、最後の .Header の形式の命名を採用しました。

詳細度や継承とうまく付き合う

CSS セレクタの詳細度や継承(カスケード)のしくみをしっかり理解していないと、組込みの思わぬスタイルが適用されてしまいます。

分量の都合上、詳細度の解説は割愛します。詳細度設計において見過ごしがちなのは、セレクタの詳細度をどれだけ高めても、内側の要素に適用されるスタイルには勝てないということです。

a {
  color: red;
}

#header {
  color: black !important;
}
<header id="header">
  <a href="/">
    LIFULL HOME'S
  </a>
</header>

このような CSS と HTML があったとき、「LIFULL HOME'S」の文字色は赤になってしまいます。

組込みにどのような CSS が指定されているかは基本的に予期できないので、念を入れておく必要があります。念を入れるには全称セレクタ * を使うのがよいでしょう。次に示すコードでは Header 内のあらゆる要素と疑似要素について、スタイルをリセットしています。

.Header *,
.Header *::before,
.Header *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  list-style-type: none;
  color: inherit;
  line-height: 1.5;
  font-family: inherit;
  letter-spacing: 0;
  text-decoration: none;
}

注意点もあります。全称セレクタは詳細度を増やさないため、.Header * のセレクタの詳細度はクラス名をセレクタにとして単独で使う場合と同じです。そのため、ありがちな a:visited などのセレクタには負けてしまいます。

これらのケースは例外的でパターンも限られるので、ヘッダ・フッタの CSS で任意のスタイルを当ててあげましょう。

.Header a:visited {
  color: inherit;
  text-decoration: none;
}

より万全を期すのであれば、共通のヘッダ・フッタの Block にはすべて id 属性を付与し、ID 起点でセレクタを書くようにするとよいでしょう。

#Header {
  ...
}
#Header .Header *,
#Header .Header *::before,
#Header .Header *::after {
  ...
}
#Header .Header__element {
  ...
}

打ち勝ちたい気持ちが早まって !important は付けないようにしましょう。うっかり付けてしまうと、各要素にこれから指定するスタイルにもすべて !important を付けなくてはいけなくなってしまいます。

プレーンな技術を使う

組込みのサービスがどのフレームワークを採用しているのか、共通ヘッダ・フッタにとっては知る由がありません。そのため共通ヘッダ・フッタは、特定のフレームワークに依存しないコードで実装されていることが望ましいでしょう。

もし特定のフレームワークに依存してしまうと、ユーザーは利用頻度の高くない共通部分のためにフレームワーク全体をダウンロードしなければいけません。表示パフォーマンスが悪化することはサービス全体にとってよいことではありません。

jQuery・Bootstrap・React・Vue・Tailwind CSS といった JS/CSS フレームワークは使わず、プレーンなコードを書くとよいでしょう。jQuery を入れてないサービスはない! ということなら jQuery ありきのコードを書くのもありです。このあたりは現場によりけりです。

Sass や TypeScript などを開発に取り入れることは特に問題ありません。クライアントサイドでの動作には直接影響しないからです。

ブレークポイントや z-index 等をカスタマイズ可能にする

組込みのデザインに適用できるように、ある程度カスタマイズができるようにしておく必要があります。代表的なものは、ブレークポイントや z-index の値です。

ブレークポイントは、サービスによってその値はバラバラなことが多いでしょう。共通ヘッダ・フッタのブレークポイントは、サービス側が用意しているポイントと一致しているほうが望ましいです。

z-index の設計もサービスごとにまちまちです。たとえば共通ヘッダに極めて大きな z-index の値を設定したことで、サービス側が持っているモーダルダイアログ(画面全体を覆うタイプのダイアログ)の上に重なってしまうかもしれません。

これらの値をカスタマイズするために PostCSS や Sass といったプリプロセッサを使うとよいでしょう。サービスによって変動する値は変数として用意しておき、各サービスで組み込む際にはその変数の値を修正し、プリプロセッサにかけてもらうことにしましょう。次のコードは Sass(scss 記法)での例です。

/* 設定部分 */

// ブレイクポイントの設定
$mq-mobile: '(max-width: 1023.9px)';
$mq-desktop: '(min-width: 1024px)';

// z-index の設定
$zi-header: 50;
$zi-menu: 51;

/* 利用部分 */

@media #{$mq-mobile} {
  // モバイル向けスタイル
}

@media #{$mq-desktop} {
  // デスクトップ向けスタイル
}

このように準備しておくことで、サービスごとに各要素へのスタイルを微調整することなく共通ヘッダ・フッタを適用できます。変更箇所を変数のみに限定させることで、オリジナルとの差分がファイルの中に分散することがなくなり、将来のバージョンアップへの追随が簡単になります。もしレイアウトが崩れたときの責任分担も明確になります。

ちなみに LIFULL HOME'S の共通ヘッダ・フッタにはこれ以外にも次の値をカスタマイズ可能な項目として用意していました。

  • コンテンツの最大幅
  • コンテンツの左右に設ける、ウィンドウとの余白の大きさ
  • 開かれたメニューの z-index

どのような項目をカスタマイズ可能にするかは、デザイン要件や機能要件に応じて設計するとよいでしょう。

html { font-size: 62.5% } に対応する

LIFULL HOME'S 特有の要件ですが、サイトによっては html 要素に font-size: 62.5% というスタイルが宣言されていることがありました。これは rem 単位の扱いを簡便にするためのテクニックです。

font-size: 62.5% テクニックとは

これは、CSS における rem 単位の指定を簡便にするためのちょっとしたテクニックです。rem 単位は「ルート要素= html 要素が持っているフォントサイズに対する相対値」を表す単位です。ブラウザーのデフォルト文字サイズはたいてい 16px ですから、1rem16px * 62.5% * 1 = 10px となります。デザインファイルから拾った値を 10 で割るだけで CSS 上の数値に変換できるようになるため、オペレーションがちょびっと効率化するメリットがあるとされています。

ブラウザーの文字サイズ設定を尊重するために CSS の中の文字サイズは rem 単位で記述することになります。rem 単位はルート要素(= html 要素)を基準とするため、html { font-size: 62.5% } の有無によって 1rem あたりの大きさが変わってしまいます。rem 単位を使って 62.5% 指定のサイト、そうでないサイト両方に対応する必要がありました。

今回は、html 要素にかかった文字サイズを打ち消すための係数を用意し、rem 単位を使用する場面では必ずその係数をかけた値を利用するようにしました。

// HTML 要素に適用されている font-size の逆数を比率で  
// 例: html{font-size:62.5%} の場合、 1 / 0.625 = 1.6
// 例: html{font-size:12px} の場合、1 / (12 / 16) = 1.333333
$font-scale-factor: 1;

// 常に 16px になる rem 値
$rem: 1rem * $font-scale-factor;

// 補正済みの rem 単位を利用
$font-size-20: 1.25 * $rem;
$font-size-16: 1 * $rem;
$font-size-12: 0.75 * $rem;

.Header__logo {  
  // どんな環境でも 16px で表示される
  font-size: $font-size-16;
}

さらに実装品質を高める

少しマニアックな内容ですが、サービス横断的に広く使われるヘッダ・フッタですので、さらにクオリティを上げていきます。

テキスト量の変動を考慮する

テキスト量が増えたときにどうするかを決めて実装に反映します。ヘッダ・フッタの内容の多くはナビゲーションのためテキスト量の増減は考えなくてよいことが多いですが、一部必要になる箇所はしっかりと対応していきます。機械翻訳にかけるとラテン系の言語ではテキスト量が増えがちなので留意しましょう。

上部固定ヘッダとページ内リンク先の要素を被らないようにする

新ヘッダ・フッタには、ページをスクロールしていくと画面上部に固定ヘッダがニョキッと現れる仕様があります。

ハッシュ(#)付きリンクのデフォルトの挙動は、遷移先の部分がウィンドウの上端にピッタリくっつくようにスクロールされて表示されます。そのため上部に固定しているヘッダがあると、遷移先の要素と固定しているヘッダとの干渉が発生します。

ヘッダーと見出しが干渉している画面キャプチャー

うまくかぶらないように表示したいものです。大丈夫です。まさにこのために使いたい CSS プロパティがあります。scroll-margin-top です。

/* 固定ヘッダーの分だけスクロール位置を調整する */
:target {  
  scroll-margin-top: 64px;  
}

ヘッダーと見出しが干渉しなくなった画面キャプチャー

これで固定ヘッダとジャンプ先の要素が干渉しなくなりました。

複数のリセット CSS 環境下で動作確認する

リセット CSS はいくつかの種類があり、各サービスでどのリセット CSS が使われているかわかりません。リセット CSS はページ全体のスタイルに影響を及ぼすものですから、種類によってはこれまで書いてきた CSS との相性問題があるかもしれません。

リセット CSS の個数は有限ですから、いくつかメジャーに使われているリセット CSS ライブラリを実際に入れてみて、表示に崩れがないかどうかを確かめます。今回の実装では以下のリセット CSS を試してみて、問題ないことを確認しました。

リセット CSS を入れ替えて検証しておくと、ヘッダ・フッタを移植したときに表示崩れを起こす可能性はぐぐっと低くなります。

読み込み速度に配慮する

ヘッダ・フッタにはロゴ画像やアイコン画像が使われています。これらの画像は急いで読み込む必要はありません。ヘッダ・フッタは、あくまでナビゲーションや情報提供を目的としていて、正味のコンテンツに比べたら重要度は高くないからです。

SVG 画像の場合、HTML に SVG データを直接埋め込むことで外部ファイルのリクエスト数を減らすことができます。しかし述べた通りヘッダ・フッタの画像は重要度が高くなく、各ページで繰り返して登場することになります。画像は外部ファイル化し、ブラウザーのキャッシュを利用する戦略のほうがよさそうです。

画像を遅延読み込みさせるために、 loading 属性と decoding 属性が利用できます。loading 属性によって、まだ画面内に表示されていない画像の読み込みを遅延させることができます。decoding 属性によって、ダウンロードされた画像のデコード処理を非同期に行ってよいことを明示します。

<a href="https://www.homes.co.jp/search/bukken-history/">
  <span>最近見た物件</span>
  <div>
    <b>3</b>
    <img src="images/icon-clock.svg" alt="" width="24" height="24" decoding="async" loading="lazy">
  </div>
</a>

すべての img 要素に width 属性と height 属性を含めることも忘れてはいけません。画像読み込みによるリフローを抑え、高速化につながります。Core Web Vitals における CLS の削減にもなります。

アクセシビリティへの配慮

LIFULL HOME'S のフロントエンドはアクセシビリティを重要視しています。ヘッダ・フッタの実装においても、多様なユーザーやアクセス方法を受け入れられるように配慮した実装を行っています。

div ではなく button を使う

メニューを開く動作は JavaScript を使って実現しています。メニューを開閉するボタンの click イベントを購読し、イベントの発生に応じてメニューの表示・非表示を切り替えます。

メニューを開閉している画面キャプチャ―

ありがちなのが、開閉ボタンを div 要素や span 要素を使ってマークアップしてしまうケースです。マウスやタッチ操作で問題なく操作できるように思いきや、Tab キーを使ってフォーカスを当てることができずキーボードで操作できなくなってしまいます。

開閉の操作に限らず、クリック起点で JavaScript の処理が作動するような要素は button 要素を使ってマークアップする必要があります。

ところで、メニューやディスクロージャ(いわゆるアコーディオン的 UI)の開閉のために、チェックボックスと CSS を駆使して実現するチョイ技が巷にあります。これは本来の使い方ではないため、たとえ JavaScript が不要になるとしてもお勧めできません。

スキップリンク機能

スキップリンクとは、ヘッダ等サイトの共通領域をすっ飛ばして、いきなりメインエリアやナビゲーションにジャンプするためのアクセシビリティの機能です。キーボードやスクリーンリーダーを利用する人にとって有用な機能です。

画面キャプチャー 左から GitHub・Google・IBM のWebサイト上でスキップリンクを表示したところ。

LIFULL HOME'S のヘッダ・フッタにもスキップリンク機能を実装しました。スキップリンクは初期状態では隠れています。ページを開いたあとキーボードの Tab キーを押すとスキップリンクが現れます。

LIFULL HOME'S ヘッダーのスキップリンク

スキップリンクの実装は難しくありません。初期状態で見えなくしておきますが、display: nonevisibility: hidden を使うとタブフォーカスの対象からも消えてしまいますから、それ以外の手段で視覚的に見えなくします。そして :focus 疑似クラスを使い、フォーカスが当たったときだけ表示すればオーケーです。

.Header__skipLink {
  position: absolute;
  top: 0;
  left: 0;
  padding: 0.5em 1.5em;
  background-color: #ed6103;
  font-weight: bold;
  transform: translateY(-100%);
}
.Header__skipLink:link,
.Header__skipLink:visited {
  color: #fff;
}
.Header__skipLink:focus {
  transform: translateY(0%);
}

文字の視認性

文字が薄すぎたり小さすぎたりすると、文字は読みづらくなります。独断で「読めるじゃん!」と判断してしまうのは尚早です。屋外で強い日差しの下では画面が見えにくいかもしれませんし、そもそも見え方には個人差があります。

このようなデザインを見かけたら、実装する前に「待った!」をかけましょう。利用者の能力や状況が多様であることを説明し、もう少しクッキリした色使いにできないか検討してもらいましょう。

実装面でも配慮できることがあります。LIFULL の日本語部分のブランド書体は、Windows や macOS に標準搭載されている游ゴシックです。すばらしい書体ですが、Windows+Chrome の組み合わせで表示したときにレンダリングが細くなりすぎてしまう問題があります。

この問題への対処方法はたくさんのブログで多彩な解決が試みられていますが、上手に解決しているケースはあまり多くありません。

body {
  font-family: "Yu Gothic Medium", sans-serif;
}

上記は font-family にウェイト付の書体名を記述するパターン。実は仕様にない書き方で、游ゴシックが表示できているのはたまたまです。また太字にしたとき「偽ボールド」になってしまうことも特徴です。

body {
  font-family: "Yu Gothic", sans-serif;
  font-weight: 500;
}

このようにすると、body の規定のウェイトが 500(Medium)になります。「偽ボールド」にもならず良い感じです。ですが 500 という数値がマジックナンバー的になってしまうため、気付かず font-weight: normal と書いてしまうと元の木阿弥です。また、すべてのテキストのレンダリングが Medium 相当の太さになってしまうため使いづらさもあります。

Windows と游ゴシックの組み合わせにだけ調整をかけるのにベストな指定は、次のような書き方だと考えています。

@font-face {
  font-family: AdjustedYuGothic;
  font-weight: normal;
  src: local("Yu Gothic Medium");
}

@font-face {
  font-family: AdjustedYuGothic;
  font-weight: bold;
  src: local("Yu Gothic Bold");
}

.Header,
.Footer {
  font-family: AdjustedYuGothic, YuGothic, sans-serif;
}

詳細を述べると長くなってしまうので割愛しますが、上記のような指定をしておくと Windows と游ゴシックの組み合わせのみ若干太い書体が適用されるようになり、可読性が向上します。ほかのフォントや、macOS でのレンダリングには影響しません。

文字の視認性は重要ですので、使い勝手のよい UI を実装するためにもこのような細かいテクニックを知っておくとよいでしょう。

文字サイズを固定しない

ブラウザーには2種類の拡大機能があることはご存じでしょうか? おそらく馴染み深いのはズーム機能でしょう。ズーム機能は画像を含むブラウザーに表示されるあらゆるものを拡大表示する機能です。もうひとつは文字サイズの拡大機能です。Webページに表示されるテキストの大きさだけをデフォルトより大きくできます。

前者のズーム機能はブラウザーが勝手にやってくれるため、特に意識する必要はありません。後者の文字サイズ拡大機能は、意図せず無効化してしまわないように制作者は意識する必要があります。この設定を有効にしているユーザーは小さい文字を読むのが苦手なユーザーと考えられます。この設定は可能な限り尊重したほうがよいでしょう。

font-size に指定する値を rem 単位にすることで文字サイズ設定を表示に反映できます。

.Header__links dt {
  font-size: 0.75rem;  /* 16px × 0.75 = 12px */
}

文字サイズが大きくなることで文字が読めなくならないようにしておくことも重要です。要素の幅や高さを固定値にしていると、文字サイズが大きくなったときにはみ出てしまったり、ほかの要素と被って読めなくなったりしてしまいます。変動する文字サイズを前提としたコーディングはなかなか高度ですが、ぜひ身に着けておきたい技能です。

画面キャプチャー 文字サイズを大きくしたときの画面キャプチャー。デザイン通りの見た目ではなくなっているが、ユーザーの設定の通りに文字サイズが大きくなっている。

キーボード操作対応

Webサイトのアクセシビリティを高めるために最も重要なことの一つが、キーボードでも操作可能にしておくことです。キーボードで操作できるために抑えておくべきポイントは次のようなことです。

  • Tab キーを使ってフォーカスが当たる
  • どこにフォーカスが当たっているか視覚的にわかる
  • フォーカス順序が自然である

「div ではなく button を使う」の節で述べた通り、クリックを起点に何かが動くようなボタンは button 要素を使ってマークアップしましょう。button 要素は Tab キーでフォーカスを受け取る対象になり、キーボード操作可能になります。

<button type="button">
  <span>
    <span>メニュー</span>
    <img src="images/icon-menu.svg" alt="" width="24" height="24" decoding="async" loading="lazy">
  </span>
</button>

また、フォーカスを受け取っていることが視覚的にわかることも同じくらい重要です。CSS で outline: none を指定してフォーカスインジケータ(フォーカスしたときに出る枠線)を完全に非表示にしてしまっているケースにはいまだによく遭遇します。大原則として むやみな outline: none は避けてください。視覚的表現を重視するプロジェクトだと、ブラウザーデフォルトのアウトラインが表示されることを嫌い、フォーカスインジケータを非表示にすることを求められることがあります。そういうときは focus-visible ポリフィルwhat-input をつかって、キーボード操作時のみアウトラインを表示するようにしてください。

ちなみに共通ヘッダ・フッタのアウトラインは次のようなコードになりました。

/* 基本のフォーカススタイルの設定 */
.Header :focus-visible,
.Footer :focus-visible {
  outline: 2px solid;
  outline: 1px auto -webkit-focus-ring-color;
  outline-color: #005fcc;
}
.js-focus-visible .Header .focus-visible,
[data-whatintent="keyboard"] .Header :focus,
.js-focus-visible .Footer .focus-visible,
[data-whatintent="keyboard"] .Footer :focus {
  outline: 2px solid;
  outline: 1px auto -webkit-focus-ring-color;
  outline-color: #005fcc;
}

.Header__stickyBar :focus-visible {
  outline-color: #ffe680;
  outline-offset: -3px;
}
.js-focus-visible .Header__stickyBar .focus-visible,
[data-whatintent="keyboard"] .Header__stickyBar :focus {
  outline-color: #ffe680;
  outline-offset: -3px;
}

ポイントは3点です。①サービス側で focus-visible Polyfillか what-input を導入することを前提に、どちらが導入されても意図通り動くようになっています。②LIFULL オレンジを背景とする箇所はデザイナー要望で色を変えています。③Firefox でのフォーカスインジケータの視認性を確保しつつ、Chrome や Edge のデフォルトインジケータのスタイルを踏襲するようにしています。

WAI-ARIA 属性を指定する

JavaScript を使って表示や値が動的に変わる UI には、WAI-ARIA が定める属性を指定することでアクセシビリティを高められることがあります。

共通ヘッダ・フッタには何ヵ所か JavaScript で制御している箇所があります。グローバルメニューと、フッタの折りたたまれたリンク集です。どちらも共通して「クリックしたら特定の要素の表示・非表示を切り替える」という振る舞いをします。「ディスクロージャ」と呼ばれる UI パターンです。

<button type="button" aria-controls="Menu" aria-expanded="false">
  <span>
    <span>メニュー</span>
    <img src="images/icon-menu.svg" alt="" width="24" height="24" decoding="async" loading="lazy">
  </span>
</button>

<div class="Menu" id="Menu">
  <!-- メニューの中身 -->
</div>

aria-expanded 属性には開閉状態に応じて true もしくは false を切り替えるように実装します。このように aria-expandedaria-controls 属性を用いると、ディスクロージャの開閉状態をスクリーンリーダー等の支援技術に伝えられます。

これらの属性以外にも、UI の種類や状態を表現するための属性が WAI-ARIA にはたくさん定義されています。WAI-ARIA オーサリング プラクティス には動作サンプル付きで UI パターンごとの実装方法が解説されています。一度流し見しておいて、UI 実装の機会があったときに思い出してみるとよいかもしれません。

ドキュメントを書く

共通ヘッダ・フッタは必ずしも気心の知れたエンジニアが使ってくれるとは限りません。交流のないほか部署のエンジニアが組込み作業をするかもしれません。実装の仕上げに、使い方や組込み方を記した文書を残しておくことが大切です。

LIFULL HOME'S のヘッダ・フッタについても重厚なドキュメントを用意しました。内容をかいつまむとたとえば次のような内容を記載しました。

  • z-index やコンテンツ幅のカスタマイズ方法
  • CSS と JS のビルド方法
  • focus-visible Polyfillの導入方法
  • サービスごとに改変可能な箇所
  • スキップリンクを動作させるため、メインエリアに id 属性を付与すること
  • メニューの開閉をイベントで受け取るための JS API
  • 不明な点があった場合の連絡先

LIFULL HOME'S 共通ヘッダー・フッターのために用意したドキュメント全体の雰囲気

あとは社内に周知できればお仕事は終わりです。ドキュメントには連絡先などを書き添えておいて、利用してくれる人のサポート役にまわるとよいでしょう。


今回私が作成したヘッダ・フッタは、LIFULL HOME'S 注文住宅で見ることができます。今後も展開が進み、広く利用されていくものと期待しています。

高品質な実装やアクセシビリティにともに取り組んでくれる仲間を募っています。よろしければこちらのページもご覧ください。

hrmos.co

hrmos.co