Docs
Search

Tabs

This page is currently under construction
Lism UI (@lism-css/ui) is still in preparation.

A component for creating tab elements.
Only interaction behavior is provided — visual styling is intentionally left for you to customize.

Overview

↓
Preview

Tab 01: Lorem ipsum dolor sit amet. Consectetur adipiscing elit, sed do eiusmod tempor Incididunt ut. Labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut.

<Tabs.Root>
<Tabs.Item>
<Tabs.Tab>Tab 1</Tabs.Tab>
<Tabs.Panel>
<DummyText pre="Tab 01: "/>
</Tabs.Panel>
</Tabs.Item>
<Tabs.Item>
<Tabs.Tab>Tab 2</Tabs.Tab>
<Tabs.Panel>
<DummyText length="l" offset={1} pre="Tab 02: " />
</Tabs.Panel>
</Tabs.Item>
<Tabs.Item>
<Tabs.Tab>Tab 3</Tabs.Tab>
<Tabs.Panel>
<DummyText length="l" offset={2} pre="Tab 03: " />
</Tabs.Panel>
</Tabs.Item>
</Tabs.Root>

Only <Tabs.Tab> and <Tabs.Panel> are recognized as children of <Tabs.Item>. Both must always be used together.

Styles

The Tabs base styles are defined in the following CSS.

_style.css
@layer lism-block {
  /* l--grid との併用が前提 */
  .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_tab[aria-selected='true'] {
    --_notSelected: ;
  }
  .b--tabs_tab[aria-selected='false'] {
    --_isSelected: ;
  }
  .b--tabs_panel {
    grid-area: panel;
    width: 100%;
  }

  /* 既定バリアントの装飾。variant指定時は適用されないため、打ち消し不要で独自スタイルを組める */
  .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);
  }

  /* line バリアント: 選択中のタブを下線で表示 */
  .b--tabs--line > .b--tabs_list {
    /* Memo: ov-x='auto' の時に margin がネガティブだと崩れるので、box-shadowで表現する。 */
    box-shadow: inset 0 -2px 0 var(--divider);
  }
  .b--tabs--line .b--tabs_tab {
    box-shadow: var(--_isSelected, inset 0 -2px 0 0 currentColor);
  }
}

The full source code is available on GitHub.

Usage

Provided as Tabs from the @lism-css/ui package.

Import

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

Props

Prop Description
<Tabs.Root>
variant
Outputs the b--tabs--{variant} class. Defaults to default, and line is also provided out of the box. When you specify your own variant, the default variant’s decoration (b--tabs--default) no longer applies, so you can build your own styles without resetting the initial ones.
<Tabs.Root>
tabId
A string ID used to generate each tab button’s id ({tabId}-{index}-tab), each panel’s id ({tabId}-{index}), and the value of aria-controls. If omitted, an ID is generated automatically.
<Tabs.Root>
defaultIndex
Specifies which tab to open initially. Values out of range (e.g. a number greater than the number of tabs) fall back to 1.
<Tabs.Root>
listProps
Props passed to the list element (b--tabs_list) wrapping the tab buttons.
<Tabs.Tab> <Tabs.Panel>
index
A tab number starting from 1. Specify the same number on the corresponding <Tabs.Tab> and <Tabs.Panel>. In a normal <Tabs.Item> composition this is set automatically, so you don’t need to specify it. It’s required when composing without <Tabs.Item>.
<Tabs.Tab> <Tabs.Panel>
tabId
The ID used to generate the id ({tabId}-{index}-tab / {tabId}-{index}). In a normal <Tabs.Item> composition, the value from <Tabs.Root> is set automatically, so you don’t need to specify it. When composing without <Tabs.Item>, this is only required in the Astro version.
<Tabs.Tab> <Tabs.Panel>
isActive
Specifies whether this tab should be open initially. In a normal <Tabs.Item> composition, this is set automatically based on defaultIndex, so you don’t need to specify it. When composing without <Tabs.Item> in the Astro version, set this on the pair of <Tabs.Tab> and <Tabs.Panel> that should be open initially.

