CSS だけで作るタブ切替ユーザインタフェース

:target 疑似クラスを使って、JavaScript は一切使用せず CSS だけでタブ切替のユーザインタフェースを実装してみます。

CSSタブ切替の UI は、よく使われますが、JavaScript で実装するケースが多いので、今回は全部 CSS のみで実装してみるテスト。目新しいことをやっているわけではありませんので、すでにやられている方もいるかもしれませんが気にせずいきます。

使うのは :target 疑似クラスで、IE9 以降は対応していますので、比較的動作環境も広いし、フラグメント識別子つきのリンク (要するに #hoge がついたリンクです) で、各タブを直接指定したリンクが簡単に張れるので、JavaScript でやるより楽に使い勝手のよいタブ切替が実装できる場合もあります。

CSS だけで作るタブ切替ユーザインターフェース

実際のサンプルは下記に。

最新の FirefoxGoogle ChromeSafariOpera など、:target 疑似クラスに対応したブラウザであれば問題なく動作するはず。IE は IE9 以降のみ。その他、スマートフォンなどでは動作に問題ないと思います。

HTML はシンプル

HTML 部分は簡単です。タブは ul 要素で作り、各タブの内容をそれぞれ、<div class="tab[No]"> のような形でまとめておくだけ。サンプルではわかりやすいようにと、汎用性を考えて、tab[No] みたいな id 名や class 名をつけていますが、ここは自由に変えられます。

下記に、全ソースコードを。

<!-- ↓ここからサンプル↓ -->
<div class="sample">
 
 <div id="tab01">
  <div id="tab02">
   <div id="tab03">
 
    <!-- ↓タブ↓ -->
    <ul id="tab">
     <li><a href="#tab01">W3C</a></li>
     <li><a href="#tab02">XHTML</a></li>
     <li><a href="#tab03">CSS</a></li>
    </ul>
    <!-- ↑タブ↑ -->
 
    <div class="contents">
 
     <!-- ↓タブ 1 の内容↓ -->
     <div class="tab01">
      <section>
       <h1>World Wide Web Consortium</h1>
       <p>...省略...</p>
      </section>
     </div>
     <!-- ↑タブ 1 の内容↑ -->
 
     <!-- ↓タブ 2 の内容↓ -->
     <div class="tab02">
      <section>
       <h1>XHTML</h1>
       <p>...省略...</p>
      </section>
     </div>
     <!-- ↑タブ 2 の内容↑ -->
 
     <!-- ↓タブ 3 の内容↓ -->
     <div class="tab03">
      <section>
       <h1>Cascading Style Sheets</h1>
       <p>...省略...</p>
      </section>
     </div>
     <!-- ↑タブ 3 の内容↑ -->
 
    <!-- .contents --></div>
 
   </div>
  </div>
 </div>
 
<!-- .sample --></div>
<!-- ↑ここまでサンプル↑ -->

class="sample" の直後に出てくる、div 要素の入れ子が気になるかもしれませんが、ここをタブ内の a 要素からリンクターゲットにすることで、その中身の表示を色々いじるセレクタとして利用するためのものです。

CSS も簡単

タブの内容は、わかる方にはすでにお察しの通りかもしれませんが、position: absolute; で全部を重ねて、あとは display プロパティなりでごにょごにょする感じです。今回のサンプルでは、opacity プロパティを使用していますが。

要素を重ねるため、サンプルでは下記のような指定をしています。

.sample .contents {
  position: relative;
}
.sample .contents div {
  background: #fff;
  margin: 0;
  padding: 0;
  position: absolute;
  top: 0;
}

次にタブの切替に関係する部分ですが、まずは該当箇所の CSS を抜き出してみましょう。

/* タブの表示切替関連 */
.sample .contents div {
  opacity: 0;
  z-index: 1;
}
.sample #tab li:first-child a {
  background: #333;
  color: #fff;
}
.sample .contents div:first-child {
  opacity: 1;
  z-index: 2;
}
.sample #tab01:target .contents .tab01,
.sample #tab02:target .contents .tab02,
.sample #tab03:target .contents .tab03 {
  opacity: 1;
  -webkit-transition: opacity .5s;
  transition: opacity .5s;
  z-index: 2;
}
.sample #tab01:target .contents div:not([class="tab01"]),
.sample #tab02:target .contents div:not([class="tab02"]),
.sample #tab03:target .contents div:not([class="tab03"]) {
  opacity: 0;
  -webkit-transition: opacity .5s;
  transition: opacity .5s;
  z-index: 1;
}
.sample #tab01:target #tab li a[href$="tab01"],
.sample #tab02:target #tab a[href$="tab02"],
.sample #tab03:target #tab a[href$="tab03"] {
  background: #333;
  color: #fff;
  -webkit-transition: background .5s;
  transition: background .5s;
}
.sample :not([id="tab01"]):target #tab li a[href$="tab01"] {
  background: #fff;
  color: #22aacc;
}

まず、ページが読み込まれた時点でのスタイルは下記になります。これは、フラグメント識別子がついていない状態で、ページが表示されることを想定しています。

