JS 打造丝滑手风琴
2025年9月8日 11:54
手风琴菜单是后台与官网的常客,但 90% 的实现依赖第三方库或 CSS Transition。今天原生 JS 手写一条「高度动画 + 状态机」的完整链路,打造丝滑手风琴效果。
效果预览
一、核心思路
- 高度动画:把
height: 0 ↔ 实际高度
交给逐帧函数createAnimation
- 状态机:用自定义属性
status="closed|opened|playing"
避免并发点击 - 复用:任意html代码结构插上即用,零配置
二、代码速览
1.动画引擎(animate.js)
function createAnimation({ from, to, totalMS = 300, onmove, onend }) {
const dis = (to - from) / (totalMS / 15);
let cur = 0;
const timer = setInterval(() => {
from += dis;
if (++cur >= totalMS / 15) {
from = to;
clearInterval(timer);
onend && onend();
}
onmove(from);
}, 15);
}
实现从一个初始值到目标值的平滑过渡效果,每帧更新一次视图。
函数参数(配置项)
- from:动画起始值(如初始位置、初始透明度等)
- to:动画目标值(最终要达到的数值)
- totalMS:动画总时长(默认 300 毫秒,即 0.3 秒)
- onmove:每帧更新时的回调函数(接收当前动画值,用于实时更新视图,比如 DOM 位置、样式等)
- onend:动画结束时的回调函数(可选,动画完成后执行)
2.交互逻辑(index.js)
const titles = document.querySelectorAll('.menu h2');
const itemHeight = 30;
titles.forEach(title =>
title.addEventListener('click', () => {
const submenu = title.nextElementSibling;
const before = document.querySelector('.submenu[status="opened"]');
before && before !== submenu && closeSubmenu(before);
toggleSubmenu(submenu);
})
);
function openSubmenu(el) {
if (el.getAttribute('status') !== 'closed') return;
el.setAttribute('status', 'playing');
createAnimation({
from: 0,
to: el.children.length * itemHeight,
onmove: h => (el.style.height = h + 'px'),
onend: () => el.setAttribute('status', 'opened'),
});
}
function closeSubmenu(el) {
if (el.getAttribute('status') !== 'opened') return;
el.setAttribute('status', 'playing');
createAnimation({
from: el.children.length * itemHeight,
to: 0,
onmove: h => (el.style.height = h + 'px'),
onend: () => el.setAttribute('status', 'closed'),
});
}
function toggleSubmenu(el) {
const status = el.getAttribute('status');
status === 'opened' ? closeSubmenu(el) : openSubmenu(el);
}
关键设计思路
- 用 status 属性(closed/opened/playing)管理子菜单状态,避免动画过程中重复触发点击事件
- 子菜单高度通过 “选项数量 × 单个高度” 动态计算,适配不同数量的子菜单
- 点击新菜单时自动关闭已打开的菜单,保证同一时间只有一个子菜单处于展开状态
3.样式骨架
<ul class="menu-container">
<li class="menu">
<h2>菜单1</h2>
<ul class="submenu">
<li>菜单1</li>
<li>菜单2</li>
<li>菜单3</li>
<li>菜单4</li>
</ul>
</li>
<li class="menu">
<h2>菜单2</h2>
<ul class="submenu">
<li>菜单1</li>
<li>菜单2</li>
<li>菜单3</li>
<li>菜单4</li>
</ul>
</li>
<li class="menu">
<h2>菜单3</h2>
<ul class="submenu">
<li>菜单1</li>
<li>菜单2</li>
<li>菜单3</li>
<li>菜单4</li>
</ul>
</li>
<li class="menu">
<h2>菜单4</h2>
<ul class="submenu">
<li>菜单1</li>
<li>菜单2</li>
<li>菜单3</li>
<li>菜单4</li>
</ul>
</li>
</ul>
三、状态机流程图
click ──► status=playing ──► height 0→n ──► status=opened
│ ▲
└─► 再次 click ──► height n→0 ──► status=closed
总结
高度动画 + 状态机 + 事件委托,让手风琴在任何项目里「开箱即合」。