Keyboard interactions

When a tab button has focus, the following keyboard interactions are available.

Key Action
← → Moves focus to the previous or next tab and selects it, looping to the opposite end when it reaches the first or last tab.
Home Moves to and selects the first tab.
End Moves to and selects the last tab.

For a vertical tab list, pass aria-orientation="vertical" through listProps. The arrow keys then switch to ↑ and ↓.

<Tabs.Root listProps={{ 'aria-orientation': 'vertical' }}>

A horizontal tab list (the default) deliberately ignores ↑ and ↓ so that those keys keep their normal browser scrolling behavior.

Note that aria-orientation is an HTML attribute, so its value cannot change with the viewport width. For layouts that switch between horizontal and vertical responsively, keep the default (horizontal, ← →).

Examples

variant=“line”

Specify variant="line" for a line-style tab list that underlines the selected tab.

↓
variant='line'

Tab 01: Lorem ipsum dolor sit amet. Consectetur adipiscing elit, sed do eiusmod tempor Incididunt ut. Labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut.

<Tabs.Root variant="line">
<Tabs.Item>
<Tabs.Tab>Tab 1</Tabs.Tab>
<Tabs.Panel>
<DummyText pre="Tab 01: " />
</Tabs.Panel>
</Tabs.Item>
<Tabs.Item>
<Tabs.Tab>Tab 2</Tabs.Tab>
<Tabs.Panel>
<DummyText length="l" offset={1} pre="Tab 02: " />
</Tabs.Panel>
</Tabs.Item>
<Tabs.Item>
<Tabs.Tab>Tab 3</Tabs.Tab>
<Tabs.Panel>
<DummyText length="l" offset={2} pre="Tab 03: " />
</Tabs.Panel>
</Tabs.Item>
</Tabs.Root>

Default open tab

Use defaultIndex on <Tabs.Root> to specify which tab is open by default.
(Index numbers start from 1.)

↓
Example

Tab 02: Consectetur adipiscing elit, sed do eiusmod tempor Incididunt ut. Labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut. Aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint.

<Tabs.Root defaultIndex={2}>
<Tabs.Item>
<Tabs.Tab>Tab 1</Tabs.Tab>
<Tabs.Panel>
<DummyText pre="Tab 01: "/>
</Tabs.Panel>
</Tabs.Item>
<Tabs.Item>
<Tabs.Tab>Tab 2</Tabs.Tab>
<Tabs.Panel>
<DummyText length="l" offset={1} pre="Tab 02: " />
</Tabs.Panel>
</Tabs.Item>
<Tabs.Item>
<Tabs.Tab>Tab 3</Tabs.Tab>
<Tabs.Panel>
<DummyText length="l" offset={2} pre="Tab 03: " />
</Tabs.Panel>
</Tabs.Item>
</Tabs.Root>

Specify the initial tab via URL parameter

Opening a page with ?lism-tab={tabId}-{index} in the URL displays the specified tab as selected from the start.

To use this feature, explicitly set tabId on <Tabs.Root>.
(If tabId is omitted, an ID is generated automatically, so it can’t be targeted from the URL.)

<Tabs.Root tabId="sample-tabs">
<Tabs.Item>
<Tabs.Tab>Tab 1</Tabs.Tab>
<Tabs.Panel>...</Tabs.Panel>
</Tabs.Item>
<Tabs.Item>
<Tabs.Tab>Tab 2</Tabs.Tab>
<Tabs.Panel>...</Tabs.Panel>
</Tabs.Item>
<Tabs.Item>
<Tabs.Tab>Tab 3</Tabs.Tab>
<Tabs.Panel>...</Tabs.Panel>
</Tabs.Item>
</Tabs.Root>

Visiting a page with the tabs above using ?lism-tab=sample-tabs-2 displays the second tab as selected.

Compose without Tabs.Item

You can also place <Tabs.List> (a list with role="tablist"), <Tabs.Tab>, and <Tabs.Panel> directly inside <Tabs.Root> instead of using <Tabs.Item>.
Use this when you want to control the output structure yourself — for example, to wrap tab buttons in <li> elements, or to insert another element between the list and the panels.

