Skip to content

Instantly share code, notes, and snippets.

@AkinAguda
Last active February 14, 2024 12:02
Show Gist options
  • Save AkinAguda/3cf82864dcd3c7fd7748ba273776e069 to your computer and use it in GitHub Desktop.
Save AkinAguda/3cf82864dcd3c7fd7748ba273776e069 to your computer and use it in GitHub Desktop.
A gradient color slider in the context of Next.js + Typescript + tailwind
/* Doing this here because it is cumbersome to do in tailwind classes*/
.slider::-moz-range-thumb {
background-color: currentColor;
height: 20px;
width: 20px;
border-radius: 100%;
border: 2px solid white;
cursor: pointer;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
background-color: currentColor;
height: 20px;
width: 20px;
border-radius: 100%;
border: 2px solid white;
cursor: pointer;
}
"use client";
import { memo, useState } from "react";
import classes from "./colorSlider.module.css";
import {
GRADIENT,
MAX_RANGE,
RGB,
determineColorFromValue,
determineValueFromColor,
} from "./utils";
type props = {
/** This is called whenever the color is changed. The color in hexadecimal.
* is passed in as the argument.
*/
onColorChange?: (color: RGB) => void;
/**
* An initial color can be passed into this component
*/
color?: RGB;
};
const getInitialValue = (initialColor: RGB | undefined) => {
if (initialColor) {
const colorValueInRange = determineValueFromColor(initialColor);
if (colorValueInRange) {
return colorValueInRange;
}
}
return 0;
};
const ColorSlider: React.FC<props> = memo(({ onColorChange, color }) => {
const [value, setValue] = useState(getInitialValue(color));
return (
<div
className="relative flex h-2 w-full items-center rounded-2xl"
style={{
backgroundImage: `linear-gradient(to right, ${GRADIENT.map((color) => `rgb(${color.join()})`).join(", ")})`,
}}
>
{(() => {
return (
<input
value={value}
min={0}
max={MAX_RANGE}
step={1}
type="range"
style={{ color: `rgb(${determineColorFromValue(value).join()})` }}
className={`absolute left-0 top-0 h-full w-full appearance-none bg-transparent text-transparent ${classes.slider}`}
onInput={(event) => {
if (event.currentTarget.value && onColorChange) {
const value = Number(event.currentTarget.value);
onColorChange(determineColorFromValue(value));
setValue(value);
}
}}
/>
);
})()}
</div>
);
});
ColorSlider.displayName = "ColorSlider";
export default ColorSlider;
export type RGB = [number, number, number];
// Function to linearly interpolate between 2 values given a third value between 0 -> 1 as it's anchor
export const lerp = (v0: number, v1: number, t: number) =>
(1 - t) * v0 + t * v1;
/**
* Inverse Linar Interpolation, get the fraction between `a` and `b` on which `v` resides.
*/
export const inLerp = (a: number, b: number, v: number) => (v - a) / (b - a);
export const MAX_RANGE = 500;
export const GRADIENT: RGB[] = [
[234, 51, 35],
[241, 159, 57],
[138, 252, 77],
[117, 251, 205],
[13, 41, 245],
[216, 46, 240],
[234, 51, 59],
];
export const determineColorFromValue = (value: number): RGB => {
const valueInZeroToOne = value / MAX_RANGE;
const valueInGradientArrayIndex = lerp(
0,
GRADIENT.length - 1,
valueInZeroToOne,
);
const colorLowerBoundaryIndex = Math.floor(valueInGradientArrayIndex);
const fractionalPart = valueInGradientArrayIndex - colorLowerBoundaryIndex;
const lowerBoundaryColor = GRADIENT[colorLowerBoundaryIndex];
const upperBoundaryColor =
GRADIENT[Math.min(colorLowerBoundaryIndex + 1, GRADIENT.length - 1)];
const lerpedCoulour = lowerBoundaryColor.map((value, index) =>
lerp(value, upperBoundaryColor[index], fractionalPart),
);
return lerpedCoulour as RGB;
};
export const isColorWithinRange = (color1: RGB, color2: RGB, color3: RGB) => {
let colorIsWithinRange = true;
for (let i = 0; i < color1.length; i++) {
const color1Component = color1[i];
const color2Component = color2[i];
const color3Component = color3[i];
const max = Math.max(color2Component, color1Component);
const min = Math.min(color2Component, color1Component);
if (!(color3Component <= max && color3Component >= min)) {
colorIsWithinRange = false;
}
}
return colorIsWithinRange;
};
export const determineValueFromColor = (color: RGB): number | undefined => {
let indexWhereColorCanBeFound = null;
for (let i = 0; i < GRADIENT.length - 1; i++) {
const color1 = GRADIENT[i];
const color2 = GRADIENT[i + 1];
const isWithinRange = isColorWithinRange(color1, color2, color);
if (isWithinRange) {
indexWhereColorCanBeFound = i;
break;
}
}
if (indexWhereColorCanBeFound !== null) {
const color1 = GRADIENT[indexWhereColorCanBeFound];
const color2 = GRADIENT[indexWhereColorCanBeFound + 1];
const lerpBetween = inLerp(color1[0], color2[0], color[0]);
const eachRangeUnit = MAX_RANGE / (GRADIENT.length - 1);
const value =
eachRangeUnit * indexWhereColorCanBeFound + lerpBetween * eachRangeUnit;
return value;
} else {
return undefined;
}
};
@AkinAguda
Copy link
Author

It renders this:

Screenshot 2024-02-14 at 13 00 30

When used like this:

"use client";

import ColorSlider from "@/components/ColorSlider";
import { RGB } from "@/components/ColorSlider/utils";
import { useState } from "react";

const ColorPicker: React.FC = () => {
  const [color, setColor] = useState([62, 140, 226] as RGB);

  return (
    <div className="flex grow items-center rounded-2.5xl bg-slate-100 p-4">
      <ColorSlider onColorChange={setColor} color={color} />
    </div>
  );
};

export default ColorPicker;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment