本文へスキップ

ツリービュー

フォルダを開閉できるファイルツリー。クリックでフォルダが開き、キャレットが回転する。階層はインデントで表現し、キーボード(矢印キー)でも操作できる。

プレビュー

クリック

操作クリック/タップで動きます。

仕様書

向く場面
  • ファイル・フォルダなど入れ子の構造を見せる
  • 階層を開閉しながら1件を選ぶエクスプローラ
  • キーボードで階層を辿らせたい業務画面
避ける場面
  • 階層が1段しかない一覧(通常のリストでよい)
  • サイト全体のナビ(ツリーは選択用)
  • モバイルで横幅が足りずインデントが潰れる
構成要素
  • ツリー
  • 行項目
  • キャレット
  • アイコン
  • 名前
  • 子階層
持っている状態
  • 通常
  • ホバー
  • フォーカス
  • 押下中
キーボード

Tab でツリーへ(1行だけ tabindex=0)・↑↓ で行移動・→ で展開/子へ・← で折りたたみ/親へ・Home/End で先頭/末尾・文字キーで名前の頭文字へ・Enter/Space で開閉

支援技術

role=tree/treeitem/group、フォルダに aria-expanded、選択行に aria-selected、ローミング tabindex(APG Tree View)。折りたたみ中は visibility:hidden で除外。reduced-motion 対応

動き

子階層 grid-template-rows 0.24s ease・キャレット回転 0.2s。閉じ終わってから非表示化

デザインの要点

  • フォーカスはツリー全体で 1 行だけ tabindex=0(ローミング)。全行に止まると、20 行のツリーを抜けるのに Tab を 20 回押す
  • → は「閉じていれば開く、開いていれば子へ」、← は「開いていれば閉じる、閉じていれば親へ」の2段階。APG Tree の定石で、1キーで階層を往復できる
  • 折りたたんだ子は visibility:hidden まで切る。高さ 0 で隠しただけの行は矢印キーの移動先に混ざる
  • インデントは --depth の CSS 変数で padding-left に掛ける。ネストごとに ul を字下げすると罫線・ホバー背景が行幅いっぱいに引けない
実装メモ

WAI-ARIA の tree パターンに倣い、フォルダ(tv-node)に aria-expanded、行に role=treeitem を付けています。フォーカスは常にツリー内で1つの行だけ tabindex=0 にする「ローミング tabindex」方式で、フォーカス中の行に aria-selected を同期します。矢印キーで上下移動、→で展開/子へ、←で折りたたみ/親へ、Home/End で先頭/末尾、文字キーで頭文字の一致する行へ、Enter/Space で開閉できます。開閉アニメは grid-template-rows: 0fr→1fr。グリッドアイテムは .tv-group の1つだけにし(複数だと暗黙行が潰れず内容がはみ出す)、折りたたみ中は visibility:hidden で操作対象からも外しています。

コード(コピーして使えます)

<ul class="tv" role="tree" aria-label="ファイル">
  <li class="tv-node" role="treeitem" aria-expanded="true">
    <div class="tv-row" tabindex="0" style="--depth:0">
      <svg class="tv-caret" width="12" height="12" viewBox="0 0 12 12" fill="none"><polyline points="4,2 8,6 4,10" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
      <svg class="tv-ic tv-folder" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6a1 1 0 0 1 1-1h5l2 2h8a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z"/></svg>
      <span class="tv-name">src</span>
    </div>
    <div class="tv-children"><ul class="tv-group" role="group">
      <li class="tv-node" role="treeitem" aria-expanded="false">
        <div class="tv-row" tabindex="-1" style="--depth:1">
          <svg class="tv-caret" width="12" height="12" viewBox="0 0 12 12" fill="none"><polyline points="4,2 8,6 4,10" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
          <svg class="tv-ic tv-folder" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6a1 1 0 0 1 1-1h5l2 2h8a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z"/></svg>
          <span class="tv-name">components</span>
        </div>
        <div class="tv-children"><ul class="tv-group" role="group">
          <li class="tv-leaf" role="treeitem">
            <div class="tv-row" tabindex="-1" style="--depth:2">
              <span class="tv-caret tv-caret--empty"></span>
              <svg class="tv-ic" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3h8l4 4v14a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"/><polyline points="14,3 14,7 18,7"/></svg>
              <span class="tv-name">Button.tsx</span>
            </div>
          </li>
          <li class="tv-leaf" role="treeitem">
            <div class="tv-row" tabindex="-1" style="--depth:2">
              <span class="tv-caret tv-caret--empty"></span>
              <svg class="tv-ic" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3h8l4 4v14a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"/><polyline points="14,3 14,7 18,7"/></svg>
              <span class="tv-name">Card.tsx</span>
            </div>
          </li>
        </ul></div>
      </li>
      <li class="tv-leaf" role="treeitem">
        <div class="tv-row" tabindex="-1" style="--depth:1">
          <span class="tv-caret tv-caret--empty"></span>
          <svg class="tv-ic" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3h8l4 4v14a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"/><polyline points="14,3 14,7 18,7"/></svg>
          <span class="tv-name">index.ts</span>
        </div>
      </li>
    </ul></div>
  </li>
  <li class="tv-leaf" role="treeitem">
    <div class="tv-row" tabindex="-1" style="--depth:0">
      <span class="tv-caret tv-caret--empty"></span>
      <svg class="tv-ic" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3h8l4 4v14a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"/><polyline points="14,3 14,7 18,7"/></svg>
      <span class="tv-name">package.json</span>
    </div>
  </li>
</ul>

タグ

同じカテゴリ「ナビ・リスト操作」のほかのパーツ

関連パーツ