In this composition, matching <Tabs.Tab> and <Tabs.Panel> pairs are linked by index (a number starting from 1). Tab switching on click or keyboard interaction, focus movement, and ARIA attribute updates are all handled by the component, so you don’t need to write any state management or event handling yourself.

↓
Example

Tab 02: Consectetur adipiscing elit, sed do eiusmod tempor Incididunt ut. Labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut. Aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint.

<Tabs.Root tabId="manual-tabs" defaultIndex={2}>
<Tabs.List>
<Tabs.Tab index={1}>Tab 1</Tabs.Tab>
<Tabs.Tab index={2}>Tab 2</Tabs.Tab>
<Tabs.Tab index={3}>Tab 3</Tabs.Tab>
</Tabs.List>
<Tabs.Panel index={1}>
<DummyText pre="Tab 01: " />
</Tabs.Panel>
<Tabs.Panel index={2}>
<DummyText length="l" offset={1} pre="Tab 02: " />
</Tabs.Panel>
<Tabs.Panel index={3}>
<DummyText length="l" offset={2} pre="Tab 03: " />
</Tabs.Panel>
</Tabs.Root>

The required props differ between the React and Astro versions.

React Astro
index Required on every <Tabs.Tab> and <Tabs.Panel>. Required on every <Tabs.Tab> and <Tabs.Panel>.
tabId Not needed — the value from <Tabs.Root> (or its auto-generated ID, if omitted) is shared automatically. Even if you set it individually, <Tabs.Root> takes priority. Specify the same value on each <Tabs.Tab> and <Tabs.Panel>.
Initial selection Set via defaultIndex (default 1) on <Tabs.Root>. Set isActive on both the <Tabs.Tab> and <Tabs.Panel> that should be open initially.
  • In the React version, <Tabs.Tab> with isActive becomes the initial selection only when defaultIndex is omitted. This compatibility feature lets you reuse the same markup as the Astro version, but using defaultIndex is recommended for React. isActive on <Tabs.Panel> isn’t needed in the React version.
  • onClick and onKeyDown handlers passed to <Tabs.Tab> in the React version are called before the component’s internal handling. Calling e.preventDefault() inside them prevents the tab switch (the same applies to the <Tabs.Item> composition).
  • The Astro version can’t share values from parent to child, so tabId, defaultIndex, and listProps on <Tabs.Root> have no effect in this composition. Pass any props you want on the list element directly to <Tabs.List> instead.
  • Specifying the initial tab via a URL parameter also works with this composition. When using it in the React version, explicitly set tabId on <Tabs.Root>.
  • Always place <Tabs.Tab> inside <Tabs.List>. Keyboard navigation follows the tabs within the same role="tablist" element in DOM order.
  • This composition doesn’t fall back to 1 for out-of-range numbers. If you set the initial selection to a number that doesn’t exist, the tabs render with none selected (you can still select one by clicking).

Vertical tabs

Setting grid-template:'list panel' auto / auto 1fr; on <Tabs.Root> (b--tabs) places the tab list and content side by side.

At the same time, changing the tab list (b--tabs_list) buttons to stack vertically (flex-direction:column;) achieves the following layout.

↓
Example: side-by-side layout on @sm and above

Tab 01: Lorem ipsum dolor sit amet. Consectetur adipiscing elit, sed do eiusmod tempor Incididunt ut. Labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut.

Resize
<Tabs.Root gt={[null, '"list panel" auto / auto 1fr']} listProps={{ fxd: [null, 'column'] }}>
<Tabs.Item>
<Tabs.Tab>Tab 1</Tabs.Tab>
<Tabs.Panel>
<DummyText pre="Tab 01: "/>
</Tabs.Panel>
</Tabs.Item>
<Tabs.Item>
<Tabs.Tab>Tab 2</Tabs.Tab>
<Tabs.Panel>
<DummyText length="l" offset={1} pre="Tab 02: " />
</Tabs.Panel>
</Tabs.Item>
<Tabs.Item>
<Tabs.Tab>Tab 3</Tabs.Tab>
<Tabs.Panel>
<DummyText length="l" offset={2} pre="Tab 03: " />
</Tabs.Panel>
</Tabs.Item>
</Tabs.Root>

