ライブラリなしで実装する定番UI 第6回 タブUI:マークアップの各要素

ウェブページでよく見かけるUIのひとつ、タブUIの実装を解説していきます。タブUIはマークアップ自体は一見難しくなさそうですが、考えるべきことは意外と多いUIです。

発行

著者 國仲 義則 フロントエンド・エンジニア
ライブラリなしで実装する定番UI シリーズの記事一覧

タブUIについて

みなさんご存じのとおり、タブUIというものはウェブブラウザやテキストエディタといったソフトウェアからウェブページ上など、さまざまな場所で使われています。

そして、それはタブと呼ばれるトリガーの集合と、それに隣接するパネル群から成ります。基本的な動作としては、タブを選択・クリックすると、該当するタブに関連付けられたパネルが表示され、それ以外のパネルは非表示になります。

タブUIは見た目の違いはあっても、一般的に動作がほとんど同じなので、実装の基本部分を押さえておけばアレンジが効きやすいUIです。タブ切り替え時に複雑なアニメーションを含んだり、他のボタン類と連動するといった特殊な動作をするタブUIもたまに存在しますが、そのような場合にも「それがタブUIであるなら、まずはこのように基礎部分を作る」という方針を立てられるので、基本的な作り方を知っていると便利です。

今回から3回に渡り、「タブが横並びになり、その直後にパネルがある」という、非常にシンプル&ベーシックなタブUIの作成を解説します。これはもっともよく見かけるパターンではないでしょうか。完成形は次のデモのようになります。

完成形のデモはWAI-ARIA Authoring Practices 1.1(2018年7月26日版)のタブUIを参考につくっていきます。

なお、ウェブサイトによっては見た目上はタブでも、実質はナビゲーションリンクであるという場合もあります。この記事においては、そういった形式のページ遷移が発生するものは除きます。また、トリガーとしての「タブ」とUIコンポーネントとしての「タブ」で紛らわしいので、記事内ではUIコンポーネントのタブは「タブUI」と呼びます。

今回のタブUI記事中では、デモは次の環境で動作確認を行いました。

  • Google Chrome 68
  • Mozilla Firefox 61
  • Internet Explorer 11 (Windows 10)
  • Microsoft Edge 42 (EdgeHTML 17)
  • Safari 11.1
  • iOS Safari 11.2
  • Android Chrome 68

タブUI全体のマークアップ

タブUIのマークアップについては『WAI-ARIAを活用したフロントエンド実装』シリーズの最終回『インタラクティブなコンテンツの実装』でも触れられており、この記事でも似たようなマークアップになります。

まずは全体を見てみましょう。次のコードでは属性ごとに改行が入っているのは見やすくするためですので、動作をつくる上では気にしなくとも問題ありません。

<div class="TabUI">
  <ul class="TabUI-tablist"
      role="tablist"
      aria-label="おすすめ記事リンク">
    <li class="TabUI-item"
        role="none presentation">
      <button type="button"
              class="TabUI-tab"
              id="tab-news"
              role="tab"
              aria-controls="panel-news"
              aria-selected="true">ニュース</button>
    </li>
    <li class="TabUI-item"
        role="none presentation">
      <button type="button"
              class="TabUI-tab"
              id="tab-popular"
              role="tab"
              aria-controls="panel-popular"
              aria-selected="false"
              tabindex="-1">人気記事</button>
    </li>
    <li class="TabUI-item"
        role="none presentation">
      <button type="button"
              class="TabUI-tab"
              id="tab-feature"
              role="tab"
              aria-controls="panel-feature"
              aria-selected="false"
              tabindex="-1">特集記事</button>
    </li>
  </ul>
  <div class="TabUI-body">
    <div class="TabUI-tabpanel"
         id="panel-news"
         role="tabpanel"
         aria-labelledby="tab-news"
         aria-hidden="false"
         tabindex="0">
      <!-- コンテンツ -->
    </div>
    <div class="TabUI-tabpanel"
         id="panel-popular"
         role="tabpanel"
         aria-labelledby="tab-popular"
         aria-hidden="true"
         tabindex="0">
      <!-- コンテンツ -->
    </div>
    <div class="TabUI-tabpanel"
         id="panel-feature"
         role="tabpanel"
         aria-labelledby="tab-feature"
         aria-hidden="true"
         tabindex="0">
      <!-- コンテンツ -->
    </div>
  </div>
</div>

タブUI全体を<div class="TabUI">で囲み、その中でUIをつくっていきます。この.TabUIにはマークアップ上の特別な役割はありませんが、タブUIを構成する各要素を一つのグループとし、最大幅を決定するという役割を持っています。

