直接在這裡試搖桿
這個搖桿看起來像小型 3D 元件,但實際上是平面的 HTML layer 疊出來的。立體感來自漸層、內陰影、外陰影、SVG 箭頭,以及由滑鼠/觸控控制的 CSS 變數。
你要做出什麼
你要做的是一個可以拖曳的 analog joystick UI。它看起來有凸起、有凹槽、像可以被按壓,但整個視覺其實都是平面圓形疊出來的。
這裡沒有 Canvas、沒有 WebGL、也沒有用圖片貼圖。核心是五個視覺 layer:外圈、暗色凹槽、內層底座、蘑菇頭、頂部細節。
如果你想看更大的版本和完整 HTML,可以打開 demo + code page。
複製 HTML 結構
HTML 結構刻意保持簡單。四個方向是實際的 button,中間則是幾個圓形 layer 疊在一起。
<button class="control top" aria-label="Move up">...</button>
<button class="control right" aria-label="Move right">...</button>
<button class="control bottom" aria-label="Move down">...</button>
<button class="control left" aria-label="Move left">...</button>
<div class="around">
<div class="handle">
<div class="button-wrapper">
<span class="inside">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</span>
</div>
</div>
</div>四個 button.control 建議保留成真的 button。這樣後面要接方向控制更直覺,也比較符合 accessibility。
around::before 會額外補出一個視覺圓形,不需要再多寫一層 HTML。這個 pseudo-element 會變成中間的暗色凹槽。
做出假 3D 深度
這個立體感不是靠單一技巧,而是一整套高光與陰影的組合。
.around用上下漸層做出外圈斜面。.around::before把中心壓暗,讓它看起來像凹槽。.handle讓內層底座有上亮下暗的厚度。.button-wrapper是蘑菇頭,也是最強浮起陰影的地方。.inside負責頂面、內部高光和小圓點細節。
.around {
background-image: linear-gradient(0deg, #f5f8fa, #9da4a8);
}
.button-wrapper {
background-image: linear-gradient(180deg, #adb9bf, #d4dbdd);
box-shadow:
0 -12px 10px rgba(255, 255, 255, 0.5),
0 9px 14px rgba(0, 0, 0, 0.5),
inset 0 10px 13px rgba(255, 255, 255, 0.72),
inset 0 -14px 18px rgba(86, 101, 108, 0.44);
}關鍵概念是:高光要在上方,重一點的陰影要在物件下方。只要光源方向一致,平面的圓形就會開始有物理感。
蘑菇頭下方一定要有貼近的陰影;沒有這層,它會像貼上去的圖,而不是浮起來的按鈕。頂面則用內部高光和底部內陰影來暗示厚度。
加上箭頭與細節
立體感成立之後,再加方向箭頭和四個小圓點。這些細節很小,但會讓它從裝飾圖案變成真正像控制器的 UI。
箭頭建議用 SVG,這樣任何尺寸都清楚。每個箭頭外層保留 button,再根據方向更新 active 顏色。
- 閒置箭頭:中性灰色。
- 按下方向:變成暖色 active 狀態。
- 斜向拖曳:兩個方向可以同時亮起,並依照力道有深淺。
- 小圓點:低對比、微微內凹,不要搶主視覺。
方向狀態最好根據 joystick vector 更新,不要只做固定的 button active 切換。
做成可拖曳
JavaScript 不需要負責畫出搖桿。它只要讀取 pointer 位置,把它轉成 x 和 y 向量,限制在圓形範圍內,再寫入 CSS 變數即可。
.button-wrapper {
transform: translate(var(--joy-x), var(--joy-y)) scale(var(--joy-scale));
}
.handle {
background:
radial-gradient(circle at calc(50% + var(--well-light-x)) calc(42% + var(--well-light-y)), ...),
radial-gradient(circle at calc(50% + var(--well-dark-x)) calc(58% + var(--well-dark-y)), ...),
linear-gradient(...);
}setJoystick(x, y); // x and y should stay between -1 and 1. // Clamp the drag distance to a circular range before writing CSS variables.
接著用向量決定每個箭頭要亮多深:
- 上:
Math.max(0, -y) - 右:
Math.max(0, x) - 下:
Math.max(0, y) - 左:
Math.max(0, -x)
蘑菇頭移動時,底座凹槽的光影和蘑菇頭陰影要同步變化,拖曳感才會可信。
可以調整的地方
demo 跑起來後,可以先調這些地方:
- 尺寸:調整外層尺寸,並讓內部圓形等比例縮放。
- 深度:先調 shadow offset 和 blur,再改顏色。
- 移動感:如果蘑菇頭太鬆,就縮小 drag radius。
- Active color:把箭頭亮起顏色換成你的 UI 主色。
- 觸控手感:用
transform,不要用left或top,動畫會更順。 - Accessibility:方向控制保留成有 label 的真 button。
常見錯誤:所有陰影都太糊、光源方向不一致、用截圖當貼圖,或是把箭頭做成純裝飾而不是控制元件。
感謝來源
感謝這次復刻背後的原始視覺與拆解參考。
- 按鈕視覺設計來源:Pinterest source
- Joystick 拆解參考:original Douyin video