Lism Tips

Tabs(タブ)

Tabs は、押したタブの中身だけを切り替えて見せる UI コンポーネントです。クラスの組み立て、line などの見た目、キーボード操作、縦並びの作り方をまとめます。

Tabs は、上に並んだタブ(見出しのボタン)を押すと、その中身だけが表示されるコンポーネントです。Lism が用意するのは、切り替えの動きと、ごく少しの見た目だけです。見た目は自分で整えて使う前提になっています。

どんなときに使う?#

  • 同じ場所で、いくつかの中身を切り替えて見せたいとき(「概要」「仕様」「口コミ」など)
  • React 版と HTML 版のコードのように、どれか1つを選んで見せたいとき

全部を縦に並べて、押したものだけ開くなら Accordion(アコーディオン) が向いています。

使い方#

b--tabs と l--grid を付けた箱に、ボタンを並べる b--tabs_list と、中身の b--tabs_panel を入れます。l--grid といっしょに使うことが前提です。

表示例

タブ1の中身です。

html
<div class="b--tabs b--tabs--default l--grid">
  <div class="b--tabs_list" role="tablist">
    <button class="b--tabs_tab set--plain" type="button" id="demo-tabs-1-1-tab" role="tab" aria-controls="demo-tabs-1-1" aria-selected="true" tabindex="0">タブ1</button>
    <button class="b--tabs_tab set--plain" type="button" id="demo-tabs-1-2-tab" role="tab" aria-controls="demo-tabs-1-2" aria-selected="false" tabindex="-1">タブ2</button>
    <button class="b--tabs_tab set--plain" type="button" id="demo-tabs-1-3-tab" role="tab" aria-controls="demo-tabs-1-3" aria-selected="false" tabindex="-1">タブ3</button>
  </div>
  <div class="b--tabs_panel" id="demo-tabs-1-1" role="tabpanel" aria-labelledby="demo-tabs-1-1-tab" tabindex="0"><p>タブ1の中身です。</p></div>
  <div class="b--tabs_panel" id="demo-tabs-1-2" role="tabpanel" aria-labelledby="demo-tabs-1-2-tab" tabindex="0" hidden><p>タブ2の中身です。</p></div>
  <div class="b--tabs_panel" id="demo-tabs-1-3" role="tabpanel" aria-labelledby="demo-tabs-1-3-tab" tabindex="0" hidden><p>タブ3の中身です。</p></div>
</div>
クラス 付ける場所
b--tabs 全体の箱(l--grid といっしょに付ける)
b--tabs--default 初期の見た目(選んでいないタブは灰色、選んだタブは内側に 1px の線が付く)
b--tabs--line 選んだタブに下線を引く見た目
b--tabs_list タブのボタンを横に並べる箱(role="tablist")。はみ出したら横にスクロールする
b--tabs_tab タブのボタン(role="tab")
b--tabs_panel 中身(role="tabpanel")

ボタンとパネルは、次のようにつなぎます。

  • ボタンの aria-controls に、パネルの id を書く
  • パネルの aria-labelledby に、ボタンの id を書く
  • 選んでいるボタンは aria-selected="true"・tabindex="0"、ほかは aria-selected="false"・tabindex="-1" にする
  • 選んでいないパネルには hidden を付ける

id はページの中で重ならない名前にします。動かすには、@lism-css/ui の CSS と、動きのスクリプト(dist/scripts/tabs.js)の両方を読み込みます。

下線の見た目(line)#

b--tabs--default の代わりに b--tabs--line を付けると、選んだタブの下に線が引かれます。

表示例

タブ1の中身です。

html
<div class="b--tabs b--tabs--line l--grid">
  <div class="b--tabs_list" role="tablist">
    <button class="b--tabs_tab set--plain" type="button" id="demo-tabs-2-1-tab" role="tab" aria-controls="demo-tabs-2-1" aria-selected="true" tabindex="0">タブ1</button>
    <button class="b--tabs_tab set--plain" type="button" id="demo-tabs-2-2-tab" role="tab" aria-controls="demo-tabs-2-2" aria-selected="false" tabindex="-1">タブ2</button>
    <button class="b--tabs_tab set--plain" type="button" id="demo-tabs-2-3-tab" role="tab" aria-controls="demo-tabs-2-3" aria-selected="false" tabindex="-1">タブ3</button>
  </div>
  <div class="b--tabs_panel" id="demo-tabs-2-1" role="tabpanel" aria-labelledby="demo-tabs-2-1-tab" tabindex="0"><p>タブ1の中身です。</p></div>
  <div class="b--tabs_panel" id="demo-tabs-2-2" role="tabpanel" aria-labelledby="demo-tabs-2-2-tab" tabindex="0" hidden><p>タブ2の中身です。</p></div>
  <div class="b--tabs_panel" id="demo-tabs-2-3" role="tabpanel" aria-labelledby="demo-tabs-2-3-tab" tabindex="0" hidden><p>タブ3の中身です。</p></div>
