07Preset Equalizer Whose Slider Value Sets the Bar Response Range
Three slider values for bass, mid, and treble each set the maximum sway range for their bar group, so moving a slider makes only that group bars swing within the newly computed range.
Best forSound-settings panels and podcast apps
BAND_RESPONSE_RANGEsynthmakeLcg

<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<symbol id="ico-play" viewBox="0 0 256 256"><g fill="currentColor"><path d="M228.23 134.69L84.15 222.81A8 8 0 0 1 72 216.12V39.88a8 8 0 0 1 12.15-6.69l144.08 88.12a7.82 7.82 0 0 1 0 13.38" opacity=".2"/><path d="M232.4 114.49L88.32 26.35a16 16 0 0 0-16.2-.3A15.86 15.86 0 0 0 64 39.87v176.26A15.94 15.94 0 0 0 80 232a16.07 16.07 0 0 0 8.36-2.35l144.04-88.14a15.81 15.81 0 0 0 0-27ZM80 215.94V40l143.83 88Z"/></g></symbol>
<symbol id="ico-pause" viewBox="0 0 256 256"><g fill="currentColor"><path d="M208 48v160a8 8 0 0 1-8 8h-40a8 8 0 0 1-8-8V48a8 8 0 0 1 8-8h40a8 8 0 0 1 8 8M96 40H56a8 8 0 0 0-8 8v160a8 8 0 0 0 8 8h40a8 8 0 0 0 8-8V48a8 8 0 0 0-8-8" opacity=".2"/><path d="M200 32h-40a16 16 0 0 0-16 16v160a16 16 0 0 0 16 16h40a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16m0 176h-40V48h40ZM96 32H56a16 16 0 0 0-16 16v160a16 16 0 0 0 16 16h40a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16m0 176H56V48h40Z"/></g></symbol>
</defs>
</svg>
<main class="stage">
<div class="peq is-demo is-playing" id="peq">
<p class="peq__hint"><span>Raise a slider value and that band's bar group sways with a wider response range.</span></p>
<div class="peq__panel">
<button class="peq__play" id="play" type="button" aria-label="Play">
<svg class="peq__ico peq__ico--play" aria-hidden="true"><use href="#ico-play"></use></svg>
<svg class="peq__ico peq__ico--pause" aria-hidden="true"><use href="#ico-pause"></use></svg>
</button>
<div class="peq__bands" id="bands">
<div class="peq__band" data-band="0">
<div class="peq__bars" data-bars="0" aria-hidden="true"></div>
<input class="peq__slider" type="range" min="0" max="100" value="30" id="slider-0" aria-label="Bass response range">
<span class="peq__label">Bass</span>
</div>
<div class="peq__band" data-band="1">
<div class="peq__bars" data-bars="1" aria-hidden="true"></div>
<input class="peq__slider" type="range" min="0" max="100" value="30" id="slider-1" aria-label="Mid response range">
<span class="peq__label">Mid</span>
</div>
<div class="peq__band" data-band="2">
<div class="peq__bars" data-bars="2" aria-hidden="true"></div>
<input class="peq__slider" type="range" min="0" max="100" value="30" id="slider-2" aria-label="Treble response range">
<span class="peq__label">Treble</span>
</div>
</div>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹(슬라이더 값 → 막대 반응폭)은 index.html 의 render() 에 있다. 여기는 무대·레이아웃만.
$duration: $dur-base;
$easing: $ease-out;
$color: $subject-ink;
@include stage($stage-orange);
.peq {
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-2;
width: min(400px, 92vw);
}
.peq__hint {
margin: 0;
color: $subject-ink;
font-size: 12px;
font-weight: 500;
line-height: 1.35;
text-align: center;
}
.peq__panel {
width: 100%;
border-radius: $r-card;
background: rgba($subject-ink, .07);
box-shadow: 0 0 0 1px rgba($subject-ink, .16);
padding: $sp-3;
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-2;
}
.peq__play {
flex: 0 0 auto;
width: $sp-8;
height: $sp-8;
border-radius: $r-pill;
border: none;
background: $color;
color: $stage-orange;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: transform $duration $easing;
}
.peq__play:hover { transform: scale(1.06); }
.peq__play:focus-visible { outline: 2px solid $subject-ink; outline-offset: 2px; }
.peq__play:active { transform: scale(.96); }
.peq__ico { width: 16px; height: 16px; }
.peq__ico--pause { display: none; }
.is-playing .peq__ico--play { display: none; }
.is-playing .peq__ico--pause { display: block; }
.peq__bands {
display: flex;
align-items: flex-start;
justify-content: center;
gap: $sp-4;
width: 100%;
}
.peq__band {
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-1;
flex: 1 1 0;
min-width: 0;
}
.peq__bars {
height: 80px;
width: 100%;
display: flex;
align-items: flex-end;
justify-content: center;
gap: 4px;
}
.peq__bar {
flex: 1 1 auto;
max-width: 8px;
height: 100%;
border-radius: $r-xs $r-xs 0 0;
background: $color;
transform: scaleY(.3);
transform-origin: bottom center; // 막대는 바닥이 기준점 — 중심 기준이 아니다
}
.peq__slider {
width: 100%;
accent-color: $color;
}
.peq__label {
font-size: 11px;
font-weight: 600;
color: rgba($subject-ink, .78);
}
@media (max-width: 340px) {
.peq__panel { padding: 4px; gap: 4px; }
.peq__play { width: 28px; height: 28px; }
.peq__bands { gap: 4px; }
.peq__bars { height: 40px; }
.peq__label { font-size: 9px; }
}
@media (prefers-reduced-motion: reduce) {
.peq__play { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.peq {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
width: min(400px, 92vw);
}
.peq__hint {
margin: 0;
color: #17141a;
font-size: 12px;
font-weight: 500;
line-height: 1.35;
text-align: center;
}
.peq__panel {
width: 100%;
border-radius: 16px;
background: rgba(23, 20, 26, 0.07);
box-shadow: 0 0 0 1px rgba(23, 20, 26, 0.16);
padding: 12px;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.peq__play {
flex: 0 0 auto;
width: 32px;
height: 32px;
border-radius: 999px;
border: none;
background: #17141a;
color: #ff4d1f;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: transform 300ms ease-out;
}
.peq__play:hover {
transform: scale(1.06);
}
.peq__play:focus-visible {
outline: 2px solid #17141a;
outline-offset: 2px;
}
.peq__play:active {
transform: scale(0.96);
}
.peq__ico {
width: 16px;
height: 16px;
}
.peq__ico--pause {
display: none;
}
.is-playing .peq__ico--play {
display: none;
}
.is-playing .peq__ico--pause {
display: block;
}
.peq__bands {
display: flex;
align-items: flex-start;
justify-content: center;
gap: 16px;
width: 100%;
}
.peq__band {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
flex: 1 1 0;
min-width: 0;
}
.peq__bars {
height: 80px;
width: 100%;
display: flex;
align-items: flex-end;
justify-content: center;
gap: 4px;
}
.peq__bar {
flex: 1 1 auto;
max-width: 8px;
height: 100%;
border-radius: 4px 4px 0 0;
background: #17141a;
transform: scaleY(0.3);
transform-origin: bottom center;
}
.peq__slider {
width: 100%;
accent-color: #17141a;
}
.peq__label {
font-size: 11px;
font-weight: 600;
color: rgba(23, 20, 26, 0.78);
}
@media (max-width: 340px) {
.peq__panel {
padding: 4px;
gap: 4px;
}
.peq__play {
width: 28px;
height: 28px;
}
.peq__bands {
gap: 4px;
}
.peq__bars {
height: 40px;
}
.peq__label {
font-size: 9px;
}
}
@media (prefers-reduced-motion: reduce) {
.peq__play {
transition: none;
}
}(function () {
var root = document.getElementById('peq');
var playBtn = document.getElementById('play');
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var isDemo = true;
// 결정론 난수 — Math.random 을 쓰지 않는다. LCG(선형합동생성기) 하나로 막대별 위상·기준 높이를 만든다.
function makeLcg(seed) {
var s = seed >>> 0;
return function () { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
}
var BAND_COUNT = 3;
var BARS_PER_BAND = 6;
var BAND_RESPONSE_RANGE = 1.6; // 슬라이더 반응폭 배율 상한 — 변형: 키우면 슬라이더를 움직였을 때 막대 그룹이 흔들리는 높이 차이가 더 커진다
var rng = makeLcg(3307);
var sliders = [];
var bands = [];
for (var b = 0; b < BAND_COUNT; b++) {
var barsEl = document.querySelector('[data-bars="' + b + '"]');
var bars = [];
for (var i = 0; i < BARS_PER_BAND; i++) {
var el = document.createElement('div');
el.className = 'peq__bar';
barsEl.appendChild(el);
bars.push({
el: el,
f1: 0.5 + rng() * 0.35,
p1: rng() * Math.PI * 2,
f2: 1.3 + rng() * 0.6,
p2: rng() * Math.PI * 2,
w2: 0.2 + rng() * 0.2,
base: 0.32 + rng() * 0.14
});
}
bands.push(bars);
sliders.push(document.getElementById('slider-' + b));
}
function synth(bar, tSec) {
var s1 = Math.sin(tSec * bar.f1 * Math.PI * 2 + bar.p1);
var s2 = Math.sin(tSec * bar.f2 * Math.PI * 2 + bar.p2) * bar.w2;
return (s1 + s2) / (1 + bar.w2); // -1..1
}
function human() { if (isDemo) { root.classList.remove('is-demo'); isDemo = false; } }
var playing = true, t0 = 0;
function render(now) {
var tSec = (now - t0) / 1000;
var effPlaying = isDemo ? true : playing;
for (var b = 0; b < BAND_COUNT; b++) {
var norm = Number(sliders[b].value) / 100; // 0..1
var amp = norm * BAND_RESPONSE_RANGE;
var bars = bands[b];
for (var i = 0; i < bars.length; i++) {
var bar = bars[i];
var sway = effPlaying ? synth(bar, tSec) : 0;
var h = bar.base + sway * amp * 0.5;
h = Math.max(0.06, Math.min(1, h));
// 막대는 바닥이 기준점이라 아래쪽 원점으로 세로로만 자란다 — 중심 기준이 아닌 실제 앵커 지점
bar.el.style.transform = 'scaleY(' + h + ')';
}
}
}
function setPlaying(next) {
playing = next;
root.classList.toggle('is-playing', next);
}
function onPlayClick() { human(); setPlaying(!playing); }
function onPlayKeydown(e) {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); human(); setPlaying(!playing); }
}
function onSliderInput() { human(); }
sliders.forEach(function (s) { s.addEventListener('input', onSliderInput); });
var driveStart = 0;
function triangle(frac) { return frac < 0.5 ? frac * 2 : 2 - frac * 2; }
function frame(now) {
if (!t0) t0 = now;
if (isDemo) {
if (!driveStart) driveStart = now;
var DRIVE_PERIOD_MS = 2000; // 렌더 캐처 창(설정 초=2.0)과 같은 주기 — 시연 전용
for (var b = 0; b < BAND_COUNT; b++) {
var phase = b / BAND_COUNT;
var frac = (((now - driveStart) / DRIVE_PERIOD_MS) + phase) % 1;
sliders[b].value = String(Math.round(triangle(frac) * 100));
}
}
render(now);
requestAnimationFrame(frame);
}
if (reduce) {
root.classList.add('is-still');
t0 = performance.now();
render(t0);
playBtn.addEventListener('click', function () { human(); setPlaying(!playing); render(performance.now()); });
playBtn.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); human(); setPlaying(!playing); render(performance.now()); }
});
sliders.forEach(function (s) { s.addEventListener('input', function () { human(); render(performance.now()); }); });
} else {
playBtn.addEventListener('click', onPlayClick);
playBtn.addEventListener('keydown', onPlayKeydown);
requestAnimationFrame(frame);
}
})();import { useEffect, useRef } from "react";
import styles from "./EqPresetSliders.module.scss";
type Bar = { el: HTMLDivElement | null; f1: number; p1: number; f2: number; p2: number; w2: number; base: number };
const BANDS = [
{ label: "저음", aria: "저음 반응폭" },
{ label: "중음", aria: "중음 반응폭" },
{ label: "고음", aria: "고음 반응폭" },
];
export default function EqPresetSliders() {
const rootRef = useRef<HTMLDivElement | null>(null);
const playRef = useRef<HTMLButtonElement | null>(null);
const barsElRefs = useRef<(HTMLDivElement | null)[]>([]);
const sliderRefs = useRef<(HTMLInputElement | null)[]>([]);
useEffect(() => {
const BAND_COUNT = 3;
const root = rootRef.current;
const playBtn = playRef.current;
const barsEls = (barsElRefs.current || []).filter((b): b is HTMLDivElement => !!b);
const sliders = (sliderRefs.current || []).filter((s): s is HTMLInputElement => !!s);
if (!root || !playBtn || barsEls.length !== BAND_COUNT || sliders.length !== BAND_COUNT) return;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let isDemo = true;
// 결정론 난수 — Math.random 을 쓰지 않는다. LCG(선형합동생성기) 하나로 막대별 위상·기준 높이를 만든다.
function makeLcg(seed: number) {
let s = seed >>> 0;
return function () { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
}
const BARS_PER_BAND = 6;
const BAND_RESPONSE_RANGE = 1.6; // 슬라이더 반응폭 배율 상한 — 변형: 키우면 슬라이더를 움직였을 때 막대 그룹이 흔들리는 높이 차이가 더 커진다
const rng = makeLcg(3307);
const bands: Bar[][] = [];
for (let b = 0; b < BAND_COUNT; b++) {
const barsEl = barsEls[b]!;
const bars: Bar[] = [];
for (let i = 0; i < BARS_PER_BAND; i++) {
const el = document.createElement("div");
el.className = styles.bar;
barsEl.appendChild(el);
bars.push({
el,
f1: 0.5 + rng() * 0.35,
p1: rng() * Math.PI * 2,
f2: 1.3 + rng() * 0.6,
p2: rng() * Math.PI * 2,
w2: 0.2 + rng() * 0.2,
base: 0.32 + rng() * 0.14,
});
}
bands.push(bars);
}
function synth(bar: Bar, tSec: number) {
const s1 = Math.sin(tSec * bar.f1 * Math.PI * 2 + bar.p1);
const s2 = Math.sin(tSec * bar.f2 * Math.PI * 2 + bar.p2) * bar.w2;
return (s1 + s2) / (1 + bar.w2); // -1..1
}
function human() { if (isDemo) { root!.classList.remove(styles["is-demo"]); isDemo = false; } }
let playing = true, t0 = 0;
function render(now: number) {
const tSec = (now - t0) / 1000;
const effPlaying = isDemo ? true : playing;
for (let b = 0; b < BAND_COUNT; b++) {
const norm = Number(sliders[b]!.value) / 100; // 0..1
const amp = norm * BAND_RESPONSE_RANGE;
const bars = bands[b];
for (let i = 0; i < bars.length; i++) {
const bar = bars[i];
const sway = effPlaying ? synth(bar, tSec) : 0;
let h = bar.base + sway * amp * 0.5;
h = Math.max(0.06, Math.min(1, h));
// 막대는 바닥이 기준점이라 아래쪽 원점으로 세로로만 자란다 — 중심 기준이 아닌 실제 앵커 지점
bar.el!.style.transform = "scaleY(" + h + ")";
}
}
}
function setPlaying(next: boolean) {
playing = next;
root!.classList.toggle(styles["is-playing"], next);
}
function onPlayClick() { human(); setPlaying(!playing); }
function onPlayKeydown(e: KeyboardEvent) {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); human(); setPlaying(!playing); }
}
function onSliderInput() { human(); }
function onReduceSliderInput() { human(); render(performance.now()); }
sliders.forEach((s) => s!.addEventListener("input", reduce ? onReduceSliderInput : onSliderInput));
let driveStart = 0;
function triangle(frac: number) { return frac < 0.5 ? frac * 2 : 2 - frac * 2; }
let raf = 0;
function frame(now: number) {
if (!t0) t0 = now;
if (isDemo) {
if (!driveStart) driveStart = now;
const DRIVE_PERIOD_MS = 2000; // 렌더 캐처 창(설정 초=2.0)과 같은 주기 — 시연 전용
for (let b = 0; b < BAND_COUNT; b++) {
const phase = b / BAND_COUNT;
const frac = ((now - driveStart) / DRIVE_PERIOD_MS + phase) % 1;
sliders[b]!.value = String(Math.round(triangle(frac) * 100));
}
}
render(now);
raf = requestAnimationFrame(frame);
}
function onReduceClick() { human(); setPlaying(!playing); render(performance.now()); }
function onReduceKeydown(e: KeyboardEvent) {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); human(); setPlaying(!playing); render(performance.now()); }
}
if (reduce) {
root.classList.add(styles["is-still"]);
t0 = performance.now();
render(t0);
playBtn.addEventListener("click", onReduceClick);
playBtn.addEventListener("keydown", onReduceKeydown);
} else {
playBtn.addEventListener("click", onPlayClick);
playBtn.addEventListener("keydown", onPlayKeydown);
raf = requestAnimationFrame(frame);
}
return () => {
cancelAnimationFrame(raf);
sliders.forEach((s) => s!.removeEventListener("input", reduce ? onReduceSliderInput : onSliderInput));
if (reduce) {
playBtn.removeEventListener("click", onReduceClick);
playBtn.removeEventListener("keydown", onReduceKeydown);
} else {
playBtn.removeEventListener("click", onPlayClick);
playBtn.removeEventListener("keydown", onPlayKeydown);
}
barsEls.forEach((el) => { if (el) el.innerHTML = ""; });
};
}, []);
return (
<main className={styles.stage}>
<div className={styles.peq + " " + styles["is-demo"] + " " + styles["is-playing"]} ref={rootRef}>
<p className={styles.hint}><span>슬라이더 값을 올리면 그 대역 막대 그룹이 흔들리는 반응폭이 커집니다</span></p>
<div className={styles.panel}>
<button className={styles.play} ref={playRef} type="button" aria-label="재생">
<svg className={`${styles.ico} ${styles.icoPlay}`} aria-hidden="true"><use href="#ico-play" /></svg>
<svg className={`${styles.ico} ${styles.icoPause}`} aria-hidden="true"><use href="#ico-pause" /></svg>
</button>
<div className={styles.bands}>
{BANDS.map((band, i) => (
<div className={styles.band} key={band.label} data-band={i}>
<div className={styles.bars} ref={(el: HTMLDivElement | null) => { if (barsElRefs.current) barsElRefs.current[i] = el; }} aria-hidden="true" />
<input
className={styles.slider}
type="range"
min={0}
max={100}
defaultValue={30}
ref={(el: HTMLInputElement | null) => { if (sliderRefs.current) sliderRefs.current[i] = el; }}
aria-label={band.aria}
/>
<span className={styles.label}>{band.label}</span>
</div>
))}
</div>
</div>
</div>
</main>
);
}07 Preset Equalizer Whose Slider Value Sets the Bar Response Range — Measured area 9.767% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · Custom Audio Player CSS — 9 Computed UI Patterns
The post this demo lives in
Custom Audio Player CSS — 9 Computed UI PatternsAmong nine custom audio player css demos, one precomputes bar heights from sine waves, one decays angular velocity so th See all demos in this post
If you like this effect - similar demos
Creative UI01 Bar spectrumAudio Visualizer JavaScript: 9 Parts, No Library
Creative UI08 Harmonic slider — how the drawing blurs as you remove circlesFourier Epicycles, 9 Ways (No Library)
Creative UI06 Burning Ship variant — one absolute value turns the set into a ship ablazeMandelbrot Zoom, 9 Ways (Escape-Time, No Library)