タブ部分のマークアップ

それぞれ詳しく見ていきます。まず、タブをまとめる、タブリストのマークアップです。

タブをまとめるタブリスト

<ul class="TabUI-tablist"
    role="tablist"
    aria-label="おすすめ記事リンク">
  <li class="TabUI-item"
      role="none presentation">...</li>  
</ul>

タブをまとめる<ul class="TabUI-tablist">には、role="tablilst"を付与し、タブのリストであることを示します。tablistロールは、このあとに出てくるtabロールのコンテキストに要求され、また、tablistロール自体もtabロールの所有を要求されます。WAI-ARIA Authoring Practices 1.1では、tablistロールにはラベルを指定することになっています。可能ならば指定しておくとよいでしょう。他の要素のテキストを参照するならaria-labelledbyで、そうでないならaria-labelが使えます。

ul要素の唯一の子となれるli要素である(スクリプト要素を除く)<li class="TabUI-item">ですが、li要素の初期ロールはlistitemとなっています。listitemに要求されるコンテキストロールはulの初期ロールでもあるlistロール、またはgroupロールです。

ここでは<ul ... role="tablist">により、ulのロールはすでにtablistに変更されていますので、その子であるli要素のロールがlistitemのままでは不都合が出てきます。よって、このliをアクセシビリティツリーから無視させることで、コンテキストに不都合が出ないようにします。もし親のul要素のロールがpresentationまたはnoneならばli自身のセマンティクスも失われる*のですが、この場合はそうではありません。その結果として、role="none presentation"を付与します。これにより、li要素のセマンティクスが失われ、アクセシビリティツリーから削除されます。

*注:ulのロールがnoneのとき、liのロールも同様になる

通常、noneまたはpresentationロールは継承されませんが、「必須の所有される要素」がある場合は事情が違います。下記を参照してください。

noneロールとpresentationロールの2つを属性値として指定していますが、nonepresentationも同じ役割のロールです。presentationロールは、その名前から誤解を生み、混乱の元となっているとされ、新たにnoneロールが設定されました*。しかし、noneロールをサポートしていない環境もまだ存在するので、使用する場合は併記することが推奨されています。ただし、もしnoneを使わないのならpresentationのみで問題ありません。

*注:presentationロールとnoneロールの簡単な経緯

次の記事を参照してください。

button要素をタブとして利用する

次に、タブ部分のマークアップを見てみます。

タブとなるbutton

<button type="button"
        class="TabUI-tab"
        id="tab-news"
        role="tab"
        aria-controls="panel-news"
        aria-selected="true">ニュース</button>
<!-- ... -->
<button type="button"
        class="TabUI-tab"
        id="tab-popular"
        role="tab"
        aria-controls="panel-popular"
        aria-selected="false"
        tabindex="-1">人気記事</button>

タブとなるのは<button type="button" class="TabUI-tab">です。なぜbutton要素を使うのかというと、タブに要求されるキーボードインタラクションには「EnterキーとSpaceキーで押せること」が含まれています。この動作を最初から持っているbutton要素を使用することで、自前でキーボードイベントのハンドリングを行う手間が省けます。

気を付ける点としては、button要素にtype属性を付与しないまま使うと、その属性値はsubmitになることが挙げられます。type属性を付与しないと、たとえばフォームの中に存在するタブUIなどの場合は、問題が発生するでしょう。それを回避するため、type="button"を付与し、デフォルトの動作を持たないボタンであると示します。また、ボタンのid属性値はタブパネルの属性から参照されますので、それぞれユニーク(一意)なものを指定します。

buttonをタブとして示すためrole="tab"を指定します。前述のとおり、tabロールはtablistロールによって所有されなくてはなりませんが、<ul class="TabUI-tablist">role="tablist"で要件は満たされています。

aria-controls属性では、操作対象となるタブパネルのid属性値を指定します。これにより、タブとタブパネルの関連付けが行われます。aria-controls属性は、現在の要素(ここではbutton)によって操作される対象を指定する属性です。

そしてaria-selected属性は、タブが現在選択されているのかどうかを示します。属性はtrue/false/undefinedの3種です。undefinedは選択可能ではない状態を示すものですので、今回の場合は使用しません。truefalseはそのまま選択されているかどうかです。デモ用のコードでは最初のタブが選択された状態になっています。

タブとして使用するbutton要素の最後の属性はtabindexです。この属性の扱いはなかなか難しいのですが、今回使うものは-1だけですので、その扱いさえ知っていれば大丈夫です。

