HTML / CSS / JS
アニメーション
2026/03/19
2026/3/16
ナビゲーションのUIに少し遊び心を加えるだけで、サイト全体の印象は大きく変わります。
このサンプルでは、メニューをホバーすると小さなドットが跳ねながら移動するナビゲーションを実装しています。
ただ位置が切り替わるだけではなく、バウンドする動きを加えることで操作に対する反応が直感的に伝わり、可愛さと気持ちよさを両立したUIになります。
CSSで見た目を制御し、JavaScriptでは位置計算のみを行う構成のため、見た目以上にシンプルで応用しやすいのも特徴です。
<header class="kumonosu-header">
<nav class="kumonosu-nav">
<ul class="kumonosu-nav-list">
<li><a href="#kumonosu-section1" class="kumonosu-nav-link">Home</a></li>
<li><a href="#kumonosu-section2" class="kumonosu-nav-link">About</a></li>
<li><a href="#kumonosu-section3" class="kumonosu-nav-link">Services</a></li>
<li><a href="#kumonosu-section4" class="kumonosu-nav-link">Contact</a></li>
</ul>
</nav>
</header>
<section class="kumonosu-container" id="kumonosu-section1">
<img src="https://images.unsplash.com/photo-1497215728101-856f4ea42174?auto=format&fit=crop&w=1920&q=80" alt="Home">
</section>
<section class="kumonosu-container" id="kumonosu-section2">
<img src="https://images.unsplash.com/photo-1522202176988-66273c2fd55f?auto=format&fit=crop&w=1920&q=80" alt="About">
</section>
<section class="kumonosu-container" id="kumonosu-section3">
<img src="https://images.unsplash.com/photo-1519389950473-47ba0277781c?auto=format&fit=crop&w=1920&q=80" alt="Services">
</section>
<section class="kumonosu-container" id="kumonosu-section4">
<img src="https://images.unsplash.com/photo-1423666639041-f56000c27a9a?auto=format&fit=crop&w=1920&q=80" alt="Contact">
</section>
<svg class="kumonosu-filters" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="kumonosu-wave-distort" x="0%" y="0%" width="100%" height="100%">
<feTurbulence type="fractalNoise" baseFrequency="0.0038 0.0038" numOctaves="1" seed="2" result="roughNoise" />
<feGaussianBlur in="roughNoise" stdDeviation="8.5" result="softNoise" />
<feComposite operator="arithmetic" k1="0" k2="1" k3="2" k4="0" in="softNoise" result="mergedMap" />
<feDisplacementMap in="SourceGraphic" in2="mergedMap" scale="-42" xChannelSelector="G" yChannelSelector="G" />
</filter>
</defs>
</svg>
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #000;
}
html, body {
scroll-behavior: smooth;
}
.kumonosu-container {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
background-color: #000;
color: #fff;
overflow: hidden;
position: relative;
}
.kumonosu-container img {
width: 100%;
height: 100%;
object-fit: cover;
filter: brightness(0.7);
}
.kumonosu-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
display: flex;
justify-content: center;
}
.kumonosu-nav {
position: relative;
width: fit-content;
margin-top: 30px;
padding: 0 30px;
border-radius: 16px;
background: rgba(255, 255, 255, 0.4);
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.2),
0 4px 16px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(8px);
}
.kumonosu-nav::before {
content: '';
position: absolute;
inset: 0;
width: 100%;
height: 100%;
backdrop-filter: url(#kumonosu-wave-distort);
z-index: -1;
}
.kumonosu-nav-list {
position: relative;
list-style: none;
display: flex;
justify-content: center;
height: 55px;
isolation: isolate;
padding: 0 15px;
}
.kumonosu-nav-list::after {
content: '';
position: absolute;
left: 0;
bottom: 6px;
width: 12px;
height: 12px;
background: white;
border-radius: 50%;
transform: translateX(var(--kumonosu-translate-x, 0)) translateY(var(--kumonosu-translate-y, 0)) rotate(var(--kumonosu-rotate-x, 0deg));
transition: none;
opacity: 0;
z-index: -1;
box-shadow:
0 4px 16px rgba(255, 255, 255, 0.6),
0 2px 8px rgba(255, 255, 255, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.8);
border: 1px solid rgba(255, 255, 255, 0.8);
}
.kumonosu-show-indicator.kumonosu-nav-list::after {
opacity: 1;
}
.kumonosu-nav-link {
position: relative;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
color: #000;
text-decoration: none;
font-weight: 600;
font-size: 0.95rem;
padding-inline: 20px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.kumonosu-nav-link:hover,
.kumonosu-nav-link.kumonosu-active {
color: #fff;
text-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
transform: translateY(-2px);
}
.kumonosu-filters {
display: none;
}
document.addEventListener('DOMContentLoaded', () => {
const navList = document.querySelector('.kumonosu-nav-list');
const navLinks = document.querySelectorAll('.kumonosu-nav-link');
let anim = null;
let currentActiveItem = null;
const animateIndicator = (from, to) => {
if (anim) clearInterval(anim);
const start = Date.now();
anim = setInterval(() => {
const duration = 500;
const p = Math.min((Date.now() - start) / duration, 1);
const e = 1 - Math.pow(1 - p, 3);
const x = from + (to - from) * e;
const y = -40 * (4 * e * (1 - e));
const r = 200 * Math.sin(p * Math.PI);
navList.style.setProperty('--kumonosu-translate-x', `${x}px`);
navList.style.setProperty('--kumonosu-translate-y', `${y}px`);
navList.style.setProperty('--kumonosu-rotate-x', `${r}deg`);
if (p >= 1) {
clearInterval(anim);
anim = null;
navList.style.setProperty('--kumonosu-translate-y', '0px');
navList.style.setProperty('--kumonosu-rotate-x', '0deg');
}
}, 16);
};
const getCurrentPosition = () => parseFloat(navList.style.getPropertyValue('--kumonosu-translate-x')) || 0;
const getItemCenter = (item) => {
const navRect = navList.getBoundingClientRect();
const itemRect = item.getBoundingClientRect();
return itemRect.left + (itemRect.width / 2) - navRect.left - 6;
};
const moveToItem = (item) => {
const current = getCurrentPosition();
const center = getItemCenter(item);
animateIndicator(current, center);
navList.classList.add('kumonosu-show-indicator');
};
const setActiveItem = (item) => {
if (currentActiveItem) currentActiveItem.classList.remove('kumonosu-active');
currentActiveItem = item;
item.classList.add('kumonosu-active');
moveToItem(item);
};
navLinks.forEach(link => {
link.addEventListener('mouseenter', () => moveToItem(link));
link.addEventListener('mouseleave', () => {
if (currentActiveItem) moveToItem(currentActiveItem);
});
link.addEventListener('click', () => setActiveItem(link));
});
// 初期化
setTimeout(() => {
if (navLinks.length > 0) setActiveItem(navLinks[0]);
}, 100);
// スクロール検知によるアクティブ切り替え
window.addEventListener('scroll', () => {
let currentId = "";
const sections = document.querySelectorAll(".kumonosu-container");
sections.forEach(section => {
const sectionTop = section.offsetTop;
if (pageYOffset >= sectionTop - 100) {
currentId = section.getAttribute("id");
}
});
navLinks.forEach(link => {
if (link.getAttribute("href") === `#${currentId}`) {
if (currentActiveItem !== link) {
if (currentActiveItem) currentActiveItem.classList.remove('kumonosu-active');
currentActiveItem = link;
link.classList.add('kumonosu-active');
const center = getItemCenter(link);
navList.style.setProperty('--kumonosu-translate-x', `${center}px`);
}
}
});
});
});
このナビゲーションでは、メニュー下に表示されるドットが現在位置を示します。
主な動作:
ドットの動きは放物線計算を使い、「ぴょん」と跳ねるような自然なアニメーションになっています。
const y = -40 * (4 * e * (1 - e));
数値を調整:
| 値 | 印象 |
|---|---|
| -20 | 控えめ |
| -40 | 標準(現在) |
| -70 | 元気に跳ねる |
const duration = 500;
例:
300 = サクサク
700 = ふんわり
.kumonosu-nav-list::after {
width: 12px;
height: 12px;
}
おすすめ例:
background: rgba(255,255,255,0.4);
backdrop-filter: blur(8px);
例:
blur(12px);
rgba(255,255,255,0.25);
透明感が強くなります。
const r = 200 * Math.sin(p * Math.PI);
ジャンプ動作はCSSだけでは作りにくいため、JavaScriptで補間計算を行っています。
そのため:
構造になっています。
位置変更は left ではなく:
transform: translate()
を使用しているため、描画パフォーマンスが高く滑らかに動作します。
ナビリンク:
href="#kumonosu-section1"
と
<section id="kumonosu-section1">
を一致させてください。
歪みエフェクト(feTurbulence)は端末によって負荷が上がる場合があります。
必要に応じて削除してもドットアニメーション自体は問題なく動作します。