</div>

最初に開いておくタブを変える#

公式の HTML の例では、最初に開いているタブのボタンに aria-selected="true" が付き、ほかのパネルには hidden が付いています。次の例は、その形で2つめのタブを開いた状態にしています。

表示例

タブ2の中身です。

html
<div class="b--tabs b--tabs--default l--grid">
  <div class="b--tabs_list" role="tablist">
    <button class="b--tabs_tab set--plain" type="button" id="demo-tabs-3-1-tab" role="tab" aria-controls="demo-tabs-3-1" aria-selected="false" tabindex="-1">タブ1</button>
    <button class="b--tabs_tab set--plain" type="button" id="demo-tabs-3-2-tab" role="tab" aria-controls="demo-tabs-3-2" aria-selected="true" tabindex="0">タブ2</button>
    <button class="b--tabs_tab set--plain" type="button" id="demo-tabs-3-3-tab" role="tab" aria-controls="demo-tabs-3-3" aria-selected="false" tabindex="-1">タブ3</button>
  </div>
  <div class="b--tabs_panel" id="demo-tabs-3-1" role="tabpanel" aria-labelledby="demo-tabs-3-1-tab" tabindex="0" hidden><p>タブ1の中身です。</p></div>
  <div class="b--tabs_panel" id="demo-tabs-3-2" role="tabpanel" aria-labelledby="demo-tabs-3-2-tab" tabindex="0"><p>タブ2の中身です。</p></div>
  <div class="b--tabs_panel" id="demo-tabs-3-3" role="tabpanel" aria-labelledby="demo-tabs-3-3-tab" tabindex="0" hidden><p>タブ3の中身です。</p></div>
</div>

URL で最初のタブを決める#

ページの URL の後ろに ?lism-tab=<名前>-<番号> を付けて開くと、そのタブが選ばれた状態で表示されます。たとえば、id が sample-tabs-2-tab のタブを開きたいなら、?lism-tab=sample-tabs-2 を付けます。番号は 1 から数えます。

キーボードで動かす#

タブのボタンを選んでいる(フォーカスがある)とき、次のキーが使えます。

キー 動き
← → となりのタブへ移り、そのタブを開く。はしまで行くと反対のはしへ戻る
Home いちばん最初のタブへ移って開く
End いちばん最後のタブへ移って開く

b--tabs_list に aria-orientation="vertical" を付けると、使うキーが ↑ ↓ に変わります。タブを縦に並べるときに付けます。

補足

横並びのときに ↑ ↓ を使わないのは、ページのスクロールのじゃまをしないためです。また、aria-orientation は HTML の属性なので、画面の幅で横並びと縦並びが入れかわる作りでは切り替えられません。その場合は付けずに、← → のまま使います。

タブを左に縦に並べる#

b--tabs の Grid の形(grid-template)を "list panel" auto / auto 1fr にすると、タブの列と中身が横に並びます。あわせて b--tabs_list を縦並び(flex-direction: column)にします。次の例は _sm を使っているので、スマートフォンでは上にタブ、広い画面では左にタブが並びます。

表示例

タブ1の中身です。

html
<div class="b--tabs b--tabs--default l--grid -gt_sm" style='--gt_sm:"list panel" auto / auto 1fr'>
  <div class="b--tabs_list -fxd_sm" style="--fxd_sm:column" role="tablist">
    <button class="b--tabs_tab set--plain" type="button" id="demo-tabs-4-1-tab" role="tab" aria-controls="demo-tabs-4-1" aria-selected="true" tabindex="0">タブ1</button>
    <button class="b--tabs_tab set--plain" type="button" id="demo-tabs-4-2-tab" role="tab" aria-controls="demo-tabs-4-2" aria-selected="false" tabindex="-1">タブ2</button>
    <button class="b--tabs_tab set--plain" type="button" id="demo-tabs-4-3-tab" role="tab" aria-controls="demo-tabs-4-3" aria-selected="false" tabindex="-1">タブ3</button>
  </div>
  <div class="b--tabs_panel" id="demo-tabs-4-1" role="tabpanel" aria-labelledby="demo-tabs-4-1-tab" tabindex="0"><p>タブ1の中身です。</p></div>
  <div class="b--tabs_panel" id="demo-tabs-4-2" role="tabpanel" aria-labelledby="demo-tabs-4-2-tab" tabindex="0" hidden><p>タブ2の中身です。</p></div>
  <div class="b--tabs_panel" id="demo-tabs-4-3" role="tabpanel" aria-labelledby="demo-tabs-4-3-tab" tabindex="0" hidden><p>タブ3の中身です。</p></div>
</div>

