ui
Tabs
軽量タブ。見た目はCSS、操作/ARIAはJS。
tabsarianavigationcss+js
Customize
--mk-tabs-gap / --mk-tabs-duration / --mk-tabs-underline-color
A11y
Arrow/Home/End キー対応。aria-selected と tabindex を同期します。
Install
“壊れにくさ”優先のため、依存は増やさずに ファイルをコピーして使う 形式です。
Files
まずは下記ファイルをプロジェクトに配置してください。
required-files.md
- tailwind-motion-kit/css/00_tokens.css (css)
- tailwind-motion-kit/css/20_tabs.css (css)
- tailwind-motion-kit/js/tabs.js (js)Option A
まずは Bundle を読み込んで、動作を確認するのがおすすめです。
globals.css
/* Bundle: まずはこれが一番簡単 */
@import "./tailwind-motion-kit/css/99_bundle.css";Option B
必要な CSS だけ読み込みたい場合はこちら。
globals.css
/* 1) CSS: グローバルCSSに追記 */
@import "./tailwind-motion-kit/css/00_tokens.css";
@import "./tailwind-motion-kit/css/20_tabs.css";JavaScript
動きやUI制御が必要な場合のみ、初期化を追加します。
init.js
import { initTabs } from "./tailwind-motion-kit/js/tabs.js";
initTabs();Snippets
Copy & paste
usage.html
<div class="mk-tabs" data-mk-tabs>
<div class="mk-tablist" role="tablist" aria-label="Example">
<button class="mk-tab" role="tab" aria-selected="true" data-mk-tab>Tab A</button>
<button class="mk-tab" role="tab" aria-selected="false" data-mk-tab>Tab B</button>
</div>
<div class="mk-tabpanels">
<section class="mk-tabpanel" role="tabpanel" data-mk-panel>...</section>
<section class="mk-tabpanel" role="tabpanel" data-mk-panel hidden>...</section>
</div>
</div>usage.js
import { initTabs } from "./tailwind-motion-kit/js/tabs.js";
initTabs();Files
この部品が参照する実ファイルです。
tailwind-motion-kit/css/00_tokens.csscss
tailwind-motion-kit/css/00_tokens.css
/*!
* Tailwind Motion Kit (mk) - Tokens
* --------------------------------
* 目的: 速度・距離・イージング等の“モーション言語”を統一し、
* パーツを増やしても品位が崩れない状態を作ります。
*
* 使い方:
* - :root の変数を上書きするだけで全体の動きを一括調整できます。
* - 個別要素は style="" か data-mk-* 属性で上書きできます(JSがある場合)。
*/
:root {
/* --- イージング(加減速) --- */
--mk-ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--mk-ease-in: cubic-bezier(0.32, 0, 0.67, 0);
--mk-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
/* “少しだけ遊び”が欲しい時用(多用は上品さを損ねます) */
--mk-ease-out-back: cubic-bezier(0.34, 1.56, 0.64, 1);
/* --- 時間 --- */
--mk-duration-fast: 160ms;
--mk-duration-medium: 280ms;
--mk-duration-slow: 600ms;
--mk-duration-slower: 900ms;
/* コンポーネントが参照する“共通デフォルト” */
--mk-duration: var(--mk-duration-medium);
--mk-ease: var(--mk-ease-out);
--mk-delay: 0ms;
/* --- 移動量 --- */
--mk-distance-1: 6px;
--mk-distance-2: 12px;
--mk-distance-3: 20px;
--mk-distance: var(--mk-distance-2);
/* --- ぼかし --- */
--mk-blur-0: 0px;
--mk-blur-1: 6px;
--mk-blur-2: 10px;
--mk-blur: var(--mk-blur-1);
/* --- 影(浮遊感) --- */
--mk-shadow-lift: 0 12px 30px rgba(0, 0, 0, 0.14);
--mk-shadow-soft: 0 10px 22px rgba(0, 0, 0, 0.10);
/* --- アンダーライン --- */
--mk-underline-thickness: 2px;
--mk-underline-offset: 0.2em;
/* --- フォーカスリング --- */
--mk-ring-color: rgba(59, 130, 246, 0.35);
--mk-ring-size: 3px;
/* --- スケルトン(ロード中) --- */
--mk-skeleton-a: rgba(0, 0, 0, 0.06);
--mk-skeleton-b: rgba(0, 0, 0, 0.12);
--mk-skeleton-speed: 1250ms;
/* --- スピナー --- */
--mk-spinner-size: 1.2em;
--mk-spinner-border: 2px;
/* --- テキスト分割表示 --- */
--mk-char-step: 25ms; /* 1文字ごとの遅延 */
/* --- ダイアログ / ドロワー --- */
--mk-dialog-duration: var(--mk-duration-slow);
--mk-dialog-ease: var(--mk-ease-out);
--mk-dialog-backdrop-opacity: 0.45;
--mk-dialog-offset: var(--mk-distance-3);
--mk-dialog-scale-from: 0.995;
--mk-dialog-radius: 18px;
/* --- 折りたたみ(アコーディオン) --- */
--mk-collapse-duration: var(--mk-duration-medium);
--mk-collapse-ease: var(--mk-ease);
/* --- Toast / 通知 --- */
--mk-toast-gap: 10px;
--mk-toast-width: min(360px, calc(100vw - 24px));
--mk-toast-duration: var(--mk-duration-medium);
--mk-toast-ease: var(--mk-ease-out);
/* --- スクロール進捗(トップバー) --- */
--mk-scroll-progress-top: 0px;
--mk-scroll-progress-height: 3px;
--mk-scroll-progress-ease: linear;
/* --- ナビ下線インジケータ --- */
--mk-nav-underline-height: 2px;
--mk-nav-underline-radius: 999px;
--mk-nav-duration: var(--mk-duration-medium);
--mk-nav-ease: var(--mk-ease-out);
/* --- UI state(idle/loading/success/error) --- */
--mk-state-hold-success: 900ms;
--mk-state-hold-error: 1400ms;
/* --- Tabs --- */
--mk-tabs-gap: 10px;
--mk-tabs-radius: 999px;
--mk-tabs-duration: var(--mk-duration-medium);
--mk-tabs-ease: var(--mk-ease-out);
--mk-tabs-underline-height: 2px;
--mk-tabs-underline-color: currentColor;
/* --- Tooltip --- */
--mk-tooltip-bg: rgba(0, 0, 0, 0.82);
--mk-tooltip-fg: #fff;
--mk-tooltip-radius: 10px;
--mk-tooltip-padding: 10px 12px;
--mk-tooltip-gap: 8px;
--mk-tooltip-shadow: 0 18px 60px rgba(0,0,0,0.35);
--mk-tooltip-max-width: 260px;
/* --- Stepper --- */
--mk-stepper-gap: 14px;
--mk-step-size: 28px;
--mk-step-line: rgba(255, 255, 255, 0.14);
--mk-step-done: rgba(59, 130, 246, 1);
--mk-step-current: rgba(255, 255, 255, 0.92);
--mk-step-todo: rgba(255, 255, 255, 0.5);
}
/* motion減衰設定のあるユーザーには、動きを最小化して“結果”を出します */
@media (prefers-reduced-motion: reduce) {
:root {
--mk-duration-fast: 0ms;
--mk-duration-medium: 0ms;
--mk-duration-slow: 0ms;
--mk-duration-slower: 0ms;
--mk-duration: 0ms;
--mk-delay: 0ms;
}
}
tailwind-motion-kit/css/20_tabs.csscss
tailwind-motion-kit/css/20_tabs.css
/*!
* Tailwind Motion Kit (mk) - Tabs
* ------------------------------
* 目的: “情報の切り替え”を、軽く・上品に。
* 方針: CSSは見た目 / JSは状態(active)とアクセシビリティ(ARIA)を担当。
*
* 推奨マークアップ:
* <div class="mk-tabs" data-mk-tabs>
* <div class="mk-tablist" role="tablist" aria-label="Example">
* <button class="mk-tab" role="tab" aria-selected="true" data-mk-tab>Tab A</button>
* <button class="mk-tab" role="tab" aria-selected="false" data-mk-tab>Tab B</button>
* </div>
* <div class="mk-tabpanels">
* <section class="mk-tabpanel" role="tabpanel" data-mk-panel>...</section>
* <section class="mk-tabpanel" role="tabpanel" data-mk-panel hidden>...</section>
* </div>
* </div>
*
* JS:
* import { initTabs } from "./js/tabs.js";
* initTabs();
*/
.mk-tabs {
display: grid;
gap: 14px;
}
.mk-tablist {
display: flex;
gap: var(--mk-tabs-gap);
align-items: center;
flex-wrap: wrap;
}
.mk-tab {
position: relative;
cursor: pointer;
user-select: none;
padding: 10px 12px;
border-radius: var(--mk-tabs-radius);
background: transparent;
color: inherit;
border: 1px solid rgba(255, 255, 255, 0.10);
opacity: 0.75;
transition:
opacity var(--mk-tabs-duration) var(--mk-tabs-ease),
background var(--mk-tabs-duration) var(--mk-tabs-ease),
border-color var(--mk-tabs-duration) var(--mk-tabs-ease);
}
.mk-tab:hover {
opacity: 0.92;
background: rgba(255, 255, 255, 0.04);
border-color: rgba(255, 255, 255, 0.16);
}
.mk-tab[data-mk-state="active"] {
opacity: 1;
background: rgba(255, 255, 255, 0.06);
border-color: rgba(255, 255, 255, 0.22);
}
.mk-tab:focus-visible {
outline: none;
box-shadow: 0 0 0 var(--mk-ring-size) var(--mk-ring-color);
}
/* underline */
.mk-tab::after {
content: "";
position: absolute;
left: 12px;
right: 12px;
bottom: -2px;
height: var(--mk-tabs-underline-height);
border-radius: 999px;
background: var(--mk-tabs-underline-color);
transform: scaleX(0);
transform-origin: left;
transition: transform var(--mk-tabs-duration) var(--mk-tabs-ease);
opacity: 0.85;
}
.mk-tab[data-mk-state="active"]::after {
transform: scaleX(1);
}
/* Panels
*
* Progressive enhancement:
* - JS が動かない環境では、パネルはすべて表示(情報欠落を避ける)
* - JS 初期化後(data-mk-enhanced="true")のみ、active 以外を非表示
*/
.mk-tabpanel {
display: block;
}
@keyframes mk-tabs-panel-in {
from { opacity: 0; transform: translate3d(0, 6px, 0); }
to { opacity: 1; transform: translate3d(0, 0, 0); }
}
/* JS 初期化後のみ切り替え(panelを隠す) */
[data-mk-tabs][data-mk-enhanced="true"] .mk-tabpanel {
display: none;
}
[data-mk-tabs][data-mk-enhanced="true"] .mk-tabpanel[data-mk-state="active"] {
display: block;
animation: mk-tabs-panel-in var(--mk-tabs-duration) var(--mk-tabs-ease) both;
}
@media (prefers-reduced-motion: reduce) {
.mk-tab,
.mk-tab::after {
transition: none !important;
}
.mk-tabpanel[data-mk-state="active"] {
animation: none !important;
}
}
tailwind-motion-kit/js/tabs.jsjs
tailwind-motion-kit/js/tabs.js
// Tailwind Motion Kit (mk) - Tabs
// --------------------------------
// Minimal, dependency-free tabs controller.
//
// Assumptions:
// - Root has [data-mk-tabs]
// - Tabs are elements with [data-mk-tab] and role="tab" within the root
// - Panels are elements with [data-mk-panel] and role="tabpanel" within the root
//
// State:
// - Active tab/panel get data-mk-state="active"
// - aria-selected and tabindex are kept in sync
export function initTabs({ rootSelector = "[data-mk-tabs]" } = {}) {
const roots = Array.from(document.querySelectorAll(rootSelector));
roots.forEach((root) => setupTabs(root));
}
function setupTabs(root) {
const tablist = root.querySelector("[role=tablist]");
const tabs = Array.from(root.querySelectorAll("[data-mk-tab]"));
const panels = Array.from(root.querySelectorAll("[data-mk-panel]"));
if (!tablist || tabs.length === 0 || panels.length === 0) return;
// Progressive enhancement flag (CSS 側で panel の表示/非表示を切り替えるため)
root.setAttribute("data-mk-enhanced", "true");
// Ensure aria
tabs.forEach((tab, i) => {
tab.setAttribute("role", "tab");
tab.setAttribute("aria-selected", tab.getAttribute("aria-selected") === "true" ? "true" : "false");
if (!tab.id) tab.id = `mk-tab-${Math.random().toString(36).slice(2)}-${i}`;
});
panels.forEach((panel, i) => {
panel.setAttribute("role", "tabpanel");
if (!panel.id) panel.id = `mk-panel-${Math.random().toString(36).slice(2)}-${i}`;
});
// Pairing by index
const pairs = tabs.map((tab, i) => ({ tab, panel: panels[i] }));
// Link tab -> panel (recommended for a11y)
pairs.forEach(({ tab, panel }) => {
if (panel?.id) tab.setAttribute("aria-controls", panel.id);
});
// Determine initial active
let activeIndex = pairs.findIndex(({ tab }) => tab.getAttribute("aria-selected") === "true");
if (activeIndex < 0) activeIndex = 0;
activate(pairs, activeIndex, { focus: false });
// Events
pairs.forEach(({ tab }, index) => {
tab.addEventListener("click", (e) => {
e.preventDefault();
activate(pairs, index, { focus: true });
});
tab.addEventListener("keydown", (e) => {
const key = e.key;
if (key !== "ArrowLeft" && key !== "ArrowRight" && key !== "Home" && key !== "End") return;
e.preventDefault();
const current = pairs.findIndex(({ tab: t }) => t.dataset.mkState === "active");
let next = current;
if (key === "ArrowLeft") next = Math.max(0, current - 1);
if (key === "ArrowRight") next = Math.min(pairs.length - 1, current + 1);
if (key === "Home") next = 0;
if (key === "End") next = pairs.length - 1;
activate(pairs, next, { focus: true });
});
});
}
function activate(pairs, index, { focus }) {
pairs.forEach(({ tab, panel }, i) => {
const isActive = i === index;
tab.dataset.mkState = isActive ? "active" : "";
tab.setAttribute("aria-selected", isActive ? "true" : "false");
tab.setAttribute("tabindex", isActive ? "0" : "-1");
if (panel) {
panel.dataset.mkState = isActive ? "active" : "";
if (isActive) {
panel.removeAttribute("hidden");
panel.setAttribute("aria-labelledby", tab.id);
} else {
panel.setAttribute("hidden", "");
}
}
if (isActive && focus) tab.focus();
});
}
Preview
UIの骨格を作る
実装して磨く
最小で出して学ぶ
Tips
- • デザイン側は CSS 変数(tokens)で一括調整できます。
- • 動きは “短く・小さく・同じ癖” を優先すると品位が保てます。
- • prefers-reduced-motion を必ず考慮してください。
