Tab basic
基本的なタブ切り替え。キーボード操作(←→ / Home / End)対応。
2023年12月以降、すべてのブラウザーで利用可能です。
- Baseline
- BaselineWidely available
- 対応ブラウザ
- Chrome120+Edge120+Firefox117+Safari17.2+
プレビューを読み込み中
<div class="tab-basic js-tab-basic">
<div class="tab-basic__list" role="tablist" aria-label="タブ">
<button
class="tab-basic__tab js-tab-basic-tab tab-basic__tab--active"
type="button"
role="tab"
aria-selected="true"
data-tab="tab-1"
>
タブ1
</button>
<button
class="tab-basic__tab js-tab-basic-tab"
type="button"
role="tab"
aria-selected="false"
tabindex="-1"
data-tab="tab-2"
>
タブ2
</button>
<button
class="tab-basic__tab js-tab-basic-tab"
type="button"
role="tab"
aria-selected="false"
tabindex="-1"
data-tab="tab-3"
>
タブ3
</button>
</div>
<div class="tab-basic__panels">
<section
class="tab-basic__panel js-tab-basic-panel tab-basic__panel--active"
role="tabpanel"
data-panel="tab-1"
>
<p class="tab-basic__text">
タブ1のコンテンツです。ここに説明テキストが入ります。
</p>
</section>
<section
class="tab-basic__panel js-tab-basic-panel"
role="tabpanel"
data-panel="tab-2"
hidden
>
<p class="tab-basic__text">
タブ2のコンテンツです。ここに説明テキストが入ります。
</p>
</section>
<section
class="tab-basic__panel js-tab-basic-panel"
role="tabpanel"
data-panel="tab-3"
hidden
>
<p class="tab-basic__text">
タブ3のコンテンツです。ここに説明テキストが入ります。
</p>
</section>
</div>
</div>
<div class="tab-basic js-tab-basic">
<div class="tab-basic__list" role="tablist" aria-label="タブ">
<button
class="tab-basic__tab js-tab-basic-tab tab-basic__tab--active"
type="button"
role="tab"
aria-selected="true"
data-tab="tab-1"
>
タブ1
</button>
<button
class="tab-basic__tab js-tab-basic-tab"
type="button"
role="tab"
aria-selected="false"
tabindex="-1"
data-tab="tab-2"
>
タブ2
</button>
<button
class="tab-basic__tab js-tab-basic-tab"
type="button"
role="tab"
aria-selected="false"
tabindex="-1"
data-tab="tab-3"
>
タブ3
</button>
</div>
<div class="tab-basic__panels">
<section
class="tab-basic__panel js-tab-basic-panel tab-basic__panel--active"
role="tabpanel"
data-panel="tab-1"
>
<p class="tab-basic__text">
タブ1のコンテンツです。ここに説明テキストが入ります。
</p>
</section>
<section
class="tab-basic__panel js-tab-basic-panel"
role="tabpanel"
data-panel="tab-2"
hidden
>
<p class="tab-basic__text">
タブ2のコンテンツです。ここに説明テキストが入ります。
</p>
</section>
<section
class="tab-basic__panel js-tab-basic-panel"
role="tabpanel"
data-panel="tab-3"
hidden
>
<p class="tab-basic__text">
タブ3のコンテンツです。ここに説明テキストが入ります。
</p>
</section>
</div>
</div>
/* reset */
:where(p).tab-basic__text {
margin: 0;
font: inherit;
}
:where(button).tab-basic__tab {
box-sizing: border-box;
padding: 0;
font: inherit;
color: inherit;
text-align: inherit;
appearance: none;
cursor: pointer;
background: none;
border: none;
}
/* snippet */
.tab-basic {
--tab-text-color: #5e6775;
--tab-text-color-active: #364153;
--tab-accent: #2b36f5;
--tab-border: #e7e7e7;
--tab-focus-color: var(--tab-accent);
--tab-radius: 4px;
--tab-duration: 0.3s;
width: 100%;
max-width: 480px;
}
.tab-basic__list {
display: grid;
grid-auto-columns: 1fr;
grid-auto-flow: column;
gap: 4px;
border-bottom: 1px solid var(--tab-border);
}
.tab-basic__tab {
position: relative;
display: grid;
place-items: center;
min-height: var(--size-touch-target, 40px);
padding-block: 12px;
padding-inline: 8px;
font-size: 14px;
font-weight: 700;
line-height: 1.5;
color: var(--tab-text-color);
border-radius: var(--tab-radius) var(--tab-radius) 0 0;
&::after {
position: absolute;
right: 8px;
bottom: -1px;
left: 8px;
height: 2px;
content: "";
background-color: var(--tab-accent);
border-radius: calc(infinity * 1px);
opacity: 0;
transform: scaleX(0.6);
transition:
opacity var(--tab-duration),
transform var(--tab-duration);
}
&:focus:not(:focus-visible) {
outline: none;
}
&:focus-visible {
outline: 2px solid var(--tab-focus-color);
outline-offset: -2px;
}
}
.tab-basic__tab--active {
color: var(--tab-text-color-active);
&::after {
opacity: 1;
transform: scaleX(1);
}
}
.tab-basic__panels {
display: grid;
}
.tab-basic__panel {
display: none;
grid-area: 1 / 1;
}
.tab-basic__panel--active {
display: block;
}
.tab-basic__text {
padding-block: 16px;
font-size: 14px;
line-height: 1.7;
color: var(--tab-text-color);
}
@media (any-hover: hover) {
.tab-basic__tab:not(.tab-basic__tab--active):hover {
color: var(--tab-text-color-active);
}
}
@media (prefers-reduced-motion: reduce) {
.tab-basic__tab::after {
transition: none;
}
}
/* reset */
:where(p).tab-basic__text {
margin: 0;
font: inherit;
}
:where(button).tab-basic__tab {
box-sizing: border-box;
padding: 0;
font: inherit;
color: inherit;
text-align: inherit;
appearance: none;
cursor: pointer;
background: none;
border: none;
}
/* snippet */
.tab-basic {
--tab-text-color: #5e6775;
--tab-text-color-active: #364153;
--tab-accent: #2b36f5;
--tab-border: #e7e7e7;
--tab-focus-color: var(--tab-accent);
--tab-radius: 4px;
--tab-duration: 0.3s;
width: 100%;
max-width: 480px;
}
.tab-basic__list {
display: grid;
grid-auto-columns: 1fr;
grid-auto-flow: column;
gap: 4px;
border-bottom: 1px solid var(--tab-border);
}
.tab-basic__tab {
position: relative;
display: grid;
place-items: center;
min-height: var(--size-touch-target, 40px);
padding-block: 12px;
padding-inline: 8px;
font-size: 14px;
font-weight: 700;
line-height: 1.5;
color: var(--tab-text-color);
border-radius: var(--tab-radius) var(--tab-radius) 0 0;
&::after {
position: absolute;
right: 8px;
bottom: -1px;
left: 8px;
height: 2px;
content: "";
background-color: var(--tab-accent);
border-radius: calc(infinity * 1px);
opacity: 0;
transform: scaleX(0.6);
transition:
opacity var(--tab-duration),
transform var(--tab-duration);
}
&:focus:not(:focus-visible) {
outline: none;
}
&:focus-visible {
outline: 2px solid var(--tab-focus-color);
outline-offset: -2px;
}
}
.tab-basic__tab--active {
color: var(--tab-text-color-active);
&::after {
opacity: 1;
transform: scaleX(1);
}
}
.tab-basic__panels {
display: grid;
}
.tab-basic__panel {
display: none;
grid-area: 1 / 1;
}
.tab-basic__panel--active {
display: block;
}
.tab-basic__text {
padding-block: 16px;
font-size: 14px;
line-height: 1.7;
color: var(--tab-text-color);
}
@media (any-hover: hover) {
.tab-basic__tab:not(.tab-basic__tab--active):hover {
color: var(--tab-text-color-active);
}
}
@media (prefers-reduced-motion: reduce) {
.tab-basic__tab::after {
transition: none;
}
}
(function () {
let instanceIndex = 0;
function syncTabFocusability(tabs) {
tabs.forEach((tab) => {
tab.tabIndex = tab.getAttribute("aria-selected") === "true" ? 0 : -1;
});
}
function activateTab(tabs, panels, activeTab) {
const name = activeTab.dataset.tab;
if (!name) return;
tabs.forEach((tab) => {
const isActive = tab === activeTab;
tab.classList.toggle("tab-basic__tab--active", isActive);
tab.setAttribute("aria-selected", String(isActive));
});
syncTabFocusability(tabs);
panels.forEach((panel) => {
const isActive = panel.dataset.panel === name;
panel.classList.toggle("tab-basic__panel--active", isActive);
panel.hidden = !isActive;
});
}
function initTabBasic(root) {
if (root.dataset.tabBasicInit === "true") return;
root.dataset.tabBasicInit = "true";
instanceIndex += 1;
const tabs = [...root.querySelectorAll(".js-tab-basic-tab[data-tab]")];
const panels = [
...root.querySelectorAll(".js-tab-basic-panel[data-panel]"),
];
if (tabs.length === 0 || panels.length === 0) return;
tabs.forEach((tab, index) => {
const panelName = tab.dataset.tab;
const panel = panels.find((item) => item.dataset.panel === panelName);
if (!panel) return;
const tabId = `tab-basic-tab-${instanceIndex}-${index + 1}`;
const panelId = `tab-basic-panel-${instanceIndex}-${index + 1}`;
tab.id = tabId;
tab.setAttribute("aria-controls", panelId);
panel.id = panelId;
panel.setAttribute("aria-labelledby", tabId);
});
tabs.forEach((tab) => {
tab.addEventListener("click", () => {
activateTab(tabs, panels, tab);
});
tab.addEventListener("keydown", (event) => {
const currentIndex = tabs.indexOf(tab);
let next;
if (event.key === "ArrowRight") {
next = tabs[currentIndex + 1] ?? tabs[0];
}
if (event.key === "ArrowLeft") {
next = tabs[currentIndex - 1] ?? tabs.at(-1);
}
if (event.key === "Home") {
next = tabs[0];
}
if (event.key === "End") {
next = tabs.at(-1);
}
if (!next) return;
event.preventDefault();
next.focus();
activateTab(tabs, panels, next);
});
});
syncTabFocusability(tabs);
}
function initAll() {
document.querySelectorAll(".js-tab-basic").forEach(initTabBasic);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initAll);
} else {
initAll();
}
})();
(function () {
let instanceIndex = 0;
function syncTabFocusability(tabs) {
tabs.forEach((tab) => {
tab.tabIndex = tab.getAttribute("aria-selected") === "true" ? 0 : -1;
});
}
function activateTab(tabs, panels, activeTab) {
const name = activeTab.dataset.tab;
if (!name) return;
tabs.forEach((tab) => {
const isActive = tab === activeTab;
tab.classList.toggle("tab-basic__tab--active", isActive);
tab.setAttribute("aria-selected", String(isActive));
});
syncTabFocusability(tabs);
panels.forEach((panel) => {
const isActive = panel.dataset.panel === name;
panel.classList.toggle("tab-basic__panel--active", isActive);
panel.hidden = !isActive;
});
}
function initTabBasic(root) {
if (root.dataset.tabBasicInit === "true") return;
root.dataset.tabBasicInit = "true";
instanceIndex += 1;
const tabs = [...root.querySelectorAll(".js-tab-basic-tab[data-tab]")];
const panels = [
...root.querySelectorAll(".js-tab-basic-panel[data-panel]"),
];
if (tabs.length === 0 || panels.length === 0) return;
tabs.forEach((tab, index) => {
const panelName = tab.dataset.tab;
const panel = panels.find((item) => item.dataset.panel === panelName);
if (!panel) return;
const tabId = `tab-basic-tab-${instanceIndex}-${index + 1}`;
const panelId = `tab-basic-panel-${instanceIndex}-${index + 1}`;
tab.id = tabId;
tab.setAttribute("aria-controls", panelId);
panel.id = panelId;
panel.setAttribute("aria-labelledby", tabId);
});
tabs.forEach((tab) => {
tab.addEventListener("click", () => {
activateTab(tabs, panels, tab);
});
tab.addEventListener("keydown", (event) => {
const currentIndex = tabs.indexOf(tab);
let next;
if (event.key === "ArrowRight") {
next = tabs[currentIndex + 1] ?? tabs[0];
}
if (event.key === "ArrowLeft") {
next = tabs[currentIndex - 1] ?? tabs.at(-1);
}
if (event.key === "Home") {
next = tabs[0];
}
if (event.key === "End") {
next = tabs.at(-1);
}
if (!next) return;
event.preventDefault();
next.focus();
activateTab(tabs, panels, next);
});
});
syncTabFocusability(tabs);
}
function initAll() {
document.querySelectorAll(".js-tab-basic").forEach(initTabBasic);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initAll);
} else {
initAll();
}
})();