専用の変数(自分で見た目を作るとき)#

b--tabs_tab では、選ばれているかどうかで中身が変わる2つの変数が使えます。マウスを乗せたとき(-hov) の set--hov で使う --_isHov・--_notHov と同じ使い方です。

変数 働き
--_isSelected 選ばれているタブでだけ、var(--_isSelected, 値) の値が効く
--_notSelected 選ばれていないタブでだけ、var(--_notSelected, 値) の値が効く

b--tabs--emboss や b--tabs--folder のように、--default・--line 以外の名前を付けると、初期の見た目(b--tabs--default の飾り)は付きません。そのため、打ち消さずに一から見た目を作れます。ただし、その見た目の CSS は自分で足します。

浮き出た形(emboss の例)#

選んだタブが、灰色の台の上で白く浮き出て見える形です。CSS を自分で足して作ります。

css
@layer lism-block {
  .b--tabs--emboss > .b--tabs_list {
    justify-self: center;
    padding: 4px;
    gap: 2px;
    background-color: var(--base-2);
    border-radius: var(--bdrs--20);
  }
  .b--tabs--emboss .b--tabs_tab {
    padding: 0.375em 0.75em;
    border-radius: calc(var(--bdrs--20) - 2px);
    background-color: var(--_isSelected, var(--base));
    box-shadow: var(--_isSelected, var(--bxsh--10));
  }
}

HTML は、b--tabs--default を b--tabs--emboss に変えるだけです。

フォルダーの形(folder の例)#

選んだタブと中身がつながって、紙のフォルダーのように見える形です。これも CSS を自分で足します。

css
@layer lism-block {
  .b--tabs--folder {
    --folder-bgc: var(--base-2);
    row-gap: 0;
  }
  .b--tabs--folder .b--tabs_tab {
    padding: 0.375em 1em;
    text-align: center;
    border-radius: var(--bdrs--20) var(--bdrs--20) 0 0;
    color: var(--_notSelected, var(--text-2));
    background-color: var(--_isSelected, var(--folder-bgc)) var(--_notSelected, transparent);
  }
  .b--tabs--folder > .b--tabs_panel {
    padding: var(--s25) var(--s30);
    background-color: var(--folder-bgc);
    border-radius: var(--bdrs--20);
    border-start-start-radius: 0;
  }
}

高さをそろえる#

タブを切り替えるたびに全体の高さが変わるのを防ぎ、いちばん長い中身に高さを合わせる方法です。b--tabs に data-tabs-keep-height="true" を付け、隠れているパネルも場所だけは取るように CSS を足します。

css
@layer lism-custom {
  [data-tabs-keep-height] > .b--tabs_panel[hidden] {
    display: block;
    visibility: hidden;
    opacity: 0;
  }
}

しくみ#

css
.b--tabs {
  grid: 'list' 'panel' / 100%;
  gap: var(--s20);
}
.b--tabs_list {
  grid-area: list;
  display: flex;
  gap: 0.25em;
  overflow-x: auto;
}
.b--tabs_tab {
  --hl: var(--hl--s);
  font-size: var(--fz--s);
  padding: 0.375em 0.5em;
}
.b--tabs_panel {
  grid-area: panel;
  width: 100%;
}
.b--tabs--default .b--tabs_tab {
  border-radius: var(--bdrs--10);
  color: var(--_notSelected, var(--text-2));
  background-color: var(--_notSelected, var(--base-2));
  box-shadow: var(--_isSelected, inset 0 0 0 1px currentColor);
}
.b--tabs--line > .b--tabs_list {
  box-shadow: inset 0 -2px 0 var(--divider);
}
.b--tabs--line .b--tabs_tab {
  box-shadow: var(--_isSelected, inset 0 -2px 0 0 currentColor);
}

b--tabs_list が横にスクロールできるとき、マイナスの margin を使うと形がくずれるので、line の下線は box-shadow(影)で描いています。

React・Astro で書く場合#

@lism-css/ui/react/Tabs(Astro は @lism-css/ui/astro/Tabs)から Tabs を読み込みます。<Tabs.Item> の中に、ボタン(<Tabs.Tab>)と中身(<Tabs.Panel>)を1組ずつ入れます。id や aria-* は自動で付きます。

jsx
import { Tabs } from '@lism-css/ui/react/Tabs';

<Tabs.Root variant="line" tabId="sample-tabs" defaultIndex={2}>
  <Tabs.Item>
    <Tabs.Tab>タブ1</Tabs.Tab>
    <Tabs.Panel>中身1</Tabs.Panel>
  </Tabs.Item>
  <Tabs.Item>
    <Tabs.Tab>タブ2</Tabs.Tab>
    <Tabs.Panel>中身2</Tabs.Panel>
  </Tabs.Item>
</Tabs.Root>
注意

