きっかけ
CSS の linear-gradient を手で書くと、だいたい linear-gradient(135deg, #ff6b6b, #f5c26b, #c5a3ff) みたいな字面になります。角度と色を入れて調整して、保存してブラウザリロードして、「もうちょっと右寄りに」と調整して、また保存して...
これを ビジュアルで直接いじれる GUI が欲しい。色ピッカー、スライダでのストップ位置調整、プリセットからの出発点、即プレビュー。そしてコピー可能な CSS 文字列を生成。
既存の gradient.io みたいなサービスもありますが、自分で書くと仕様がわかるし、200 行以内に収まることを確認できた。
作ったもの
Gradient Designer — https://sen.ltd/portfolio/gradient-designer/
- linear / radial の切替
- 角度 (linear) または 形状 (circle / ellipse, radial)
- 複数カラーストップ — 追加・削除・色ピッカー・位置スライダ
- 即時プレビュー — 大きな色面でリアルタイム確認
- コピーボタン付き CSS 出力
- 5 プリセット: Sunset / Ocean / Forest / Spotlight / Mono
vanilla JS + HTML + CSS、ゼロ依存、ビルドツール不要。ロジックは 90 行、プリセット 60 行。node --test で 10 ケース。
buildGradient(config) は 15 行
構造化された config から CSS 文字列を組み立てる関数:
export function buildGradient(config) {
if (!config || !Array.isArray(config.stops) || config.stops.length < 2) {
return 'none'
}
const stops = config.stops
.slice()
.sort((a, b) => a.position - b.position)
.map((s) => `${s.color} ${s.position}%`)
.join(', ')
if (config.type === 'radial') {
const shape = config.shape === 'circle' ? 'circle' : 'ellipse'
return `radial-gradient(${shape}, ${stops})`
}
const angle = Number.isFinite(config.angle) ? config.angle : 90
return `linear-gradient(${angle}deg, ${stops})`
}
ポイント:
-
.slice().sort()— 元のstops配列を変更しないよう copy してから並べる。UI 側は position を自由に動かせて、出力時に正規化。 -
ストップが 2 つ未満なら
none— グラデーションにならないので無効値として扱う - 角度のデフォルトは 90° — 一般的な「左から右」方向
たったこれだけで linear / radial 両方対応。CSS の文法がシンプルなので、ビルダーもシンプル。cron と同じく、「構築は簡単、パースが難しい」 の典型例。
ストップを sort してから出力するのが正解
UI で色ストップを追加するとき、位置を 60% にしたり 20% にしたりバラバラにドラッグできるようにしたい。でも CSS 側には位置順にソート済みの状態で渡さないと見た目が崩れます。
config.stops
.slice()
.sort((a, b) => a.position - b.position)
UI 内部は「ストップが追加された順」を保っていて、出力直前にだけソートします。なぜなら:
- ストップの追加・削除を UI でやるとき、挿入順を維持したほうが操作が自然
- CSS 出力は位置順でないと破綻する
- 2 つの状態を独立に持つことで、UI のドラッグ中にフリッカーしない
「内部状態と出力状態を分離する」のは小さな UI を書くときの王道パターン。
即プレビューは textarea への代入
プレビューを描画するには、background プロパティを即座に更新するだけ:
function update() {
const css = buildGradient(state.config)
$('preview').style.background = css
$('output').textContent = `background: ${css};`
}
各 UI イベント(色変更・位置変更・角度変更等)から update() を呼ぶだけで、プレビューとコード出力の両方が更新される。即時フィードバックの UX は vanilla JS でもこれで十分作れる。
React や Vue を持ってきたくなる誘惑がありますが、プレビュー更新が 1 つの要素の style.background を書き換えるだけなら、フレームワークのオーバーヘッドはノイズ。
プリセットは「いい感じの出発点」の提供
5 つのプリセットを用意:
export const PRESETS = [
{
name: 'Sunset',
config: {
type: 'linear',
angle: 135,
stops: [
{ color: '#ff6b6b', position: 0 },
{ color: '#f5c26b', position: 50 },
{ color: '#c5a3ff', position: 100 },
],
},
},
// Ocean / Forest / Spotlight / Mono
]
Sunset / Ocean / Forest はよくあるグラデーション、Spotlight は radial の例、Mono はモノクロ。空の状態からはユーザーが何を触ればいいか分からないので、即座に「こういう形ができるよ」を見せてからカスタマイズさせる、というのがプリセットの役割。
プリセットクリックで state.config = structuredClone(preset.config) して re-render、そこから自由にいじれる。「プリセットをそのまま使う」より「プリセットから始めていじる」のほうが圧倒的に多いので、編集可能なプリセットとして機能する。
コピーボタン
出力 CSS をワンクリックでクリップボードへ:
$('copy').addEventListener('click', async () => {
const css = buildGradient(state.config)
await navigator.clipboard.writeText(`background: ${css};`)
showToast('CSS copied')
})
プレフィックス background: 込みでコピーするのは、VSCode / CodePen に貼り付けてすぐ動くため。background: だけ足す手間を消す、という細かい UX。
テスト
node --test で 10 ケース:
test('basic linear gradient', () => {
const css = buildGradient({
type: 'linear',
angle: 90,
stops: [
{ color: '#000', position: 0 },
{ color: '#fff', position: 100 },
],
})
assert.equal(css, 'linear-gradient(90deg, #000 0%, #fff 100%)')
})
test('radial circle', () => {
const css = buildGradient({
type: 'radial',
shape: 'circle',
stops: [
{ color: '#fff', position: 0 },
{ color: '#000', position: 100 },
],
})
assert.equal(css, 'radial-gradient(circle, #fff 0%, #000 100%)')
})
test('stops are sorted by position', () => {
const css = buildGradient({
type: 'linear',
angle: 0,
stops: [
{ color: '#fff', position: 100 }, // 先に 100%
{ color: '#000', position: 0 }, // 後に 0%
],
})
// should output in order 0% → 100%
assert.ok(css.indexOf('#000 0%') < css.indexOf('#fff 100%'))
})
test('less than 2 stops returns none', () => {
const css = buildGradient({ type: 'linear', angle: 0, stops: [] })
assert.equal(css, 'none')
})
ストップの順序テストは indexOf 比較で書くのがコツ。文字列の部分一致で順序を保証できます。=== で完全一致を要求するより、壊れにくい。
おわりに
SEN 合同会社の ポートフォリオシリーズ 100+ の 20 件目です。
- 📦 レポジトリ: https://github.com/sen-ltd/gradient-designer
- 🌐 ライブデモ: https://sen.ltd/portfolio/gradient-designer/
- 🏢 会社: https://sen.ltd/
conic-gradient 対応は次の改修候補です。
