Last active
February 14, 2024 12:02
-
-
Save AkinAguda/3cf82864dcd3c7fd7748ba273776e069 to your computer and use it in GitHub Desktop.
A gradient color slider in the context of Next.js + Typescript + tailwind
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
It renders this:
When used like this: