The Stacki colour picker as a system component — saturation/brightness square, hue (and optional opacity) tracks, hex and RGB/HSL channel fields, and the eyedropper where the browser has one. Ported from the owner's flowtricks/stacki; metrics and drag behaviour are the original's, the skin reads system tokens. Emits mk-color on its root whenever the colour changes, and the root exposes mkSetColor(hex) for programmatic control — the picker knows nothing about what the colour is FOR.
The default — no opacity lane, opening on the reference blue. Drag the square, the hue track, or type in any field; the fields and the square never fight (HSVA is the canonical state, as in the original).
<div class="mk-color-picker mk-color-picker--no-alpha" role="group" aria-label="Color picker" data-color-picker data-initial="#2f6feb">
<div class="mk-color-sb" data-role="sb" tabindex="0" role="slider" aria-label="Saturation and brightness">
<span class="mk-color-sb-thumb" data-role="sbThumb"></span>
</div>
<div class="mk-color-sliders">
<button type="button" class="mk-color-eyedrop is-hidden" data-role="eyedrop" title="Pick a color from the screen" aria-label="Eyedropper">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="m2 22 1-1h3l9-9"></path>
<path d="M3 21v-3l9-9"></path>
<path d="m15 6 3.4-3.4a2.1 2.1 0 1 1 3 3L18 9l.4.4a2.1 2.1 0 1 1-3 3l-3.8-3.8a2.1 2.1 0 1 1 3-3l.4.4Z"></path>
</svg>
</button>
<div class="mk-color-slider-stack">
<div class="mk-color-slider" data-role="hue" tabindex="0" role="slider" aria-label="Hue" aria-valuemin="0" aria-valuemax="360" style="background:linear-gradient(to right,#f00,#ff0 17%,#0f0 33%,#0ff 50%,#00f 67%,#f0f 83%,#f00)">
<span class="mk-color-slider-thumb" data-role="hueThumb"></span>
</div>
</div>
</div>
<div class="mk-color-inputs">
<div class="mk-color-field is-wide">
<input class="mk-color-field-input" data-role="fHex" aria-label="Hex" spellcheck="false">
</div>
<div class="mk-color-channels">
<div class="mk-color-field">
<input class="mk-color-field-input" data-role="c0" aria-label="Channel 1" inputmode="numeric">
</div>
<div class="mk-color-field">
<input class="mk-color-field-input" data-role="c1" aria-label="Channel 2" inputmode="numeric">
</div>
<div class="mk-color-field">
<input class="mk-color-field-input" data-role="c2" aria-label="Channel 3" inputmode="numeric">
</div>
</div>
</div>
<div class="mk-color-modes">
<div class="mk-color-field is-wide">
<button type="button" class="mk-color-mode is-hex is-on" data-role="mHex" aria-pressed="true" title="Write this colour as hex">HEX</button>
</div>
<div class="mk-color-channels">
<button type="button" class="mk-color-mode" data-role="mCh" aria-label="Toggle RGB and HSL" title="Toggle RGB and HSL">
<span data-role="l0">R</span>
<span data-role="l1">G</span>
<span data-role="l2">B</span>
</button>
</div>
</div>
</div>
<script>
// One definition, many instances: the initializer is installed once and every // emitted picker calls it on its own root. Scoped queries, no ids — two // pickers on a page cannot cross wires. if (!window.mkColorPicker) window.mkColorPicker = (function () { 'use strict' var clamp = function (n, lo, hi) { return Math.max(lo, Math.min(hi, n)) } var round = Math.round function hsvaToRgba(k) { var sn = k.s / 100, vn = k.v / 100 var c = vn * sn, x = c * (1 - Math.abs(((k.h / 60) % 2) - 1)), m = vn - c var p = k.h
< 60 ? [c,x,0] : k.h < 120 ? [x,c,0] : k.h < 180 ? [0,c,x] : k.h < 240 ? [0,x,c] : k.h < 300 ? [x,0,c] : [c,0,x] return { r: round((p[0]+m)*255), g: round((p[1]+m)*255), b: round((p[2]+m)*255), a: k.a } } function rgbaToHsva(q) { var rn = q.r/255, gn = q.g/255, bn = q.b/255 var max = Math.max(rn,gn,bn), min = Math.min(rn,gn,bn), d = max - min var h = 0 if (d) { if (max === rn) h = ((gn - bn) / d) % 6 else if (max === gn) h = (bn - rn) / d + 2 else h = (rn - gn) / d + 4 h = (h * 60 + 360) % 360 } return { h: round(h), s: round(max === 0 ? 0 : (d / max) * 100), v: round(max * 100), a: q.a } } function rgbaToHsl(q) { var rn = q.r/255, gn = q.g/255, bn = q.b/255 var max = Math.max(rn,gn,bn), min = Math.min(rn,gn,bn), d = max - min var l = (max + min) / 2, h = 0, s = 0 if (d) { s = d / (1 - Math.abs(2*l - 1)) if (max === rn) h = ((gn - bn) / d) % 6 else if (max === gn) h = (bn - rn) / d + 2 else h = (rn - gn) / d + 4 h = (h * 60 + 360) % 360 } return { h: round(h), s: round(s*100), l: round(l*100) } } function hslToRgba(h, s, l, a) { var sn = s/100, ln = l/100 var c = (1 - Math.abs(2*ln - 1)) * sn var x = c * (1 - Math.abs(((h/60) % 2) - 1)) var m = ln - c/2 var p = h < 60 ? [c,x,0] : h < 120 ? [x,c,0] : h < 180 ? [0,c,x] : h < 240 ? [0,x,c] : h < 300 ? [x,0,c] : [c,0,x] return { r: round((p[0]+m)*255), g: round((p[1]+m)*255), b: round((p[2]+m)*255), a: a } } var hex2 = function (n) { return clamp(round(n), 0, 255).toString(16).padStart(2, '0') } var formatHex = function (q) { var base = '#' + hex2(q.r) + hex2(q.g) + hex2(q.b) return q.a < 1 ? base + hex2(q.a * 255) : base } function parseHex(v) { var m = /^#?([0-9a-f]{3,8})$/i.exec(String(v).trim()) if (!m) return null var h = m[1] if (h.length === 3 || h.length === 4) h = h.split('').map(function (c) { return c + c }).join('') if (h.length !== 6 && h.length !== 8) return null return { r: parseInt(h.slice(0,2),16), g: parseInt(h.slice(2,4),16), b: parseInt(h.slice(4,6),16), a: h.length === 8 ? parseInt(h.slice(6,8),16) / 255 : 1 } } return function init(rootEl) { var el = function (role) { return rootEl.querySelector('[data-role="' + role + '"]') } // HSVA is the canonical state, as in the original — dragging through a // greyscale value doesn't lose the hue you were on. var hsva = { h: 214, s: 76, v: 99, a: 1 } var channelMode = 'rgb' var editing = null var sb = el('sb'), hue = el('hue'), alpha = el('alpha') function announce() { var rgba = hsvaToRgba(hsva) var solid = formatHex({ r: rgba.r, g: rgba.g, b: rgba.b, a: 1 }) rootEl.dispatchEvent(new CustomEvent('mk-color', { detail: { hex: solid, rgb: [rgba.r, rgba.g, rgba.b], a: rgba.a, // The value as CSS would write it — solid hex at full opacity, an // rgba() below it. Consumers that only understand solids keep // reading .hex; the record grammar has held rgba() seeds since // volt's border, so .css is not new vocabulary. css: rgba.a < 1 ? 'rgba(' + rgba.r + ', ' + rgba.g + ', ' + rgba.b + ', ' + (Math.round(rgba.a * 100) / 100) + ')' : solid, }, })) } function paint(quiet) { var rgba = hsvaToRgba(hsva) var solid = formatHex({ r: rgba.r, g: rgba.g, b: rgba.b, a: 1 }) var hueColor = formatHex(hsvaToRgba({ h: hsva.h, s: 100, v: 100, a: 1 })) sb.style.background = 'linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, ' + hueColor + ')' el('sbThumb').style.left = hsva.s + '%' el('sbThumb').style.top = (100 - hsva.v) + '%' el('sbThumb').style.background = solid el('hueThumb').style.left = (hsva.h / 360 * 100) + '%' if (alpha) { el('alphaThumb').style.left = (hsva.a * 100) + '%' el('alphaFill').style.background = 'linear-gradient(to right, transparent, ' + solid + ')' } if (editing !== 'hex') el('fHex').value = formatHex(rgba).toUpperCase() var ch = channelMode === 'rgb' ? [rgba.r, rgba.g, rgba.b] : (function () { var q = rgbaToHsl(rgba); return [q.h, q.s, q.l] })() for (var i = 0; i < 3; i++) if (editing !== 'c' + i) el('c' + i).value = String(ch[i]) if (alpha && editing !== 'a') el('fA').value = String(round(hsva.a * 100)) var labels = channelMode === 'rgb' ? ['R','G','B'] : ['H','S','L'] labels.forEach(function (t, i) { el('l' + i).textContent = t }) sb.setAttribute('aria-valuetext', 'saturation ' + hsva.s + ', brightness ' + hsva.v) hue.setAttribute('aria-valuenow', hsva.h) if (alpha) alpha.setAttribute('aria-valuenow', round(hsva.a * 100)) if (!quiet) announce() } function fromRgba(next, keepHue) { var h = rgbaToHsva(next) if (keepHue && h.s === 0) h.h = hsva.h hsva = h } function bindDrag(node, onMove) { if (!node) return node.addEventListener('pointerdown', function (e) { e.preventDefault() node.focus() var rect = node.getBoundingClientRect() var report = function (ev) { onMove(clamp((ev.clientX - rect.left) / rect.width, 0, 1), clamp((ev.clientY - rect.top) / rect.height, 0, 1)) paint() } report(e) var move = function (ev) { report(ev) } var up = function () { window.removeEventListener('pointermove', move) window.removeEventListener('pointerup', up) } window.addEventListener('pointermove', move) window.addEventListener('pointerup', up) }) } bindDrag(sb, function (fx, fy) { hsva.s = round(fx * 100); hsva.v = round((1 - fy) * 100) }) bindDrag(hue, function (fx) { hsva.h = round(fx * 360) }) bindDrag(alpha, function (fx) { hsva.a = round(fx * 100) / 100 }) sb.addEventListener('keydown', function (e) { var dx = 0, dy = 0 if (e.key === 'ArrowRight') dx = 1; else if (e.key === 'ArrowLeft') dx = -1 else if (e.key === 'ArrowUp') dy = 1; else if (e.key === 'ArrowDown') dy = -1 else return e.preventDefault() hsva.s = clamp(hsva.s + dx * 2, 0, 100) hsva.v = clamp(hsva.v + dy * 2, 0, 100) paint() }) var track = function (node, get, set, step, max) { if (!node) return node.addEventListener('keydown', function (e) { var d = e.key === 'ArrowRight' || e.key === 'ArrowUp' ? 1 : e.key === 'ArrowLeft' || e.key === 'ArrowDown' ? -1 : 0 if (!d) return e.preventDefault() set(clamp(get() + d * step, 0, max)) paint() }) } track(hue, function () { return hsva.h }, function (v) { hsva.h = v }, 4, 360) track(alpha, function () { return round(hsva.a * 100) }, function (v) { hsva.a = v / 100 }, 2, 100) var field = function (node, key, apply) { if (!node) return node.addEventListener('focus', function () { editing = key }) node.addEventListener('blur', function () { editing = null; paint(true) }) node.addEventListener('input', function () { apply(node.value); paint() }) } field(el('fHex'), 'hex', function (v) { var c = parseHex(v) if (c) fromRgba(c, true) }) var chField = function (i) { field(el('c' + i), 'c' + i, function (v) { var n = parseFloat(v) if (!Number.isFinite(n)) return var rgba = hsvaToRgba(hsva) if (channelMode === 'rgb') { var next = { r: rgba.r, g: rgba.g, b: rgba.b, a: hsva.a } next[['r','g','b'][i]] = clamp(round(n), 0, 255) fromRgba(next, true) } else { var q = rgbaToHsl(rgba) var vals = [q.h, q.s, q.l] vals[i] = clamp(round(n), 0, i === 0 ? 360 : 100) fromRgba(hslToRgba(vals[0], vals[1], vals[2], hsva.a), true) } }) } for (var i = 0; i < 3; i++) chField(i) field(el('fA'), 'a', function (v) { var n = parseFloat(v) if (Number.isFinite(n)) hsva.a = clamp(n, 0, 100) / 100 }) el('mCh').addEventListener('click', function () { channelMode = channelMode === 'rgb' ? 'hsl' : 'rgb' paint(true) }) el('mHex').addEventListener('click', function () { el('fHex').select() }) if (typeof window.EyeDropper === 'function') { var btn = el('eyedrop') btn.classList.remove('is-hidden') btn.addEventListener('click', function () { new window.EyeDropper().open().then(function (res) { var c = parseHex(res.sRGBHex) if (c) { c.a = hsva.a; fromRgba(c, true); paint() } }).catch(function () {}) }) } // Programmatic set: quiet, so feeding the picker a colour does not echo it // straight back as a change event. rootEl.mkSetColor = function (hex) { var c = parseHex(hex) if (c) { fromRgba(c, true); paint(true) } } var initial = parseHex(rootEl.getAttribute('data-initial')) if (initial) fromRgba(initial, true) paint(true) } })() mkColorPicker(document.currentScript.previousElementSibling) </script> alpha: true — the checkerboard track and the A column appear, and the hex field speaks 8 digits when opacity drops below 100.
<div class="mk-color-picker" role="group" aria-label="Color picker" data-color-picker data-initial="#2f6feb">
<div class="mk-color-sb" data-role="sb" tabindex="0" role="slider" aria-label="Saturation and brightness">
<span class="mk-color-sb-thumb" data-role="sbThumb"></span>
</div>
<div class="mk-color-sliders">
<button type="button" class="mk-color-eyedrop is-hidden" data-role="eyedrop" title="Pick a color from the screen" aria-label="Eyedropper">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="m2 22 1-1h3l9-9"></path>
<path d="M3 21v-3l9-9"></path>
<path d="m15 6 3.4-3.4a2.1 2.1 0 1 1 3 3L18 9l.4.4a2.1 2.1 0 1 1-3 3l-3.8-3.8a2.1 2.1 0 1 1 3-3l.4.4Z"></path>
</svg>
</button>
<div class="mk-color-slider-stack">
<div class="mk-color-slider" data-role="hue" tabindex="0" role="slider" aria-label="Hue" aria-valuemin="0" aria-valuemax="360" style="background:linear-gradient(to right,#f00,#ff0 17%,#0f0 33%,#0ff 50%,#00f 67%,#f0f 83%,#f00)">
<span class="mk-color-slider-thumb" data-role="hueThumb"></span>
</div>
<div class="mk-color-slider mk-color-alpha" data-role="alpha" tabindex="0" role="slider" aria-label="Opacity" aria-valuemin="0" aria-valuemax="100">
<span class="mk-color-alpha-fill" data-role="alphaFill"></span>
<span class="mk-color-slider-thumb" data-role="alphaThumb"></span>
</div>
</div>
</div>
<div class="mk-color-inputs">
<div class="mk-color-field is-wide">
<input class="mk-color-field-input" data-role="fHex" aria-label="Hex" spellcheck="false">
</div>
<div class="mk-color-channels">
<div class="mk-color-field">
<input class="mk-color-field-input" data-role="c0" aria-label="Channel 1" inputmode="numeric">
</div>
<div class="mk-color-field">
<input class="mk-color-field-input" data-role="c1" aria-label="Channel 2" inputmode="numeric">
</div>
<div class="mk-color-field">
<input class="mk-color-field-input" data-role="c2" aria-label="Channel 3" inputmode="numeric">
</div>
</div>
<div class="mk-color-field">
<input class="mk-color-field-input" data-role="fA" aria-label="Opacity percent" inputmode="numeric">
</div>
</div>
<div class="mk-color-modes">
<div class="mk-color-field is-wide">
<button type="button" class="mk-color-mode is-hex is-on" data-role="mHex" aria-pressed="true" title="Write this colour as hex">HEX</button>
</div>
<div class="mk-color-channels">
<button type="button" class="mk-color-mode" data-role="mCh" aria-label="Toggle RGB and HSL" title="Toggle RGB and HSL">
<span data-role="l0">R</span>
<span data-role="l1">G</span>
<span data-role="l2">B</span>
</button>
</div>
<div class="mk-color-field">
<span class="mk-color-mode-static">A</span>
</div>
</div>
</div>
<script>
// One definition, many instances: the initializer is installed once and every // emitted picker calls it on its own root. Scoped queries, no ids — two // pickers on a page cannot cross wires. if (!window.mkColorPicker) window.mkColorPicker = (function () { 'use strict' var clamp = function (n, lo, hi) { return Math.max(lo, Math.min(hi, n)) } var round = Math.round function hsvaToRgba(k) { var sn = k.s / 100, vn = k.v / 100 var c = vn * sn, x = c * (1 - Math.abs(((k.h / 60) % 2) - 1)), m = vn - c var p = k.h
< 60 ? [c,x,0] : k.h < 120 ? [x,c,0] : k.h < 180 ? [0,c,x] : k.h < 240 ? [0,x,c] : k.h < 300 ? [x,0,c] : [c,0,x] return { r: round((p[0]+m)*255), g: round((p[1]+m)*255), b: round((p[2]+m)*255), a: k.a } } function rgbaToHsva(q) { var rn = q.r/255, gn = q.g/255, bn = q.b/255 var max = Math.max(rn,gn,bn), min = Math.min(rn,gn,bn), d = max - min var h = 0 if (d) { if (max === rn) h = ((gn - bn) / d) % 6 else if (max === gn) h = (bn - rn) / d + 2 else h = (rn - gn) / d + 4 h = (h * 60 + 360) % 360 } return { h: round(h), s: round(max === 0 ? 0 : (d / max) * 100), v: round(max * 100), a: q.a } } function rgbaToHsl(q) { var rn = q.r/255, gn = q.g/255, bn = q.b/255 var max = Math.max(rn,gn,bn), min = Math.min(rn,gn,bn), d = max - min var l = (max + min) / 2, h = 0, s = 0 if (d) { s = d / (1 - Math.abs(2*l - 1)) if (max === rn) h = ((gn - bn) / d) % 6 else if (max === gn) h = (bn - rn) / d + 2 else h = (rn - gn) / d + 4 h = (h * 60 + 360) % 360 } return { h: round(h), s: round(s*100), l: round(l*100) } } function hslToRgba(h, s, l, a) { var sn = s/100, ln = l/100 var c = (1 - Math.abs(2*ln - 1)) * sn var x = c * (1 - Math.abs(((h/60) % 2) - 1)) var m = ln - c/2 var p = h < 60 ? [c,x,0] : h < 120 ? [x,c,0] : h < 180 ? [0,c,x] : h < 240 ? [0,x,c] : h < 300 ? [x,0,c] : [c,0,x] return { r: round((p[0]+m)*255), g: round((p[1]+m)*255), b: round((p[2]+m)*255), a: a } } var hex2 = function (n) { return clamp(round(n), 0, 255).toString(16).padStart(2, '0') } var formatHex = function (q) { var base = '#' + hex2(q.r) + hex2(q.g) + hex2(q.b) return q.a < 1 ? base + hex2(q.a * 255) : base } function parseHex(v) { var m = /^#?([0-9a-f]{3,8})$/i.exec(String(v).trim()) if (!m) return null var h = m[1] if (h.length === 3 || h.length === 4) h = h.split('').map(function (c) { return c + c }).join('') if (h.length !== 6 && h.length !== 8) return null return { r: parseInt(h.slice(0,2),16), g: parseInt(h.slice(2,4),16), b: parseInt(h.slice(4,6),16), a: h.length === 8 ? parseInt(h.slice(6,8),16) / 255 : 1 } } return function init(rootEl) { var el = function (role) { return rootEl.querySelector('[data-role="' + role + '"]') } // HSVA is the canonical state, as in the original — dragging through a // greyscale value doesn't lose the hue you were on. var hsva = { h: 214, s: 76, v: 99, a: 1 } var channelMode = 'rgb' var editing = null var sb = el('sb'), hue = el('hue'), alpha = el('alpha') function announce() { var rgba = hsvaToRgba(hsva) var solid = formatHex({ r: rgba.r, g: rgba.g, b: rgba.b, a: 1 }) rootEl.dispatchEvent(new CustomEvent('mk-color', { detail: { hex: solid, rgb: [rgba.r, rgba.g, rgba.b], a: rgba.a, // The value as CSS would write it — solid hex at full opacity, an // rgba() below it. Consumers that only understand solids keep // reading .hex; the record grammar has held rgba() seeds since // volt's border, so .css is not new vocabulary. css: rgba.a < 1 ? 'rgba(' + rgba.r + ', ' + rgba.g + ', ' + rgba.b + ', ' + (Math.round(rgba.a * 100) / 100) + ')' : solid, }, })) } function paint(quiet) { var rgba = hsvaToRgba(hsva) var solid = formatHex({ r: rgba.r, g: rgba.g, b: rgba.b, a: 1 }) var hueColor = formatHex(hsvaToRgba({ h: hsva.h, s: 100, v: 100, a: 1 })) sb.style.background = 'linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, ' + hueColor + ')' el('sbThumb').style.left = hsva.s + '%' el('sbThumb').style.top = (100 - hsva.v) + '%' el('sbThumb').style.background = solid el('hueThumb').style.left = (hsva.h / 360 * 100) + '%' if (alpha) { el('alphaThumb').style.left = (hsva.a * 100) + '%' el('alphaFill').style.background = 'linear-gradient(to right, transparent, ' + solid + ')' } if (editing !== 'hex') el('fHex').value = formatHex(rgba).toUpperCase() var ch = channelMode === 'rgb' ? [rgba.r, rgba.g, rgba.b] : (function () { var q = rgbaToHsl(rgba); return [q.h, q.s, q.l] })() for (var i = 0; i < 3; i++) if (editing !== 'c' + i) el('c' + i).value = String(ch[i]) if (alpha && editing !== 'a') el('fA').value = String(round(hsva.a * 100)) var labels = channelMode === 'rgb' ? ['R','G','B'] : ['H','S','L'] labels.forEach(function (t, i) { el('l' + i).textContent = t }) sb.setAttribute('aria-valuetext', 'saturation ' + hsva.s + ', brightness ' + hsva.v) hue.setAttribute('aria-valuenow', hsva.h) if (alpha) alpha.setAttribute('aria-valuenow', round(hsva.a * 100)) if (!quiet) announce() } function fromRgba(next, keepHue) { var h = rgbaToHsva(next) if (keepHue && h.s === 0) h.h = hsva.h hsva = h } function bindDrag(node, onMove) { if (!node) return node.addEventListener('pointerdown', function (e) { e.preventDefault() node.focus() var rect = node.getBoundingClientRect() var report = function (ev) { onMove(clamp((ev.clientX - rect.left) / rect.width, 0, 1), clamp((ev.clientY - rect.top) / rect.height, 0, 1)) paint() } report(e) var move = function (ev) { report(ev) } var up = function () { window.removeEventListener('pointermove', move) window.removeEventListener('pointerup', up) } window.addEventListener('pointermove', move) window.addEventListener('pointerup', up) }) } bindDrag(sb, function (fx, fy) { hsva.s = round(fx * 100); hsva.v = round((1 - fy) * 100) }) bindDrag(hue, function (fx) { hsva.h = round(fx * 360) }) bindDrag(alpha, function (fx) { hsva.a = round(fx * 100) / 100 }) sb.addEventListener('keydown', function (e) { var dx = 0, dy = 0 if (e.key === 'ArrowRight') dx = 1; else if (e.key === 'ArrowLeft') dx = -1 else if (e.key === 'ArrowUp') dy = 1; else if (e.key === 'ArrowDown') dy = -1 else return e.preventDefault() hsva.s = clamp(hsva.s + dx * 2, 0, 100) hsva.v = clamp(hsva.v + dy * 2, 0, 100) paint() }) var track = function (node, get, set, step, max) { if (!node) return node.addEventListener('keydown', function (e) { var d = e.key === 'ArrowRight' || e.key === 'ArrowUp' ? 1 : e.key === 'ArrowLeft' || e.key === 'ArrowDown' ? -1 : 0 if (!d) return e.preventDefault() set(clamp(get() + d * step, 0, max)) paint() }) } track(hue, function () { return hsva.h }, function (v) { hsva.h = v }, 4, 360) track(alpha, function () { return round(hsva.a * 100) }, function (v) { hsva.a = v / 100 }, 2, 100) var field = function (node, key, apply) { if (!node) return node.addEventListener('focus', function () { editing = key }) node.addEventListener('blur', function () { editing = null; paint(true) }) node.addEventListener('input', function () { apply(node.value); paint() }) } field(el('fHex'), 'hex', function (v) { var c = parseHex(v) if (c) fromRgba(c, true) }) var chField = function (i) { field(el('c' + i), 'c' + i, function (v) { var n = parseFloat(v) if (!Number.isFinite(n)) return var rgba = hsvaToRgba(hsva) if (channelMode === 'rgb') { var next = { r: rgba.r, g: rgba.g, b: rgba.b, a: hsva.a } next[['r','g','b'][i]] = clamp(round(n), 0, 255) fromRgba(next, true) } else { var q = rgbaToHsl(rgba) var vals = [q.h, q.s, q.l] vals[i] = clamp(round(n), 0, i === 0 ? 360 : 100) fromRgba(hslToRgba(vals[0], vals[1], vals[2], hsva.a), true) } }) } for (var i = 0; i < 3; i++) chField(i) field(el('fA'), 'a', function (v) { var n = parseFloat(v) if (Number.isFinite(n)) hsva.a = clamp(n, 0, 100) / 100 }) el('mCh').addEventListener('click', function () { channelMode = channelMode === 'rgb' ? 'hsl' : 'rgb' paint(true) }) el('mHex').addEventListener('click', function () { el('fHex').select() }) if (typeof window.EyeDropper === 'function') { var btn = el('eyedrop') btn.classList.remove('is-hidden') btn.addEventListener('click', function () { new window.EyeDropper().open().then(function (res) { var c = parseHex(res.sRGBHex) if (c) { c.a = hsva.a; fromRgba(c, true); paint() } }).catch(function () {}) }) } // Programmatic set: quiet, so feeding the picker a colour does not echo it // straight back as a change event. rootEl.mkSetColor = function (hex) { var c = parseHex(hex) if (c) { fromRgba(c, true); paint(true) } } var initial = parseHex(rootEl.getAttribute('data-initial')) if (initial) fromRgba(initial, true) paint(true) } })() mkColorPicker(document.currentScript.previousElementSibling) </script> alpha: true | Field | Type | Notes |
|---|---|---|
value |
text | A hex colour the picker opens on. |
alpha |
boolean | Off by default: a palette seed is a solid, and 8-digit hex is a value the record grammar does not expect. On, the alpha track and field appear. |
footer |
text | Raw HTML rendered inside the picker's card, below the fields — a page-author slot for whatever closes the loop around the tool (a Done button, an apply row). The picker itself stays ignorant of it. |