09Bar response — a button where several bars' height values jump by differing amounts on each click before settling
On each click, the height value of each of several arranged bars jumps by a differing amount, and over time each bar's height value decreases until it settles back to its starting height.
Best forA sound-toggle button, a play/pause button
DECAY=0.14MIN_H=0.14hit

<main class="stage">
<div class="ctavum is-demo" id="ctavum">
<div class="ctavum__panel">
<button class="ctavum__btn" id="ctavumBtn" type="button">
<span class="ctavum__bars" id="ctavumBars"></span>
<span class="ctavum__label">Turn on notification sound</span>
</button>
<span class="ctavum__hint">Each click makes the bars jump then settle</span>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹(피크-감쇠 곡선)은 JS 에 있다 — 여기는 정적 배치만.
@include stage($stage-ink);
.stage { position: relative; }
.ctavum__panel {
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-4;
padding: $sp-5;
border-radius: $r-card;
background: $tick-panel;
}
.ctavum__btn {
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-3;
border: 0;
border-radius: $r-control;
background: $subject-ink;
padding: $sp-4 $sp-5;
cursor: pointer;
}
.ctavum__bars {
display: flex;
align-items: flex-end;
gap: 4px;
height: 40px;
}
.ctavum__bar {
width: 6px;
height: 14%;
border-radius: $r-xs;
background: $tick-up;
}
.ctavum__bar:nth-child(4),
.ctavum__bar:nth-child(5) {
background: $tick-down;
}
.ctavum__label {
color: $subject-cream;
font-family: inherit;
font-weight: 700;
font-size: 13px;
}
.ctavum__hint {
color: $tick-mute;
font-size: 12px;
text-align: center;
}
@media (max-width: 340px) {
.ctavum__panel { padding: $sp-4; gap: $sp-3; }
.ctavum__btn { padding: $sp-3 $sp-4; }
.ctavum__bars { height: 34px; }
}
@media (prefers-reduced-motion: reduce) {
.ctavum__bar { transition: none !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.stage {
position: relative;
}
.ctavum__panel {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 20px;
border-radius: 16px;
background: #231f28;
}
.ctavum__btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
border: 0;
border-radius: 12px;
background: #17141a;
padding: 16px 20px;
cursor: pointer;
}
.ctavum__bars {
display: flex;
align-items: flex-end;
gap: 4px;
height: 40px;
}
.ctavum__bar {
width: 6px;
height: 14%;
border-radius: 4px;
background: #3ddc97;
}
.ctavum__bar:nth-child(4),
.ctavum__bar:nth-child(5) {
background: #ff7a5e;
}
.ctavum__label {
color: #fff7e6;
font-family: inherit;
font-weight: 700;
font-size: 13px;
}
.ctavum__hint {
color: #9a93a6;
font-size: 12px;
text-align: center;
}
@media (max-width: 340px) {
.ctavum__panel {
padding: 16px;
gap: 12px;
}
.ctavum__btn {
padding: 12px 16px;
}
.ctavum__bars {
height: 34px;
}
}
@media (prefers-reduced-motion: reduce) {
.ctavum__bar {
transition: none !important;
}
}(function () {
var root = document.getElementById('ctavum');
var btn = document.getElementById('ctavumBtn');
var barsWrap = document.getElementById('ctavumBars');
var N = 5;
var bars = [];
for (var i = 0; i < N; i++) {
var el = document.createElement('span');
el.className = 'ctavum__bar';
barsWrap.appendChild(el);
bars.push({ el: el, h: 0.14 });
}
// 계산 겹: 클릭마다 막대별 목표 높이 값을 서로 다른 난수로 정하고, 매 프레임
// 현재 높이에서 목표까지 감쇠(DECAY)시켜 실제 VU 미터의 피크-감쇠 곡선을 흉내낸다.
var DECAY = 0.14, MIN_H = 0.14;
var raf = null;
function render() {
for (var i = 0; i < N; i++) {
bars[i].el.style.height = (bars[i].h * 100).toFixed(1) + '%';
}
}
function tick() {
var moving = false;
for (var i = 0; i < N; i++) {
var b = bars[i];
var diff = MIN_H - b.h;
if (b.h > MIN_H) {
b.h += diff * DECAY;
if (b.h - MIN_H > 0.004) { moving = true; } else { b.h = MIN_H; }
}
}
render();
if (moving) { raf = requestAnimationFrame(tick); } else { raf = null; }
}
function hit() {
for (var i = 0; i < N; i++) {
bars[i].h = Math.max(bars[i].h, MIN_H + Math.random() * 0.82);
}
render();
if (!raf) { raf = requestAnimationFrame(tick); }
}
function wireReal() {
btn.addEventListener('click', function () { root.classList.remove('is-demo'); stopAutoplay(); hit(); });
}
wireReal();
render();
var autoplayTimer = null;
function stopAutoplay() { if (autoplayTimer) { clearInterval(autoplayTimer); autoplayTimer = null; } }
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!reduce) { hit(); autoplayTimer = setInterval(hit, 420); }
})();import { useEffect, useRef } from "react";
import styles from "./CtaBtnVuMeterTap.module.scss";
export type CtaBtnVuMeterTapProps = {
color?: string;
};
const N = 5;
export default function CtaBtnVuMeterTap({
color = "#4cd4a6",
}: CtaBtnVuMeterTapProps) {
const rootRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const root = rootRef.current;
if (!root) return;
const btn = root.querySelector<HTMLButtonElement>(`.${styles.btn}`);
const barsWrap = root.querySelector<HTMLSpanElement>(`.${styles.bars}`);
const hint = root.querySelector<HTMLSpanElement>(`.${styles.hint}`);
if (!btn || !barsWrap || !hint) return;
const bars: { el: HTMLSpanElement; h: number }[] = [];
for (let i = 0; i < N; i++) {
const el = document.createElement("span");
el.className = styles.bar;
barsWrap.appendChild(el);
bars.push({ el, h: 0.14 });
}
// 계산 겹: 클릭마다 막대별 목표 높이 값을 서로 다른 난수로 정하고, 매 프레임
// 현재 높이에서 목표까지 감쇠(DECAY)시켜 실제 VU 미터의 피크-감쇠 곡선을 흉내낸다.
const DECAY = 0.14;
const MIN_H = 0.14;
let raf: number | null = null;
function render() {
for (let i = 0; i < N; i++) {
bars[i].el.style.height = (bars[i].h * 100).toFixed(1) + "%";
}
}
function tick() {
let moving = false;
for (let i = 0; i < N; i++) {
const b = bars[i];
const diff = MIN_H - b.h;
if (b.h > MIN_H) {
b.h += diff * DECAY;
if (b.h - MIN_H > 0.004) { moving = true; } else { b.h = MIN_H; }
}
}
render();
raf = moving ? requestAnimationFrame(tick) : null;
}
function hit() {
for (let i = 0; i < N; i++) {
bars[i].h = Math.max(bars[i].h, MIN_H + Math.random() * 0.82);
}
render();
if (raf === null) { raf = requestAnimationFrame(tick); }
}
function onClick() { stopAutoplay(); hit(); }
btn.addEventListener("click", onClick);
render();
let autoplayTimer: number | null = null;
function stopAutoplay() { if (autoplayTimer !== null) { window.clearInterval(autoplayTimer); autoplayTimer = null; } }
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (!reduce) { hit(); autoplayTimer = window.setInterval(hit, 420); }
return () => {
stopAutoplay();
if (raf !== null) cancelAnimationFrame(raf);
btn.removeEventListener("click", onClick);
};
}, []);
return (
<div ref={rootRef} className={styles.stage}>
<div className={styles.panel}>
<button className={styles.btn} type="button" style={{ background: color }}>
<span className={styles.bars} />
<span className={styles.label}>알림음 켜기</span>
</button>
<span className={styles.hint}>클릭할 때마다 막대가 튀었다가 가라앉습니다</span>
</div>
</div>
);
}09 Bar response — a button where several bars' height values jump by differing amounts on each click before settling — Measured area 0.293% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · CTA Button Design: 9 Interaction Patterns
The post this demo lives in
CTA Button Design: 9 Interaction PatternsOne shadow length value grows and shrinks with hover state, one perimeter progress value depletes to zero over time, and See all demos in this post
If you like this effect - similar demos
Buttons05 The button becomes its own progress bar9 button state css patterns, loading to done
Buttons05 Theme button where the sun and moon trade places9 Icon Button CSS Patterns — Press, Drag, Expand
Buttons09 Snap-to-anchor bounce — the released position value is pulled toward the nearest of several preset anchor values and settles with a bounceButton Press Animation UI: 9 Calculation Patterns