conical-gradients を利用してドーナツ型のノブ型 UI を実装する
初めに
以前出場したハッカソンでノブ型 UI をフルスクラッチで作りました。そこで、簡単な作り方をまとめながらハッカソン中には実装しきれなかった機能も追加してみたいと思います。
完成品は以下になります。(スマホの場合は、ドーナツ上でスクロールしてください)
1. ドーナツを作る
ひとまずドーナツを作成してみます。ドーナツは色々な実装方法があるかと思いますが、今回はradial-gradient
を利用して実装します。
const Home = () => {
return (
<div className="page">
<div className="circle">
100%
</div>
</div>
)
}
:root {
--knob-size: 128px; /* 後から利用するためCSS変数に切り出し */
}
.circle {
background: radial-gradient(circle, transparent 40%, pink 40%);
width: var(--knob-size);
height: var(--knob-size);
border-radius: 100%;
display: grid;
place-items: center;
}
.page {
display: flex;
padding: 4rem;
}
2. パーセントに応じてドーナツを欠けさせる
ハッカソン中は、before
とafter
の擬似要素を使って円の一部を隠すことで対応していました。しかし、conical-gradients
という円錐グラデーションを用いると簡単に実装できますので、そちらをご紹介します。
:root {
--knob-size: 128px;
--knob-occupied-color: #ffc0cb;
--knob-bg-color: #f0f0f0;
--knob-amount: 0.4turn;
}
.circle {
background: radial-gradient(circle, #ffffff 40%, transparent 40%),
conic-gradient(
var(--knob-occupied-color) var(--knob-amount),
var(--knob-bg-color) var(--knob-amount)
);
/* ... */
}
供養のために、ハッカソン時の実装も載せておきます。
ハッカソン中の実装イメージ(供養)
0~180 度までは、before 要素で左半分、after 要素で左半分を隠し、角度に応じて after を回転させることで対応しました。
180 度以降は、before 要素で右側の円グラフを表示し、after 要素を回転させました。
conical-gradients
円錐グラデーションを適用する関数です。
radial-gradient
は円の中心から外側に向かう方向にグラデーションですが、conical-gradients
は円周の方向に向かうグラデーションを定義できます。
この 2 つのグラデーションを組み合わせることで、ドーナツ型の円グラフを実現することができます。
turn
角度を指定する単位としては deg
, rad
などがありますが、turn
は一周を 1
とする単位です。
3. パララックスの実装
次にパララックスを実装します。円グラフをホバーした状態でスクロールすると、円グラフの値が増減するような実装を行います。
3-1. スクロール領域の確保
まずは円グラフ(div.circle
)をdiv.scrollable-area
で囲み、さらにdiv.parallax-window
で囲みます。
div.parallax-window
は円グラフと同じ width と height、div.scrollable-area
は大きめの height を持たせます。こうすることで、div.parallax-window
のなかで、div.scrollable-area
がスクロールされます。
{
/* ↓ height,widthは円グラフ(.circle)と同じ */
}
<div className="parallax-window">
{/* ↓ heightはかなり大きくしておく */}
<div className="scrollable-area">
<div className="circle">100%</div>
</div>
</div>;
以下は分かりやすさのためにdiv.scrollable-area
の背景にグラデーションを設定しています。
export default function App() {
return (
<div className="page">
<div className="parallax-window scrollbar-none">
<div className="circle">100%</div>
<div className="scrollable-area" />
</div>
</div>
);
}
:root {
--knob-size: 128px;
--knob-occupied-color: #ffc0cb;
--knob-bg-color: #f0f0f0;
--knob-amount: 0.4turn;
--knob-scroll-size: 500px;
}
.parallax-window {
width: var(--knob-size);
height: var(--knob-size);
overflow-y: scroll;
}
.scrollable-area {
height: var(--knob-scroll-size);
}
3-2. スクロール量の取得
次にスクロール量を取得し、今まで「100%」と表示していた部分をスクロール量に応じて変化させます。
以下のイメージでは、スクロール部分にグラデーションを設定して分かりやすくしています。また、ボックス上部のパーセンテージ表記も加えています。
import throttle from 'lodash.throttle'
import { useRef, useState } from 'react'
const Home = () => {
const [scroll, setScroll] = useState(0)
const scrollRef = useRef<HTMLDivElement>(null)
const handleScroll = throttle(() => {
if (scrollRef.current == null) return;
const scrollRate =
scrollRef.current.scrollTop /
(scrollRef.current.scrollHeight - scrollRef.current.clientHeight);
// 少数第一位までで四捨五入
setScroll(Math.round(scrollRate * 1000) / 1000);
}, 30);
const roundedScroll = Math.round(scroll * 100);
return (
<div className="page">
<div className="parallax-window" ref={scrollRef} onScroll={handleScroll}>
<div className="circle">
{scroll}%
</div>
<div className="scrollable-area" />
</div>
</div>
)
}
handleScroll
簡単に handleScroll 関数の説明です。
const handleScroll = throttle(() => {
if (scrollRef.current == null) return;
const scrollRate =
scrollRef.current.scrollTop /
(scrollRef.current.scrollHeight - scrollRef.current.clientHeight);
// 少数第一位までで四捨五入
setScroll(Math.round(scrollRate * 1000) / 1000);
}, 100);
onScroll イベントは発生頻度が高く、パフォーマンス上問題となる場合があるので、loadsh の throttle 関数を用いて関数の実行を間引きます。スクロール量はscrollRef.current.scrollTop
、スクロール可能な高さはscrollRef.current.scrollHeight - scrollRef.current.clientHeight
で取得できるので、これらを利用して 0~100 の範囲で正規化しています。
3-3. 円グラフがスクロールされないようにする
これまでの実装では、スクロール量は取得できるものの、スクロールに応じて円グラフが隠れてしまいます。そこで、円グラフにposition: sticky;
を追加し、スクロールの影響を受けないように対策します。
.circle {
...
+ // スクロールエリアの上部に固定
+ position: sticky;
+ top: 0;
}
4. スクロール量をドーナツに反映する
最後にスクロール量をドーナツの円グラフに反映させていきます。まだ単位付き attr
がサポートされていないため、style 属性で CSS 変数の値を設定することで、スクロール量を CSS に渡します。
return (
<div className="page flex flex-col">
<div className="parallax-window" ref={scrollRef} onScroll={handleScroll}>
- <div className="circle">
+ <div className="circle" style={{ '--scroll--amount': `${scroll}turn` }}>
{scroll}%
</div>
<div className="scrollable-area" />
</div>
</div>
)
5. スタイルの整形
最後に、スクロールバーを非表示にします。また、数字部分を monospace とし、表示する値によって文字幅が変化しないようにします。これらのスタイルを適用して完成です!
.circle {
/* ... */
font-variant-numeric: tabular-nums; /* 数字の文字幅を一定に */
cursor: pointer;
}
/* スクロールバーを非表示に */
.scrollbar-none {
overscroll-behavior: none;
scrollbar-width: none;
}
.scrollbar-none::-webkit-scrollbar {
display: none;
}
Discussion