Opt-in

The following are example style variations you can build yourself.

b--tabs_tab exposes --_isSelected and --_notSelected variables, which are useful for styling. (They can be used in the same way as --_isHov and --_notHov from set--hov.)

variant=“emboss”

↓
variant='emboss'

Tab 01: Lorem ipsum dolor sit amet. Consectetur adipiscing elit, sed do eiusmod tempor Incididunt ut. Labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut.

<Tabs.Root variant="emboss">
<Tabs.Item>
<Tabs.Tab>Tab 1</Tabs.Tab>
<Tabs.Panel>
<DummyText pre="Tab 01: " />
</Tabs.Panel>
</Tabs.Item>
<Tabs.Item>
<Tabs.Tab>Tab 2</Tabs.Tab>
<Tabs.Panel>
<DummyText length="l" offset={1} pre="Tab 02: " />
</Tabs.Panel>
</Tabs.Item>
<Tabs.Item>
<Tabs.Tab>Tab 3</Tabs.Tab>
<Tabs.Panel>
<DummyText length="l" offset={2} pre="Tab 03: " />
</Tabs.Panel>
</Tabs.Item>
</Tabs.Root>
↓
Required additional 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); /* 親の bdrs - (親のpadding / 2) */
    background-color: var(--_isSelected, var(--base));
    box-shadow: var(--_isSelected, var(--bxsh--10));
  }
}

variant=“folder”

↓
variant='folder'

Tab 01: Lorem ipsum dolor sit amet. Consectetur adipiscing elit, sed do eiusmod tempor Incididunt ut. Labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut.

<Tabs.Root variant="folder">
<Tabs.Item>
<Tabs.Tab>Tab 1</Tabs.Tab>
<Tabs.Panel>
<DummyText pre="Tab 01: " />
</Tabs.Panel>
</Tabs.Item>
<Tabs.Item>
<Tabs.Tab>Tab 2</Tabs.Tab>
<Tabs.Panel>
<DummyText length="l" offset={1} pre="Tab 02: " />
</Tabs.Panel>
</Tabs.Item>
<Tabs.Item>
<Tabs.Tab>Tab 3</Tabs.Tab>
<Tabs.Panel>
<DummyText length="l" offset={2} pre="Tab 03: " />
</Tabs.Panel>
</Tabs.Item>
</Tabs.Root>

This creates a folder-style variation where the tab buttons appear connected to the panel.

↓
Required additional 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;
  }
}

Fixed height

An example of how to fix the overall tabs height to match the tallest content panel.

↓
Example (height is fixed to Tab 2)

Tab 01: Lorem ipsum dolor sit amet. Consectetur adipiscing elit, sed do eiusmod tempor Incididunt ut. Labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut.

<Tabs.Root data-tabs-keep-height g="15">
<Tabs.Item>
<Tabs.Tab px="10">Tab 1</Tabs.Tab>
<Tabs.Panel>
<DummyText pre="Tab 01: " />
</Tabs.Panel>
</Tabs.Item>
<Tabs.Item>
<Tabs.Tab px="10">Tab 2</Tabs.Tab>
<Tabs.Panel>
<DummyText length="xl" offset={1} pre="Tab 02: " />
</Tabs.Panel>
</Tabs.Item>
<Tabs.Item>
<Tabs.Tab px="10">Tab 3</Tabs.Tab>
<Tabs.Panel>
<DummyText length="l" offset={2} pre="Tab 03: " />
</Tabs.Panel>
</Tabs.Item>
</Tabs.Root>
↓
Required additional CSS
@layer lism-custom {
  [data-tabs-keep-height] > .b--tabs_panel[hidden] {
    display: block;
    visibility: hidden;
    opacity: 0;
  }
}

© 2026 Lism CSS.