/* まず、すべてのタブの内容を一旦見えなくしてしまいます */
.sample .contents div {
  opacity: 0;
  z-index: 1;
}
/* 初期状態で tab01 がアクティブになる想定で、そのタブが選択されている状態のスタイルを指定します。
   汎用性を考えて、:first-child 擬似クラスで指定しています */
.sample #tab li:first-child a {
  background: #333;
  color: #fff;
}
/* 同じく tab01 の内容も表示します。z-index プロパティで重ね順も一番上にしておきます */
.sample .contents div:first-child {
  opacity: 1;
  z-index: 2;
}

これで、フラグメント識別子がついていない状態でページが読み込まれた場合、tab01 がアクティブになった状態が初期表示になります。

あとは、タブをクリックして、フラグメント識別子がついた状態のスタイルを指定してきます。つまり、<a href="#tab01"> をクリックしたとき、<div id="tab01"> をターゲットにリンクが動作するわけですから、これを :target 疑似クラスで取得して、その子要素の表示を切り替えればいいわけです。

なお、transition プロパティはエフェクトのためだけですので、たいした意味はありません。

/* タブの内容について「#tab01 がターゲットになっているとき、
   .tab01 を表示する」などそれぞれ指定 */
.sample #tab01:target .contents .tab01,
.sample #tab02:target .contents .tab02,
.sample #tab03:target .contents .tab03 {
  opacity: 1;
  -webkit-transition: opacity .5s;
  transition: opacity .5s;
  z-index: 2;
}
/* タブの内容について「#tab01 がターゲットになっているとき、
   .tab01 以外は非表示する」などそれぞれ指定 */
.sample #tab01:target .contents div:not([class="tab01"]),
.sample #tab02:target .contents div:not([class="tab02"]),
.sample #tab03:target .contents div:not([class="tab03"]) {
  opacity: 0;
  -webkit-transition: opacity .5s;
  transition: opacity .5s;
  z-index: 1;
}
/* ↑ここまでで、アクティブなタブの内容は表示され、それ以外は非表示になる */
 
/* 次にタブの方。「#tab01 がターゲットになっているとき、
   href="tab01" をもつ a 要素の背景/文字色を変更する」などそれぞれ指定 */
.sample #tab01:target #tab li a[href$="tab01"],
.sample #tab02:target #tab a[href$="tab02"],
.sample #tab03:target #tab a[href$="tab03"] {
  background: #333;
  color: #fff;
  -webkit-transition: background .5s;
  transition: background .5s;
}
/* 初期状態でアクティブなタブだけ「#tab01 がターゲットなっていないとき、
   href="tab01" をもつ a 要素の背景/文字色を変更する」指定をしてアクティブ状態を解除する */
.sample :not([id="tab01"]):target #tab li a[href$="tab01"] {
  background: #fff;
  color: #22aacc;
}

以上です。ソースコード自体はとても簡単ですね。

実際に動作するサンプルは下記で確認できます。

課題とか

  1. position: absolute; を使っているため、タブコンテンツの下部にページの内容が続く場合は面倒
  2. 各タブのリンクターゲットがタブの内容を含む要素になっていないため、純粋なページ内リンクとして機能しない
  3. 上記 (1) を解決するには各タブの内容(div class="tab01" など)の方に id 名を振ってやればいいけど、それをすると、アクティブなタブのスタイルを指定したりするのが今のところ CSS だけだと無理。あと、タブの内容が多い (高さがある) と、タブをクリックする度にページが内容部分上端までスクロールするのでタブが画面外に消えちゃう

(1) は、タブの内容部分を position: absolute; で重ねてしまっているので、このタブコンテンツの下に他の要素が続くようなレイアウトで、各タブの中身の高さが固定じゃない場合、ちょっとレイアウトが大変かも (と書いても伝わりにくいかもしれませんが、気になる方はサンプルソースのタブの下に何か HTML を足して表示してみてください。タブコンテンツの下に重なってしまいます)。

個人的にはあまり好きではありませんが、タブコンテンツ自体の高さを決め打ちしてしまって、タブの内容の高さがそれ以上の場合は、スクロールバーを表示するとか、多少の工夫が必要かもしれません。

これに関しては、もうひとつの方法として、position: absolute; を使わない方法もありますので、別バージョンも後日アップします。(追記: こちらに書きました

(2) は結構問題だと思うんですが、そもそも文書として正確じゃないし、例えば、IE の古いやつとかで閲覧したときにはタブの見た目にしないで、単なる縦並びの各項目とページ内リンクのリストとして表示みたいな振り分けをしたいときできなくなっちゃいますね。

でも、(3) の問題のうち、前半のアクティブなタブに対するスタイル指定はまぁいいとして、後半は実際にやってみればわかりますがかなり使いにくいので、どうしましょうかねって感じです。

ということで、あとは下記にサンプルのファイル一式を置いてありますので、適当にいじってみてください。

関連エントリー

記事をここまで御覧頂きありがとうございます。
この記事が気に入ったらサポートしてみませんか?