Build a color picker
Written byPhuoc Nguyen
Created
09 Nov, 2023
Tags
HTML color input, React color picker
A color picker is a useful tool that helps users choose and adjust colors in a graphical user interface (GUI). For web developers, color pickers are crucial because they enable them to select colors for various elements of their website, including text, backgrounds, and borders. By adjusting hue, saturation, and brightness values or entering hexadecimal codes, developers can quickly find the perfect shade of a color. Using a color picker also ensures consistency in the website's design by allowing developers to save custom colors for later use.
Color pickers are versatile tools that can be used in many different scenarios. For example, in a rich text editor, users can format text with different styles and add multimedia content. With a color picker integrated into the editor's toolbar, users can select custom colors for text and background.
Web developers can also use color pickers to create custom interface elements such as buttons, icons, and borders that match their brand or design language.
In real-life scenarios, color pickers are handy tools for web designers to customize a website's theme. For instance, a designer may want to match the website's color scheme with their client's brand colors. By using a color picker, designers can easily select custom colors for text, background, borders, buttons, and other interface elements.
In summary, a color picker is a must-have tool for any web developer who wants to create visually appealing and consistent designs. In this post, we'll explore how to create a color picker with React.
#Introducing the HTML color input
Did you know that modern browsers come with a built-in color picker? It's true! You can easily access it by setting the
`type`
attribute to `color`
.html
<input type="color" />
This input type lets users select a color from a palette by clicking on it or adjusting its hue, saturation, and brightness values. Want to see how it works? Give it a try by clicking on the input in the demo below.
Adding a color picker to your HTML file is a breeze. Just insert the element and voila! The user can interact with it, and the selected color is updated in real-time. This makes it easy for developers to capture the selected color and use it in their application.
The built-in HTML color input has some advantages. It doesn't require any extra libraries or dependencies, and it's built into the browser. So you don't have to worry about compatibility issues or extra code bloat.
However, there are some limitations to the built-in color picker. It's not very customizable, and the color palette and layout are fixed. You can't change the number of colors displayed or adjust the layout to match your application's design. Plus, it doesn't work well with touch devices, which can lead to a poor user experience.
Finally, if you need to create custom gradients or patterns, the built-in color picker may not cut it. You'll need a more advanced tool that provides greater control over each element's color and opacity.
To address those problems, we decided to make our own color picker tool.
#Creating a draggable component
Imagine having a color picker that lets users change the saturation, hue, and brightness of a color. Users can adjust the corresponding value by dragging a cursor in a given area. For instance, they can change the brightness between 0 and 1.
In this post, we'll use the same approach we outlined in the previous post to constrain a draggable element within its container. However, instead of using a reusable hook as we did earlier, we'll turn it into a reusable component.
The
`Draggable`
component in the code snippet below is a handy React component that lets you make any element draggable. It does this by adding event listeners to the element's `mousedown`
and `touchstart`
events, which allows users to drag the element around the screen with ease.tsx
const Draggable = ({ children }) => {
const [node, setNode] = React.useState<HTMLElement>();
const [{ dx, dy }, setOffset] = React.useState({
dx: 0,
dy: 0,
});
const ref = React.useCallback((nodeEle) => {
setNode(nodeEle);
}, []);
React.useEffect(() => {
node.addEventListener("mousedown", handleMouseDown);
node.addEventListener("touchstart", handleTouchStart);
return () => {
node.removeEventListener("mousedown", handleMouseDown);
node.removeEventListener("touchstart", handleTouchStart);
};
}, [node, dx, dy]);
return children({ ref, dx, dy });
};
If you're not familiar with the pattern used in the`Draggable`
component, I recommend visiting this post for details.
When compared to the custom hook we created in the previous post, there are some differences in the
`Draggable`
component. First, it doesn't return a ref. Second, it doesn't update the element position directly.Instead, it passes three props to its children:
`ref`
, `dx`
, and `dy`
. The `ref`
prop attaches the draggable behavior to an HTML element. The `dx`
and `dy`
props represent the x and y offsets of the element relative to its original position. We can update the position of the draggable element effortlessly by passing these values as props while rendering it. This allows us to create interactive user interfaces that respond to user input in real-time.Here's a simple example of using the
`Draggable`
component:tsx
<Draggable>
{
({ ref, dx, dy }) => (
<div className="draggable__container">
<div
className="draggable"
ref={ref}
style={{
transform: `translate(${dx}px, ${dy}px)`,
}}
/>
</div>
)
}
</Draggable>
Previously, we made the entire element draggable within its container. However, for our color picker, we only want the center of the draggable handler element to be affected.
To achieve this, we'll start by making the handler element circular. We can do this by adding a
`border-radius`
property and setting it to 50%. This will give us a perfect circle. We'll also set the `width`
and `height`
properties to ensure that the element is equal in both dimensions.css
.draggable {
border-radius: 50%;
width: 1.5rem;
height: 1.5rem;
}
To position the draggable handler element in our color picker, we use CSS's
`transform`
property. This lets us move the element in 2D space by translating, rotating, or scaling it.For our draggable handler, we want it at the center of a container element. So we set its
`top`
and `left`
properties to negative half of its width and height. This centers the element within its container by moving it up and left by half of its dimensions.Finally, we use the
`translate()`
function to apply a translation transform with zero values for both the x and y axes. This ensures that any changes to the draggable's state only modify its position relative to its initial centered location. As users move the handler, the transform property will be updated dynamically.css
.draggable {
top: -0.75rem;
left: -0.75rem;
transform: translate(0, 0);
}
Give the demo below a try! Simply drag the circle and move it to any side of the container. You'll notice that the center of the circle cannot go outside of the container.
#Setting the initial position for the handler
In the demo above, the draggable handler starts at the top left corner of the container. However, in real-world applications, we often need to set the initial position of the handler. For instance, in a color picker, the handler position can be determined based on the initial color.
To address this, the
`Draggable`
component provides a new property called `onMount`
. This property enables us to set the initial position of the handler. Here's how it works:tsx
const Draggable = ({ onMount }) => {
const ref = React.useCallback((nodeEle) => {
setNode(nodeEle);
if (nodeEle && onMount) {
const parentRect = nodeEle.parentElement.getBoundingClientRect();
onMount({
parentWidth: parentRect.width,
parentHeight: parentRect.height,
setOffset,
});
}
}, []);
};
The
`onMount`
property is a function that gets called when the draggable component is first set up. It gives us some useful information, like the width and height of the container, and a function to update the handler's position.By setting the draggable handler's initial position based on the initial color value, we can make things easier for users. For example, if we start with red as the initial color, we might want to put the draggable handler in the center of our color picker at 100% saturation and 50% brightness.
To accomplish this, we can use an
`onMount`
function in our `Draggable`
component. This function will calculate and set the initial position of the handler element using the `parentWidth`
, `parentHeight`
, and `setOffset`
properties. Once we've figured out the starting position, we can update the handler's state with `setOffset`
, and start dragging it from its new location right away.tsx
const handleOnMount = ({ parentWidth, parentHeight, setOffset }) => {
const initX = (saturation / 100) * parentWidth;
const initY = ((100 - brightness) / 100) * parentHeight;
setOffset({ dx: initX, dy: initY });
};
// Render
<Draggable onMount={handleOnMount}>
...
</Draggable>
Later in the upcoming sections, we'll show you how we utilize the
`onMount`
property so you can see it in action.#Adding a callback for user interactions
Currently, our draggable component updates its internal states,
`dx`
and `dy`
, when users drag the handler around. These states can be used to update the position of the handler, but in real life, we often want more control over user interactions.That's where the
`onMove`
property comes in. This new feature allows us to trigger a callback function whenever the user moves the handler element. The callback function receives an object containing the current x and y offsets of the draggable element relative to its initial position, as well as the width and height of its parent container.ts
const handleMouseMove = (e: React.MouseEvent) => {
const parent = node.parentElement;
const parentRect = parent.getBoundingClientRect();
const parentWidth = parentRect.width;
const parentHeight = parentRect.height;
// Calculate dx and dy ...
onMove({
dx,
dy,
parentWidth,
parentHeight,
});
};
Let's say we have a color picker. We can use offsets to figure out which color the user has selected. But with the
`onMove`
feature, we have even more control over how our users interact with the color picker.For example, we can use this feature to update the selected color based on where the user moves the draggable handler. If the user moves it horizontally, we can update the hue value. If they move it vertically, we can update the saturation/brightness values.
Here's an example implementation:
tsx
const handleOnMove = ({ dx, dy, parentWidth, parentHeight }) => {
// Calculate new saturation and brightness values
const saturation = (dx / parentWidth) * 100;
const brightness = (1 - dy / parentHeight) * 100;
// Update selected color state
setSelectedColor({
...selectedColor,
s: saturation,
v: brightness,
});
};
// Render
<Draggable onMove={handleOnMove}>
...
</Draggable>
By implementing this code, whenever users move the draggable handler within our color picker container, we will update our state with new hue, saturation, and brightness values. This will enable us to display a live preview of the currently selected color to users in real-time!
#Understanding the data model for color representation
Before we dive into building our own color picker, let's first talk about the data model we use to represent a color. We use an object with different properties to store the color values. These properties include:
`r`
,`g`
,`b`
to represent the Red, Green, and Blue values`h`
,`s`
,`v`
to represent the Hue, Saturation, and Value (brightness) values`a`
to represent the Alpha (transparency) value.
To manage the color state of our color picker, we use React's
`useState`
hook. In our example, we start with an initial color object that contains values for red, green, blue, and alpha. We then convert this RGB value to HSV using a utility function called `rgbToHsv`
.To keep things simple, we won't go into the details of how the`rgbToHsv`
function works. However, you can take a look at the`rgbToHsv.ts`
file to see the implementation if you're interested.
The resulting object contains values for hue, saturation, and brightness. We merge these values with the original RGB values to create a new object that represents the complete color state.
ts
const initialColor = {
r: 0,
g: 0,
b: 0,
a: 1,
};
const hsv = rgbToHsv(initialColor);
const [color, setColor] = React.useState({
...initialColor,
...hsv,
});
Now that we have the
`color`
state available in our color picker component, we can use it to display the currently selected color and update it as users interact with the component.It's important to keep in mind that color values in JavaScript are usually represented as floating-point numbers. However, for displaying colors to users, we want to work with whole numbers instead.
To make sure our displayed colors are always whole numbers, we can use the
`Math.round()`
function to round the color properties of our `color`
state object to the nearest integer value. Simply call this function on each color property (`r`
, `g`
, `b`
, and `h`
) and voilà! Your colors will be looking sharp and whole.ts
const r = Math.round(color.r);
const g = Math.round(color.g);
const b = Math.round(color.b);
const h = Math.round(color.h);
const s = Math.round(color.s);
const v = Math.round(color.v);
To obtain a more precise result when dealing with the alpha property - a floating-point number between 0 and 1 - we multiplied it by 1000 before rounding and then divided by 1000 afterwards.
ts
const a = Math.round(color.a * 1000) / 1000;
By taking a simple step, we can significantly enhance the visual appearance of our color picker component, particularly when dealing with brighter or more saturated colors where even a slight change in value can be easily noticeable.
These numbers will be essential in generating the color in various formats, such as RGB, RGB with alpha, or HSV.
ts
const rgbColor = `${r} ${g} ${b}`;
const rgbaColor = `${r} ${g} ${b} / ${a}`;
const hslColor = `${h} 100% 50%`;
const hsvColor = `${h} ${s}% ${v}% ${a}`;
const hexColor = rgbToHex({ r, g, b, a });
#Creating a Saturation picker
We've made some progress with the
`Draggable`
component by adding `onMount`
and `onMove`
properties. Additionally, we've set up the data model to track the current color. Now, it's time to use this data to create the first piece of our color picker - the saturation picker.To accomplish this, we'll be utilizing the
`Draggable`
component we created earlier.tsx
<Draggable
onMove={handleChangeSaturation}
onMount={handleMountSaturation}
>
...
</Draggable>
The
`handleMountSaturation()`
function is called when the saturation picker is first loaded. It gets an object with the parent container's width and height, along with a function to update the handler's position.In this function, we calculate the initial
`dx`
and `dy`
offsets for our draggable handler based on the current saturation and brightness values in our color state. We then update our handler's state using the `setOffset`
function.By using this function, we can make sure that our draggable handler starts in the right position every time it's loaded.
ts
const handleMountSaturation = ({ parentWidth, parentHeight, setOffset }) => {
const dx = color.s * parentWidth;
const dy = (1 - color.v) * parentHeight;
setOffset({ dx, dy });
};
On the other hand, the
`handleChangeSaturation()`
function is responsible for updating the color state based on how the user interacts with the saturation picker.When the user moves the draggable element, this function receives an object that contains the current x and y offsets of the element relative to its initial position, as well as the width and height of its parent container. We then use these values to calculate new saturation and brightness values based on the draggable element's current location.
To convert these new saturation and brightness values into their corresponding RGB values, we use a handy utility function called
`hsvToRgb`
. Once again, we'll skip over the calculations that this helper function performs. If you're curious to see them, you can check out the file with the same name.We merge these new RGB values with our existing color state object using spread syntax (
`...`
) to create a new object that represents our updated color state.Lastly, we call
`setColor()`
with this new color state object to update our component's internal state.ts
const handleChangeSaturation = React.useCallback(({ dx, dy, parentWidth, parentHeight }) => {
const s = 100 * dx / parentWidth;
const v = 100 * (1 - (dy / parentHeight));
const rgb = hsvToRgb({
h: color.h,
s,
v,
});
setColor({
...color,
...rgb,
s,
v,
});
}, [color]);
Great, now that we have the color state ready, let's use its properties to preview the saturation area.
To do this, we can use CSS'
`linear-gradient`
property and set the background color of our container element. First, we'll set a gradient that goes from transparent to black, starting at 0% opacity and ending at 100% opacity. We'll make sure the gradient goes vertically from top to bottom. This creates a cool black-to-transparent fade-out effect as you move down the saturation area.css
background-image: linear-gradient(to bottom, transparent, black);
Next, we'll make a new gradient that goes from white to transparent, starting at 0% opacity on the left and gradually increasing to 100% opacity on the right. This will create a smooth white-to-transparent gradient that fades out as it moves towards the right.
css
background-image: linear-gradient(to right, white, transparent);
Lastly, we'll apply the current hue value and maximum saturation (100%) and brightness (50%) to set the background color of our container element.
tsx
backgroundColor: `hsl(${h} 100% 50%)`
By combining these three properties together in our container element's
`style`
object, we can create a preview for the saturation area that dynamically updates based on user interaction with the draggable handler.tsx
<div
style={{
backgroundImage: "linear-gradient(to bottom, transparent, black), linear-gradient(to right, white, transparent)",
backgroundColor: `hsl(${hue} 100% 50%)`,
}}
>
...
</div>
To preview the handler, we set its
`backgroundColor`
property to match the current RGB value of our color state object and adjust its `transform`
property to reflect the current `dx`
and `dy`
offset values.tsx
<div
className="draggable"
ref={ref}
style={{
backgroundColor: `rgb(${rgbColor})`,
transform: `translate(${dx}px, ${dy}px)`,
}}
/>
Go ahead and give it a try in the demo below. Our color picker component comes equipped with a draggable handler that dynamically updates its position and appearance based on user interaction. As you move the handler around the saturation area, you'll see it change color, giving you a live preview of the currently selected color.
#Creating a Hue picker
If you want to give your users the ability to change the hue property, you can use the
`Draggable`
component once again.tsx
<Draggable onMove={handleChangeHue} onMount={handleMountHue}>
...
</Draggable>
The
`handleMountHue()`
function gets called when the hue picker loads for the first time. It receives an object containing the width and height of the parent container, along with a function to update the handler's position.Inside this function, we calculate where to put the draggable handler initially, based on the current hue value in our color state.
To do this, we use the current hue value and multiply it by the parent container's width. This gives us a percentage-based value that represents where on the gradient we want our draggable handler to start. We then convert this to a pixel value by multiplying it with
`parentWidth`
, which gives us the exact horizontal position of our draggable handler.In short, this function sets the initial position of the draggable handler on the hue picker based on the current hue value.
ts
const dx = color.h * parentWidth;
After obtaining the
`dx`
offset, we use the `setOffset`
function to update our draggable handler's state. This ensures that it always starts in the correct position every time it loads.ts
const handleMountHue = ({ parentWidth, parentHeight, setOffset }) => {
const dx = color.h * parentWidth;
setOffset((offset) => ({
...offset,
dx,
}));
};
The
`handleChangeHue()`
function is responsible for updating the color state when the user interacts with the hue picker. As the user moves the draggable element, we receive an object with its current position and the size of its container.We use these values to calculate the new hue value based on the element's position. We multiply the element's position by 360 because the hue value is represented as an angle on a color wheel, where 0 and 360 degrees are the same color (red). This ensures that we get an accurate hue value within this range.
Next, we convert this new hue value into its corresponding RGB value using the
`hsvToRgb`
utility function. We then merge these new RGB values with our existing color state object using spread syntax (`...`
) to create a new object that represents our updated color state.Finally, we call
`setColor()`
with this new color state object to update our component's internal state. This ensures that our color picker works as expected and provides a smooth and seamless user experience.ts
const handleChangeHue = React.useCallback(({ dx, parentWidth }) => {
const h = (dx / parentWidth) * 360;
const rgb = hsvToRgb({
h,
s: color.s,
v: color.v,
});
setColor({
...color,
...rgb,
h,
});
}, [color]);
To preview the hue picker, we create a gradient background that goes from red to violet in seven steps, circling around the color wheel. Then, we set this gradient as the background image of our container element using the
`linear-gradient`
property of CSS.css
background-image: linear-gradient(
to right,
rgb(255 0 0),
rgb(255 255 0),
rgb(0 255 0),
rgb(0 255 255),
rgb(0 0 255),
rgb(255 0 255),
rgb(255 0 0)
);
This creates a smooth gradient that transitions from one color to another in a circular fashion, just like the colors on a traditional color wheel are arranged.
To preview the draggable handler, we set its
`backgroundColor`
property to match the current HSL value of our color state object. Then we adjust its `transform`
property to reflect the current `dx`
offset value.tsx
<div
className="draggable"
ref={ref}
style={{
backgroundColor: `hsl(${hslColor})`,
transform: `translate(${dx}px, 0)`,
}}
/>
By combining all of these properties into the
`style`
object of our container element, we can create an interactive preview of the hue picker that updates in real-time as the user interacts with the draggable handler.#Adding an Alpha picker
We're almost done with the color picker! The last thing we need is an Alpha picker. Luckily, we can use the
`Draggable`
component again, just like we did for the Saturation and Hue pickers.tsx
<Draggable onMove={handleChangeAlpha} onMount={handleMountAlpha}>
...
</Draggable>
The
`handleMountAlpha()`
function is quite similar to the `handleMountHue()`
function. But, it has a different job of setting the initial position of the draggable handler on the alpha picker.When we call this function, we get an object with the parent container's width and height, and a
`setOffset`
function that we can use to update the state of our draggable handler.To set the initial position of our draggable handler, we calculate its position based on the current alpha value in our color state object. We multiply the current alpha value by the parent container's width to get a percentage-based value that represents where on the gradient we want our draggable handler to start. We then convert this percentage-based value to a pixel value by multiplying it with the
`parentWidth`
, which gives us the exact horizontal position of our draggable handler.Finally, we call
`setOffset()`
with an updated offset object that includes our new `dx`
value. This ensures that our draggable handler always starts in the correct position every time it loads.ts
const handleMountAlpha = ({ parentWidth, parentHeight, setOffset }) => {
const dx = color.a * parentWidth;
setOffset((offset) => ({
...offset,
dx,
}));
};
The
`handleChangeAlpha()`
function is responsible for updating the alpha value in our color state object when the user interacts with the alpha picker. As the user moves the draggable element, we receive an object with its current position and the size of its container.To calculate the new alpha value based on the element's position, we divide the element's position by the width of its parent container. This gives us a percentage-based value that represents how far along the gradient our draggable handler is. This helps us get an accurate alpha value within the desired range.
Then, we create a new object by merging our existing color state with a new
`a`
property that represents our updated alpha value. Finally, we call `setColor()`
with this new color state object to update our component's internal state.ts
const handleChangeAlpha = ({ dx, parentWidth }) => {
const a = dx / parentWidth;
setColor({
...color,
a,
});
};
By combining all three picker components, you now have a complete color picker that enables users to select any color they desire. Additionally, they will receive real-time previews and feedback as they interact with its various parts.
Let's take a look at the final demo:
There are some functionalities that we can improve for our color picker. One way to do this is by adding support for different color formats. Currently, our color picker only supports the RGB color format, but many designers and developers prefer other formats like HSL, HEX, or CMYK.
By adding support for these formats, we can make our color picker more versatile and user-friendly, allowing users to select colors in their preferred format without having to convert them manually.
Another way to improve the color picker is by adding an option for users to input a specific color value directly into the color picker. This would allow them to enter an RGB, HSL, HEX, or CMYK value and have it displayed and selectable within the color picker. These changes will make our color picker more efficient and user-friendly, ultimately improving the overall experience for our users.
#Conclusion
To sum it up, building a color picker is an excellent way to enhance your React skills while learning about color theory and the basics of UI design.
Throughout this guide, we've covered various concepts, including using React hooks, designing custom components and utilities, and leveraging CSS properties like gradients and transforms to develop interactive UI elements.
By following these steps, you can create a versatile and user-friendly color picker that caters to the needs of designers and developers alike. However, there's always room for improvement by adding new features or optimizing existing ones.
We encourage you to experiment with different color formats or input methods, explore new design patterns or interactions, or integrate your color picker into other projects.
We hope that this post has provided you with a strong foundation for creating your own custom color pickers in React. By applying these principles and techniques to your own projects, you can design beautiful interfaces that engage users while providing them with valuable tools for their work.
Questions? 🙋
Do you have any questions about front-end development? If so, feel free to create a new issue on GitHub using the button below. I'm happy to help with any topic you'd like to learn more about, even beyond what's covered in this post.
While I have a long list of upcoming topics, I'm always eager to prioritize your questions and ideas for future content. Let's learn and grow together! Sharing knowledge is the best way to elevate ourselves 🥷.
Recent posts ⚡
Newsletter 🔔
If you're into front-end technologies and you want to see more of the content I'm creating, then you might want to consider subscribing to my newsletter.
By subscribing, you'll be the first to know about new articles, products, and exclusive promotions.
Don't worry, I won't spam you. And if you ever change your mind, you can unsubscribe at any time.
Phước Nguyễn