アニメーション
CSSとJSで作る、ガラス風UIの展開式Dockナビゲーション
2026/04/02
2026/4/1
Webサイトのナビゲーションを「ただのメニュー」ではなく、操作そのものを体験に変えるUIとして設計したDock型ナビゲーションです。
ガラス風の背景表現とスムーズなアニメーションにより、デスクトップアプリのような操作感を実現しています。
タブをクリックすると対応するパネルが展開し、カテゴリ・タグ・メニューなどを整理して表示可能。CSSによる視覚表現とJavaScriptによる状態管理を組み合わせた、実用性とデザイン性を両立したUIです。
Preview プレビュー
Code コード
<div class="kumonosu-wrapper">
<aside class="kumonosu-dock" id="kumonosu-dock">
<div class="kumonosu-panel-stack">
<!-- メニューパネル -->
<section class="kumonosu-panel" data-panel="menu">
<div class="kumonosu-item">
<div class="kumonosu-text-group">
<div class="kumonosu-title">サイトについて</div>
<div class="kumonosu-subtitle">活動内容やビジョンのご紹介</div>
</div>
</div>
<div class="kumonosu-item">
<div class="kumonosu-text-group">
<div class="kumonosu-title">お問い合わせ</div>
<div class="kumonosu-subtitle">フォームよりお気軽にご連絡ください</div>
</div>
</div>
</section>
<!-- カテゴリパネル -->
<section class="kumonosu-panel" data-panel="category">
<div class="kumonosu-item">
<div class="kumonosu-icon-ghost"><i data-lucide="folder"></i></div>
<div class="kumonosu-text-group">
<div class="kumonosu-title">カテゴリ1</div>
<div class="kumonosu-subtitle">最新のニュースとトピック</div>
</div>
<div class="kumonosu-tag">最新</div>
</div>
<div class="kumonosu-item">
<div class="kumonosu-icon-ghost"><i data-lucide="folder"></i></div>
<div class="kumonosu-text-group">
<div class="kumonosu-title">カテゴリ2</div>
<div class="kumonosu-subtitle">深く掘り下げた特集記事</div>
</div>
</div>
</section>
<!-- タグパネル -->
<section class="kumonosu-panel" data-panel="tags">
<div class="kumonosu-item">
<div class="kumonosu-icon-ghost"><i data-lucide="hash"></i></div>
<div class="kumonosu-text-group">
<div class="kumonosu-title">タグ1</div>
<div class="kumonosu-subtitle">注目度の高いキーワード</div>
</div>
</div>
<div class="kumonosu-item">
<div class="kumonosu-icon-ghost"><i data-lucide="hash"></i></div>
<div class="kumonosu-text-group">
<div class="kumonosu-title">タグ2</div>
<div class="kumonosu-subtitle">過去のアーカイブ一覧</div>
</div>
</div>
</section>
</div>
<div class="kumonosu-divider"></div>
<nav class="kumonosu-nav">
<span class="kumonosu-pill" id="kumonosu-pill"></span>
<button class="kumonosu-tab" data-panel="menu">
<i data-lucide="menu"></i><span>メニュー</span>
</button>
<button class="kumonosu-tab" data-panel="category">
<i data-lucide="grid"></i><span>カテゴリ</span>
</button>
<button class="kumonosu-tab" data-panel="tags">
<i data-lucide="tag"></i><span>タグ</span>
</button>
</nav>
</aside>
</div>
@layer reset {
*, *::before, *::after {
box-sizing: border-box;
}
html {
-moz-text-size-adjust: none;
-webkit-text-size-adjust: none;
text-size-adjust: none;
}
body {
margin: 0;
min-block-size: 100vh;
line-height: 1.6;
background: #0a0a0a;
color: #fff;
overflow-x: hidden;
}
button {
font: inherit;
cursor: pointer;
border: none;
background: none;
color: inherit;
}
}
:root {
/* 接頭辞をつけた変数名で定義 */
--kumonosu-bg-dock: rgba(25, 25, 25, 0.75);
--kumonosu-glass-border: rgba(255, 255, 255, 0.12);
--kumonosu-text-main: #ffffff;
--kumonosu-text-sub: #a0a0a0;
--kumonosu-tab-idle: #808080;
--kumonosu-tab-active: #000000;
--kumonosu-pill: #ffffff;
--kumonosu-hover: rgba(255, 255, 255, 0.08);
--kumonosu-font-base: "Inter", "Hiragino Kaku Gothic ProN", "Hiragino Sans", sans-serif;
--kumonosu-radius-dock: 28px;
--kumonosu-radius-item: 16px;
--kumonosu-duration: 0.35s;
--kumonosu-ease: cubic-bezier(0.2, 0.8, 0.2, 1);
}
.kumonosu-wrapper {
display: flex;
align-items: flex-end;
justify-content: center;
min-height: 100vh;
padding-bottom: 4rem;
font-family: var(--kumonosu-font-base);
background: radial-gradient(circle at center, #1a1a1a 0%, #050505 100%);
}
/* ドックのメインコンテナ */
.kumonosu-dock {
background: var(--kumonosu-bg-dock);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border: 1px solid var(--kumonosu-glass-border);
border-radius: var(--kumonosu-radius-dock);
padding: 6px;
display: flex;
flex-direction: column;
position: relative;
transition: gap var(--kumonosu-duration) var(--kumonosu-ease);
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
z-index: 10;
}
/* 背景拡張 */
.kumonosu-dock::before {
content: "";
background: var(--kumonosu-bg-dock);
backdrop-filter: blur(20px);
border: 1px solid var(--kumonosu-glass-border);
border-radius: var(--kumonosu-radius-dock);
inset: 0;
position: absolute;
transition: inset 0.4s var(--kumonosu-ease);
z-index: -1;
}
.kumonosu-dock.kumonosu-is-open {
gap: 12px;
}
.kumonosu-dock.kumonosu-is-open::before {
inset: -1.5rem -1.2rem -0.2rem -1.2rem;
}
.kumonosu-panel-stack {
overflow: hidden;
width: 360px;
position: relative;
}
.kumonosu-panel {
height: 0;
opacity: 0;
pointer-events: none;
transform: translateY(10px);
transition: height 0.4s var(--kumonosu-ease), opacity 0.3s, transform 0.3s;
display: flex;
flex-direction: column;
}
.kumonosu-panel.kumonosu-is-active {
height: auto;
opacity: 1;
pointer-events: auto;
transform: translateY(0);
padding: 8px 0;
}
.kumonosu-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 12px 16px;
border-radius: var(--kumonosu-radius-item);
position: relative;
cursor: pointer;
}
.kumonosu-item-hover-indicator {
background: var(--kumonosu-hover);
border-radius: var(--kumonosu-radius-item);
position: absolute;
z-index: -1;
opacity: 0;
pointer-events: none;
}
.kumonosu-icon-ghost {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
color: var(--kumonosu-text-sub);
transition: transform 0.3s var(--kumonosu-ease);
flex-shrink: 0;
}
.kumonosu-item:hover .kumonosu-icon-ghost,
.kumonosu-item:hover .kumonosu-text-group {
transform: translateX(8px);
}
.kumonosu-text-group {
display: flex;
flex-direction: column;
transition: transform 0.3s var(--kumonosu-ease);
}
.kumonosu-title {
font-weight: 600;
color: var(--kumonosu-text-main);
font-size: 1rem;
line-height: 1.2;
}
.kumonosu-subtitle {
font-size: 0.8rem;
color: var(--kumonosu-text-sub);
margin-top: 4px;
}
.kumonosu-tag {
margin-left: auto;
border: 1px solid var(--kumonosu-glass-border);
color: var(--kumonosu-text-sub);
font-size: 0.7rem;
padding: 2px 8px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.05);
flex-shrink: 0;
}
.kumonosu-divider {
background: var(--kumonosu-glass-border);
height: 0;
opacity: 0;
margin: 0 10px;
transition: height 0.3s, opacity 0.3s;
}
.kumonosu-dock.kumonosu-is-open .kumonosu-divider {
height: 1px;
opacity: 1;
}
.kumonosu-nav {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
position: relative;
}
.kumonosu-pill {
background: var(--kumonosu-pill);
border-radius: 14px;
position: absolute;
height: calc(100% - 4px);
top: 2px;
z-index: 0;
opacity: 0;
pointer-events: none;
}
.kumonosu-tab {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px 0;
color: var(--kumonosu-tab-idle);
font-weight: 600;
font-size: 0.9rem;
position: relative;
z-index: 1;
transition: color 0.3s;
}
.kumonosu-tab.kumonosu-is-active {
color: var(--kumonosu-tab-active);
}
.kumonosu-tab i {
width: 16px;
height: 16px;
stroke-width: 2.5px;
}
class KumonosuDockNavigation {
constructor() {
this.state = {
activePanelId: null,
isOpen: false
};
this.elements = {};
this.panelHoverState = new Map();
this.init();
}
init() {
lucide.createIcons();
this.cacheElements();
this.setupPanels();
this.bindEvents();
}
cacheElements() {
this.elements = {
dock: document.getElementById('kumonosu-dock'),
pill: document.getElementById('kumonosu-pill'),
tabs: Array.from(document.querySelectorAll('.kumonosu-tab')),
panels: Array.from(document.querySelectorAll('.kumonosu-panel')),
panelsById: {}
};
this.elements.panels.forEach(p => {
this.elements.panelsById[p.dataset.panel] = p;
});
}
setupPanels() {
this.elements.panels.forEach(panel => {
const indicator = document.createElement("span");
indicator.className = "kumonosu-item-hover-indicator";
panel.prepend(indicator);
const hoverEntry = {
indicator,
activeItem: null
};
this.panelHoverState.set(panel, hoverEntry);
panel.addEventListener("pointerover", (e) => {
const item = e.target.closest('.kumonosu-item');
if (item && panel.contains(item)) this.renderHoverIndicator(panel, item);
});
panel.addEventListener("pointerleave", () => {
gsap.to(indicator, {
opacity: 0,
duration: 0.2
});
hoverEntry.activeItem = null;
});
});
}
bindEvents() {
this.elements.tabs.forEach(tab => {
tab.addEventListener("click", () => this.handleTabSelect(tab.dataset.panel));
});
document.addEventListener("pointerdown", (e) => {
if (this.state.isOpen && !this.elements.dock.contains(e.target)) {
this.closeMenu();
}
});
}
handleTabSelect(panelId) {
if (this.state.isOpen && this.state.activePanelId === panelId) {
this.closeMenu();
} else {
this.openMenu(panelId);
}
}
openMenu(panelId) {
this.state.isOpen = true;
this.state.activePanelId = panelId;
this.updateUI();
}
closeMenu() {
this.state.isOpen = false;
this.state.activePanelId = null;
this.updateUI();
}
updateUI() {
const {
dock,
tabs,
pill,
panelsById
} = this.elements;
// クラス名に接頭辞を使用
dock.classList.toggle('kumonosu-is-open', this.state.isOpen);
tabs.forEach(tab => {
const isActive = this.state.isOpen && tab.dataset.panel === this.state.activePanelId;
tab.classList.toggle('kumonosu-is-active', isActive);
if (isActive) {
gsap.to(pill, {
left: tab.offsetLeft,
width: tab.offsetWidth,
opacity: 1,
duration: 0.3,
ease: "power3.inOut"
});
}
});
if (!this.state.isOpen) gsap.to(pill, {
opacity: 0,
duration: 0.2
});
Object.keys(panelsById).forEach(id => {
panelsById[id].classList.toggle('kumonosu-is-active', this.state.isOpen && id === this.state.activePanelId);
});
}
renderHoverIndicator(panel, item) {
const entry = this.panelHoverState.get(panel);
if (!entry || entry.activeItem === item) return;
entry.activeItem = item;
gsap.to(entry.indicator, {
top: item.offsetTop,
left: item.offsetLeft,
width: item.offsetWidth,
height: item.offsetHeight,
opacity: 1,
duration: 0.25,
ease: "power2.out"
});
}
}
// 初期化
new KumonosuDockNavigation();
https://unpkg.com/lucide@latest
https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js
Explanation 詳しい説明
基本構造
このUIは3つのレイヤーで構成されています。
- Dockコンテナ(全体UI)
- 切り替え可能なパネルスタック
- タブナビゲーション
タブ操作によって表示パネルを切り替えるステート管理型ナビゲーションになっています。
JavaScriptクラス KumonosuDockNavigation がUI全体の状態を制御します。
Dockナビゲーションの仕組み
状態は次の2つで管理されています。
- isOpen(Dockの開閉状態)
- activePanelId(現在表示中のパネル)
クリック時の挙動:
- タブクリック
- パネルID取得
- 状態更新
- UIクラス切替
- GSAPアニメーション実行
これによりDOMを書き換えずUIのみ更新する設計になっています。
スライダー的挙動(パネル切替アニメーション)
一般的なスライダーではなく、パネル展開型UIとして動作します。
特徴:
- 高さ0 → auto へ展開
- opacityフェード
- translateYによる浮き上がり演出
height + opacity + transform
を組み合わせた軽量トランジションです。
Glassmorphism(ガラス風UI)
Dock背景は以下で構成されています。
- 半透明背景
- backdrop-filter blur
- 境界線の透明表現
- 深いシャドウ
主な指定:
backdrop-filter: blur(20px)saturate(180%)- 半透明RGBAカラー
これにより背景と自然に融合するUIになります。
アクティブタブのピルアニメーション
選択中タブは「pill」と呼ばれる背景要素がスライドして追従します。
GSAPで以下をアニメーション:
- left位置
- width
- opacity
タブサイズに自動追従するため、レスポンシブでも崩れません。
Hoverインジケーター
各パネルにはホバー追従UIが追加されています。
処理内容:
- pointeroverで対象item取得
- 要素サイズを計算
- GSAPで背景を移動
hover時に項目全体がハイライトされ、操作対象が明確になります。
カスタム方法
パネル追加
<section class="kumonosu-panel" data-panel="new">
data-panel名をタブ側と一致させるだけで追加可能です。
タブ追加
<button class="kumonosu-tab" data-panel="new">
JS修正なしで動作します。
カラー変更
:root {
--kumonosu-bg-dock:
--kumonosu-pill:
}
変数設計のためテーマ変更が容易です。
Dockサイズ変更
.kumonosu-panel-stack {
width: 360px;
}
数値変更のみで全体幅を調整できます。
アニメーション速度調整
--kumonosu-duration: 0.35s;
UI全体の体感速度を一括制御できます。
注意点
backdrop-filterの対応
古いブラウザではblurが効かない場合があります。
フォールバック背景色を設定すると安全です。
height:autoアニメーション
CSSのみでは完全な高さ補間はできないため、要素量が極端に多い場合はJS制御への変更を検討してください。
GSAP依存
以下ライブラリが必須です。
- GSAP
- Lucide Icons
CDN削除時は動作しません。
position構造
Dockは重なりUIのため:
- z-index管理
- overflow:hidden
- fixed要素との重なり
に注意してください。
このUIが向いている用途
- ブログカテゴリナビ
- ポートフォリオメニュー
- SaaS管理UI
- ドキュメントサイト
- アプリ風Webサイト