tabindex="-1"を指定すると、Tabキーのフォーカス移動ではその要素に到達できなくなります。簡単に言うとスキップされます。デモのコードで言うと、1つ目のタブ以外はTabキーでフォーカスを合わせられないということになります。

選択されているタブ(ここでは最初のタブ)にはtabindex属性自体が指定されていませんが、ここでtabindex="0"を付ける必要はありません。その理由は、button要素は元からTabキーフォーカス可能な要素であり、そこにわざわざtabindex="0"を指定するのは過剰だからです。ただし、タブとして使用する要素がbuttonではなくdivliなどで、EnterキーやSpaceキーによるクリックを自前で実装するときは、tabindex="0"は必要となります。

ところで、「tabindex="-1"ではタブが選択できないではないか」と思われるのではないでしょうか。タブにフォーカスが当たっているとき、タブ同士でのフォーカスの移動は左右カーソルキーで行います。これはJavaScriptで実装する必要があります。

tabindex="-1"は本当に必要?

はじめに書いたとおり、この記事では基本的にWAI-ARIA Authoring Practices 1.1(2018年7月26日版)をベースとしてデモを作成しています。WAI-ARIA Authoring Practices 1.1のタブUIのタブにもtabindex="-1"が使用されています。

デモではフォーカス順序が

  1. タブUIの前のコンテンツ
  2. 選択されているタブ
  3. 選択されているタブに関連付けられたタブパネル → 該当するタブパネルの中身
  4. タブUIの次のコンテンツ

となります。

しかし、タブUIのタブ同士のフォーカス移動がカーソルキーで行われることを知っているユーザーはどれほどいるのか(特にウェブページ上では)という疑問があります。それを知らないユーザーはTabキーで移動できないことに非常にストレスを感じることでしょう。そのとき、ウェブサイトの所有者としても、コンテンツを読んでもらえないというデメリットが発生します。

解決法のひとつとして、タブUIに何かヘルプ文言を入れるという手がありますが、あまり現実的ではないかもしれません。タブUIが出てくるごとにヘルプ文言を読ませられるのは冗長すぎます。ヘルプボタンなどを置いたり、ツールチップで示す、などの方法もありますが、キーボードではなくマウスユーザーにとっては邪魔なだけなのでこれも悩ましいところです。または当シリーズの3回目でも紹介したライブラリwhat-inputなどを使って、ユーザーの入力ソースを監視し、タブにフォーカスが当たった時点でツールチップを表示するかどうか決定するという手もありますが、どちらにせよ実装のコストは高まります。

そういった疑問や実装コストの点から、タブのtabindex属性はあえて指定しない、という手段を選ぶ方もいます。

こればかりはアンケートを取るなどしないとユーザーが本当にタブUIを使えているのかわかりませんし、受託で「つくって納品したら終わり」というタイプの案件の場合は手の出しようがありません。こういった点にどう対処するのか、ということについては、チームやクライアントと相談して決めていくしかないでしょう。

ul > liでマークアップした理由

li要素の部分で、「ul > liでマークアップしてrole="none"するのは無駄ではないのか。わざわざnoneロールを使うなら、こういったコードでよいのでは?」と、次のように考えた方もいるでしょう。

<div role="tablist">
  <button type="button" role="tab">テキスト</button>
  ...
</div>

もちろんこれでも問題ありません。実際に、WAI-ARIA Authoring Practices 1.1の例はこのようなマークアップをしています。ではなぜ、デモのようなマークアップをしたのか、それは次のような理由からです。

あくまでも個人的な範囲になりますが「WAI-ARIAを用いたタブUIのつくり方」といった記事では、ulを使ってマークアップされている例をよく見かけるので、読む方に馴染みやすいのではないかという考えました。また、タブは等価なラベルが並んでいるのでul > liとして並べるのに違和感がないということもあります(実際には役割を上書きしてしまうのですが)。あとはスタイル指定のしやすさです。そうなると、当然「ul > liも代わりにdiv > divでもよいのでは?」となりますが、それでもよいと考えています。

タブパネルのマークアップ

次にパネル部分を見ていきましょう。前掲したコードから、関係する部分を挙げてみます。

タブパネル

<div class="TabUI-body">
  <div class="TabUI-tabpanel"
       id="panel-news"
       role="tabpanel"
       aria-labelledby="tab-news"
       aria-hidden="false"
       tabindex="0">
    <!-- コンテンツ -->
  </div>
</div>

<div class="TabUI-body">はタブパネルのグループ化のために使っています。なくても機能としては問題ありませんが、同じ役割のもの(ここではタブパネル)をグループ化しておくとわかりやすいという意味もあって、このようにしています。

