Skip to content

Slider Color Picker

Use three sliders to edit RGB channels and use-diff to animate the value.

Code

The following example assumes that you have imported the component and set up the theme.

Color Picker
<div class="slider-wrapper">
<!-- Color preview -->
6 collapsed lines
<div class="preview-container">
<div class="color-preview"></div>
<strong>
<sv-text-flow class="color-value" use-diff></sv-text-flow>
</strong>
</div>
<!-- Red slider -->
6 collapsed lines
<div class="slider-container">
<label for="red-slider">Red</label>
<sv-slider id="red-slider" class="color-slider" max="255" value="100">
<div class="custom-thumb" aria-hidden="true" slot="thumb-1"></div>
</sv-slider>
</div>
<!-- Green slider -->
6 collapsed lines
<div class="slider-container">
<label for="green-slider">Green</label>
<sv-slider id="green-slider" class="color-slider green" max="255">
<div class="custom-thumb" aria-hidden="true" slot="thumb-1"></div>
</sv-slider>
</div>
<!-- Blue slider -->
6 collapsed lines
<div class="slider-container">
<label for="blue-slider">Blue</label>
<sv-slider id="blue-slider" class="color-slider" max="255">
<div class="custom-thumb" aria-hidden="true" slot="thumb-1"></div>
</sv-slider>
</div>
<!-- Color swatches -->
10 collapsed lines
<div class="swatches-container">
<button type="button" style="background: #f00" aria-label="Red" data-color="255 0 0"></button>
<button type="button" style="background: #0f0" aria-label="Green" data-color="0 255 0"></button>
<button type="button" style="background: #00f" aria-label="Blue" data-color="0 0 255"></button>
<button type="button" style="background: #ff0" aria-label="Yellow" data-color="255 255 0"></button>
<button type="button" style="background: #f0f" aria-label="Magenta" data-color="255 0 255"></button>
<button type="button" style="background: #0ff" aria-label="Cyan" data-color="0 255 255"></button>
<button type="button" style="background: #000" aria-label="Black" data-color="0 0 0"></button>
<button type="button" style="background: #fff" aria-label="White" data-color="255 255 255"></button>
</div>
</div>
<script>
110 collapsed lines
/** @type {HTMLDivElement} */
const wrapper = document.querySelector(".slider-wrapper");
/** @type {Slider} */
const redSlider = document.querySelector("#red-slider");
/** @type {Slider} */
const greenSlider = document.querySelector("#green-slider");
/** @type {Slider} */
const blueSlider = document.querySelector("#blue-slider");
/** @type {HTMLSpanElement} */
const previewValue = document.querySelector(".color-value");
/** @type {NodeListOf<HTMLButtonElement>} */
const swatches = document.querySelectorAll(".swatches-container button");
const prefersReducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
CSS.registerProperty({
name: "--red",
syntax: "<number>",
inherits: true,
initialValue: "0",
});
CSS.registerProperty({
name: "--green",
syntax: "<number>",
inherits: true,
initialValue: "0",
});
CSS.registerProperty({
name: "--blue",
syntax: "<number>",
inherits: true,
initialValue: "0",
});
const color = {
red: getChannelValue(redSlider),
green: getChannelValue(greenSlider),
blue: getChannelValue(blueSlider),
};
/** @param {MouseEvent} event */
function swatchClickHandler(event) {
/** @type {HTMLButtonElement} */
const button = event.currentTarget;
// Swatch new RGB values
const [nR, nG, nB] = button.dataset.color.split(" ").map(Number);
const duration = prefersReducedMotion.matches ? 0 : 300;
const easing = "cubic-bezier(0.25, 1, 0.5, 1)";
// Old RGB values
const [oR, oG, oB] = [redSlider, greenSlider, blueSlider].map(getChannelValue);
// Animate gradients
if (duration > 0) {
wrapper.animate(
[
{ "--red": oR, "--green": oG, "--blue": oB },
{ "--red": nR, "--green": nG, "--blue": nB },
],
{ duration, easing: "linear" }
);
}
// Set the new value with animation
const animate = duration > 0;
redSlider.setValue({ value: nR, animate, duration, easing });
greenSlider.setValue({ value: nG, animate, duration, easing });
blueSlider.setValue({ value: nB, animate, duration, easing });
onColorChange();
}
function onColorChange() {
color.red = getChannelValue(redSlider);
color.green = getChannelValue(greenSlider);
color.blue = getChannelValue(blueSlider);
wrapper.style.setProperty("--red", String(color.red));
wrapper.style.setProperty("--green", String(color.green));
wrapper.style.setProperty("--blue", String(color.blue));
const previewText = `RGB(${color.red}, ${color.green}, ${color.blue})`;
previewValue.textContent = previewText;
}
/** @param {Slider} slider */
function getChannelValue(slider) {
const value = slider.value;
if (Array.isArray(value)) return value[0] ?? slider.min;
return value;
}
onColorChange();
redSlider.addEventListener("valuechange", onColorChange);
greenSlider.addEventListener("valuechange", onColorChange);
blueSlider.addEventListener("valuechange", onColorChange);
for (const swatch of swatches) {
swatch.addEventListener("click", swatchClickHandler);
}
</script>
<style>
.slider-wrapper {
--red: 0;
--green: 0;
--blue: 0;
4 collapsed lines
display: flex;
flex-direction: column;
gap: 2em;
padding: 1em;
}
.preview-container {
2 collapsed lines
display: flex;
gap: 1em;
}
.color-preview {
5 collapsed lines
inline-size: 4em;
block-size: 2em;
background-color: rgb(var(--red) var(--green) var(--blue));
border: 1px solid #fff;
border-radius: var(--sv-bdr-rad-md);
}
.slider-container {
3 collapsed lines
display: flex;
flex-direction: column;
gap: 0.5em;
}
.color-slider {
--sv-thickness: 1.8em;
--sv-thumb-size: 2.2em;
--sv-bdr-rad-md: 2em;
--sv-bdr-sz-sm: 2px;
--sv-tooltip-offset: 2em;
--sv-ease: cubic-bezier(0.25, 1, 0.5, 1);
&::part(slider) {
box-shadow: 0 0 7px #00000060;
}
&::part(track) {
border-color: #fff;
}
&::part(fill) {
background-color: transparent;
}
&::part(tooltip) {
background-color: transparent;
border: none;
}
&::part(tooltip-arrow) {
display: none;
}
.custom-thumb {
29 collapsed lines
position: relative;
inline-size: var(--sv-thumb-size);
block-size: var(--sv-thumb-size);
background-color: #fff;
border-radius: 50%;
box-shadow: 0 0 7px 0 #00000080;
transition-timing-function: var(--sv-ease);
transition-duration: var(--sv-dur);
transition-property: inline-size, block-size;
&::after {
position: absolute;
inset-block-start: 50%;
inset-inline-start: 50%;
z-index: -1;
inline-size: var(--sv-thumb-size);
block-size: var(--sv-thumb-size);
margin: auto;
content: "";
background-color: rgb(255 255 255 / 40%);
border-radius: 50%;
box-shadow: 0 0 7px 0 #00000080;
backdrop-filter: blur(2px);
translate: -50% -50%;
}
@media (prefers-reduced-motion: reduce) {
transition-duration: 0s;
}
}
&:state(--active) {
cursor: grabbing;
.custom-thumb {
inline-size: 4px;
block-size: 4px;
}
}
}
#red-slider::part(track) {
background: linear-gradient(90deg, rgb(0 var(--green) var(--blue)), rgb(255 var(--green) var(--blue)));
}
#green-slider::part(track) {
background: linear-gradient(90deg, rgb(var(--red) 0 var(--blue)), rgb(var(--red) 255 var(--blue)));
}
#blue-slider::part(track) {
background: linear-gradient(90deg, rgb(var(--red) var(--green) 0), rgb(var(--red) var(--green) 255));
}
.swatches-container {
13 collapsed lines
display: flex;
flex-wrap: wrap;
gap: 0.5em;
justify-content: center;
/* Swatch */
button {
inline-size: 2em;
block-size: 2em;
cursor: pointer;
border: 1px solid var(--sl-color-text);
border-radius: var(--sv-bdr-rad-md);
}
}
</style>