<Tabs.Item> の中で認識されるのは <Tabs.Tab> と <Tabs.Panel> だけです。この2つは、必ず両方とも入れます。

Props 決めること
variant <Tabs.Root> 用。b--tabs--{variant} を付ける。初期値 default。line も使える
tabId <Tabs.Root> 用。ボタンの id({tabId}-{番号}-tab)とパネルの id({tabId}-{番号})の元になる名前。書かなければ自動で作られる
defaultIndex <Tabs.Root> 用。最初に開くタブの番号。タブの数を超えるなど、ありえない番号なら 1 になる
listProps <Tabs.Root> 用。b--tabs_list に渡す指定
index <Tabs.Tab>・<Tabs.Panel> 用。1 から数える番号。<Tabs.Item> を使わない書き方でだけ必要
tabId(子) <Tabs.Tab>・<Tabs.Panel> 用。<Tabs.Item> を使わない書き方の Astro 版でだけ必要
isActive <Tabs.Tab>・<Tabs.Panel> 用。最初に開く組に付ける。<Tabs.Item> を使わない書き方の Astro 版で使う

URL でタブを決める機能を使うときは、tabId を必ず書きます(自動で作られた名前は URL から指定できません)。縦に並べるときは、listProps={{ 'aria-orientation': 'vertical' }} で b--tabs_list に属性を渡します。縦並びの例は、次のように書きます。

jsx
<Tabs.Root gt={[null, '"list panel" auto / auto 1fr']} listProps={{ fxd: [null, 'column'] }}>
  …
</Tabs.Root>

Tabs.Item を使わない書き方#

ボタンを li で包みたいときや、ボタンの列と中身のあいだに別の要素をはさみたいときは、<Tabs.Item> を使わずに組み立てられます。<Tabs.Root> の中に <Tabs.List>(role="tablist" の箱)・<Tabs.Tab>・<Tabs.Panel> を直接置き、ボタンと中身を index の番号でつなぎます。切り替え・キーボード操作・aria-* の書きかえは、この書き方でも自動で行われます。

jsx
<Tabs.Root tabId="manual-tabs" defaultIndex={2}>
  <Tabs.List>
    <Tabs.Tab index={1}>タブ1</Tabs.Tab>
    <Tabs.Tab index={2}>タブ2</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panel index={1}>中身1</Tabs.Panel>
  <Tabs.Panel index={2}>中身2</Tabs.Panel>
</Tabs.Root>
astro
<Tabs.Root>
  <Tabs.List>
    <Tabs.Tab tabId="manual-tabs" index={1}>タブ1</Tabs.Tab>
    <Tabs.Tab tabId="manual-tabs" index={2} isActive>タブ2</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panel tabId="manual-tabs" index={1}>中身1</Tabs.Panel>
  <Tabs.Panel tabId="manual-tabs" index={2} isActive>中身2</Tabs.Panel>
</Tabs.Root>
項目 React Astro
index 各 <Tabs.Tab>・<Tabs.Panel> に必ず書く 各 <Tabs.Tab>・<Tabs.Panel> に必ず書く
tabId <Tabs.Root> の値が自動で使われる(子に書いても Root が優先) 各 <Tabs.Tab>・<Tabs.Panel> に同じ値を書く
最初に開くタブ <Tabs.Root> の defaultIndex(初期値 1) 開く組の <Tabs.Tab> と <Tabs.Panel> の両方に isActive
  • React 版では、defaultIndex を書かないときだけ、isActive を付けた <Tabs.Tab> が最初に開きます。Astro 版と同じ書き方を使えるようにするためのもので、React では defaultIndex を使うのがおすすめです。<Tabs.Panel> の isActive は React 版では要りません
  • React 版で <Tabs.Tab> に渡した onClick・onKeyDown は、中の処理より先に呼ばれます。そこで e.preventDefault() を呼ぶと、タブは切り替わりません(<Tabs.Item> を使う書き方でも同じです)
  • Astro 版のこの書き方では、<Tabs.Root> の tabId・defaultIndex・listProps は効きません。ボタンの列に渡したい指定は、<Tabs.List> に直接書きます
  • URL でタブを決める機能は、この書き方でも使えます。React 版では <Tabs.Root> に tabId を書きます
注意
  • <Tabs.Tab> は必ず <Tabs.List> の中に置きます。キーボード操作は、同じ role="tablist" の中のタブを、HTML に書いた順にたどるためです
  • この書き方では、ありえない番号を 1 に直す働きがありません。無い番号を最初のタブにすると、どのタブも開いていない状態で表示されます(タブを押せば開きます)

関連するページ#

公式ドキュメント

2026年10月5日時点の内容(lism-css 1.0.1・@lism-css/ui 0.40.1)をもとに、やさしい日本語でまとめています。

ページの一覧