<div class="TabUI-tabpanel">が、タブパネルです。id属性の値は先に触れたように、タブから参照されますので、関連付けるタブのaria-controls属性の値との差異が出ないようにしましょう。タブパネルであることを示すにはtabpanelロールを指定します。

aria-labelledby属性の値にはタブのid属性値を指定します。これにより、タブパネルのラベルに、関連付けられたタブのテキストが使用され、タブとタブパネルの相互関連付けが行われます。このことを考えると、各タブのテキストはタブパネルのラベルとして使用できるようなものにすることが望ましいでしょう。もしタブの中身が画像なら、きちんと代替テキストを提供しましょう。

aria-hidden属性は、タブパネルが表示されているかどうかを示すのに使用しています。WAI-ARIA Authoring Practices 1.1にある例ではhidden属性を使用していますが、こちらの場合はHTMLの属性としての意味を考えた結果(後述)と、「アニメーションをつけたい」などの要求に応えづらくなるので、筆者は採用していません。表示/非表示を切り替えるだけならば、直接style="display:none"をJavaScriptから指定/解除するのもよいでしょう。なお、複数のタブが選択可能なタブUIの場合、aria-hiddenでもhiddenでもなく、「aria-expanded属性で開閉状態を示すことを確認すべき(SHOULD)である」とされています。

タブパネルの最後の属性tabindexですが、使用している要素がdivであり、初期状態ではTabキーによるフォーカス移動ができません。tabindex属性により、フォーカス可能にします。ここにtabindexを指定するかどうかは制作者によるところが大きいでしょうが、WAI-ARIA Authoring Practices 1.1の例にもあるように「タブパネル内にフォーカス可能な要素がない場合などは特に、パネルへの移動の手助けとなる」という意味もあって指定しています。もちろん、タブ内にフォーカス可能な要素があってもタブパネルへの移動が便利になることに違いはありません。しかし、仕様として求められているものではありません。

また、すべてのtabindex0となっていますが、非表示状態のものがdisplay:nonevisibility:hiddenなどの場合はフォーカスが当たることはありませんので、非表示のものでも-1にする必要はありません。

hidden属性はタブUIに使ってはいけないのか?

HTML Standardのhidden属性の項目日本語訳)を読むと「タブ付きインターフェイスは単にオーバーフロープレゼンテーションの一種であるため、タブ付きダイアログでパネルを隠すためにhiddenを使用することは誤りである」となっています。しかし、WAI-ARIA Authoring Practices 1.1の例では使用されています。そして、そこにhidden属性についての説明は特にありません。

単に表示/非表示(読み上げの可否も含む)だけであれば、使っても使わなくても結果は同じであるでしょう。hidden属性が真であるとき、その要素はdisplay:noneの状態であり、CSSで強制的に表示することが可能だからです。

すべてのHTML要素はhiddenコンテンツ属性設定を持ってもよい。hidden属性は真偽属性である。要素で指定される場合、それは、要素がまだないこと、またはもはやページの現在の状態には直接関係がない、または、ユーザーが直接アクセスするのとは対照的に、ページの他の部分で再利用するコンテンツを宣言するために使用されていることを示す。

しかし、上記のHTML Standard記載の属性の意味を尊重するならば、この属性をタブパネルの表示/非表示に使うことは間違いとまでは言えなくとも、ふさわしいとは言いづらいと筆者は考えています。hidden属性の仕様が変更される、または、WAI-ARIA Authoring Practices 1.1の例で採用されなくなるまでは、マークアップする人やUI設計者の解釈や思想に依存することになるでしょう。

ここまでのまとめ

ここまでHTML部分の説明をしてきました。

タブUIのマークアップは、「WAI-ARIAを導入してみましょう」のような内容の記事で例として採用されていることも多いです。ですが、その属性の意味を知り、解釈した上で使用することが大切です。Tabキーフォーカスについても、この記事で書いたように複数の解釈が存在しています。

WAI-ARIAのサンプルとしてよく使用されており、なおかつマークアップ自体は一見そんなに難しくないタブUIですが、これまでこのシリーズでつくってきたUIと同じように、考えるべきことは意外と多いです。この記事が、要件を元にUI設計者やチームメンバーと話し合いながら、または個人としても、タブUIのベターなマークアップを行うきっかけになればと思います。

現在の状態と次回やること

最後に、この時点での状態をデモで確認してみてください。

HTMLしか書いていないので当然ですが、情報の閲覧は可能でも、見た目も機能もタブUIとして成り立っていません。次回は、タブUIらしく見えるスタイル指定と、JavaScriptによる切り替え機能を作成します。