Colorful Picker
This tutorial shows you how to build a color picker cell in React using the react-colorful library and Handsontable’s EditorComponent.
import { HotTable, HotColumn, EditorComponent } from '@handsontable/react-wrapper';import { registerAllModules } from 'handsontable/registry';import { rendererFactory } from 'handsontable/renderers';import { HexColorPicker } from 'react-colorful';import './example1.css';
registerAllModules();
/* start:skip-in-preview */const inputData = [ { id: 640329, itemName: 'Lunar Core', itemNo: 'XJ-12', leadEngineer: 'Ellen Ripley', cost: 350000, inStock: true, category: 'Lander', itemQuality: 87, origin: '🇺🇸 USA', quantity: 2, valueStock: 700000, repairable: false, supplierName: 'TechNova', restockDate: '2025-08-01', operationalStatus: 'Awaiting Parts', }, { id: 863104, itemName: 'Zero Thrusters', itemNo: 'QL-54', leadEngineer: 'Sam Bell', cost: 450000, inStock: false, category: 'Propulsion', itemQuality: 0, origin: '🇩🇪 Germany', quantity: 0, valueStock: 0, repairable: true, supplierName: 'PropelMax', restockDate: '2025-09-15', operationalStatus: 'In Maintenance', }, { id: 395603, itemName: 'EVA Suits', itemNo: 'PM-67', leadEngineer: 'Alex Rogan', cost: 150000, inStock: true, category: 'Equipment', itemQuality: 79, origin: '🇮🇹 Italy', quantity: 50, valueStock: 7500000, repairable: true, supplierName: 'SuitCraft', restockDate: '2025-10-05', operationalStatus: 'Ready for Testing', }, { id: 679083, itemName: 'Solar Panels', itemNo: 'BW-09', leadEngineer: 'Dave Bowman', cost: 75000, inStock: true, category: 'Energy', itemQuality: 95, origin: '🇺🇸 USA', quantity: 10, valueStock: 750000, repairable: false, supplierName: 'SolarStream', restockDate: '2025-11-10', operationalStatus: 'Operational', }, { id: 912663, itemName: 'Comm Array', itemNo: 'ZR-56', leadEngineer: 'Louise Banks', cost: 125000, inStock: false, category: 'Communication', itemQuality: 0, origin: '🇯🇵 Japan', quantity: 0, valueStock: 0, repairable: true, supplierName: 'CommTech', restockDate: '2025-12-20', operationalStatus: 'Decommissioned', }, { id: 315806, itemName: 'Habitat Dome', itemNo: 'UJ-23', leadEngineer: 'Dr. Ryan Stone', cost: 1000000, inStock: true, category: 'Shelter', itemQuality: 93, origin: '🇨🇦 Canada', quantity: 3, valueStock: 3000000, repairable: false, supplierName: 'DomeInnovate', restockDate: '2026-01-25', operationalStatus: 'Operational', }, { id: 954632, itemName: 'Oxygen Unit', itemNo: 'FK-87', leadEngineer: 'Dr. Grace Augustine', cost: 600000, inStock: true, category: 'Life Support', itemQuality: 85, origin: '🇺🇸 USA', quantity: 15, valueStock: 9000000, repairable: true, supplierName: 'OxyGenius', restockDate: '2026-03-02', operationalStatus: 'Awaiting Parts', }, { id: 734944, itemName: 'Processing Rig', itemNo: 'LK-13', leadEngineer: 'Jake Sully', cost: 350000, inStock: true, category: 'Mining', itemQuality: 81, origin: '🇦🇺 Australia', quantity: 25, valueStock: 8750000, repairable: true, supplierName: 'RigTech', restockDate: '2026-04-15', operationalStatus: 'Ready for Testing', }, { id: 834662, itemName: 'Navigation Module', itemNo: 'XP-24', leadEngineer: 'Dr. Ellie Arroway', cost: 450000, inStock: true, category: 'Navigation', itemQuality: 89, origin: '🇫🇷 France', quantity: 8, valueStock: 3600000, repairable: false, supplierName: 'NavSolutions', restockDate: '2026-05-30', operationalStatus: 'In Maintenance', }, { id: 714329, itemName: 'Surveyor Arm', itemNo: 'QA-86', leadEngineer: 'Mark Watney', cost: 100000, inStock: true, category: 'Exploration', itemQuality: 78, origin: '🇺🇸 USA', quantity: 40, valueStock: 4000000, repairable: true, supplierName: 'ExploreTech', restockDate: '2026-07-12', operationalStatus: 'Decommissioned', },];
export const data = inputData.map((el) => ({ ...el, // eslint-disable-next-line no-mixed-operators color: `#${ // eslint-disable-next-line no-mixed-operators Math.round(0x1000000 + 0xffffff * Math.random()) .toString(16) .slice(1) .toUpperCase() }`,}));/* end:skip-in-preview */
const colorCellRenderer = rendererFactory(({ td, value }) => { td.innerHTML = `<span class="color-picker-cell"><span class="color-picker-swatch" style="background:${value}"></span></span>`;});
const colorValidator = (value, callback) => { callback(value.length === 7 && value[0] === '#'); // validate color format};
export const ColorPickerEditor = () => { return ( <EditorComponent> {({ value, setValue }) => ( <div className="color-picker-editor"> <input className="color-picker-editor-input" value={value} readOnly /> <div className="color-picker-editor-popover"> <HexColorPicker color={value || '#000000'} onChange={(color) => setValue(color)} /> </div> </div> )} </EditorComponent> );};
const ExampleComponent = () => { return ( <HotTable data={data} colHeaders={['ID', 'Item Name', 'Item Color', 'Item No.', 'Cost', 'Value in Stock']} autoRowSize={true} rowHeaders={true} height="auto" width="100%" licenseKey="non-commercial-and-evaluation" > <HotColumn data="id" type="numeric" width={80} className="htLeft" /> <HotColumn data="itemName" type="text" width={200} className="htLeft" /> <HotColumn data="color" width={120} editor={ColorPickerEditor} hotRenderer={colorCellRenderer} validator={colorValidator} className="htLeft" /> <HotColumn data="itemNo" type="text" width={100} className="htLeft" /> <HotColumn data="cost" type="numeric" width={70} className="htLeft" /> <HotColumn data="valueStock" type="numeric" width={130} className="htRight" /> </HotTable> );};
export default ExampleComponent;import { HotTable, HotColumn, EditorComponent } from '@handsontable/react-wrapper';import { registerAllModules } from 'handsontable/registry';import { rendererFactory } from 'handsontable/renderers';import { HexColorPicker } from 'react-colorful';import './example1.css';
registerAllModules();
/* start:skip-in-preview */const inputData = [ { id: 640329, itemName: 'Lunar Core', itemNo: 'XJ-12', leadEngineer: 'Ellen Ripley', cost: 350000, inStock: true, category: 'Lander', itemQuality: 87, origin: '🇺🇸 USA', quantity: 2, valueStock: 700000, repairable: false, supplierName: 'TechNova', restockDate: '2025-08-01', operationalStatus: 'Awaiting Parts', }, { id: 863104, itemName: 'Zero Thrusters', itemNo: 'QL-54', leadEngineer: 'Sam Bell', cost: 450000, inStock: false, category: 'Propulsion', itemQuality: 0, origin: '🇩🇪 Germany', quantity: 0, valueStock: 0, repairable: true, supplierName: 'PropelMax', restockDate: '2025-09-15', operationalStatus: 'In Maintenance', }, { id: 395603, itemName: 'EVA Suits', itemNo: 'PM-67', leadEngineer: 'Alex Rogan', cost: 150000, inStock: true, category: 'Equipment', itemQuality: 79, origin: '🇮🇹 Italy', quantity: 50, valueStock: 7500000, repairable: true, supplierName: 'SuitCraft', restockDate: '2025-10-05', operationalStatus: 'Ready for Testing', }, { id: 679083, itemName: 'Solar Panels', itemNo: 'BW-09', leadEngineer: 'Dave Bowman', cost: 75000, inStock: true, category: 'Energy', itemQuality: 95, origin: '🇺🇸 USA', quantity: 10, valueStock: 750000, repairable: false, supplierName: 'SolarStream', restockDate: '2025-11-10', operationalStatus: 'Operational', }, { id: 912663, itemName: 'Comm Array', itemNo: 'ZR-56', leadEngineer: 'Louise Banks', cost: 125000, inStock: false, category: 'Communication', itemQuality: 0, origin: '🇯🇵 Japan', quantity: 0, valueStock: 0, repairable: true, supplierName: 'CommTech', restockDate: '2025-12-20', operationalStatus: 'Decommissioned', }, { id: 315806, itemName: 'Habitat Dome', itemNo: 'UJ-23', leadEngineer: 'Dr. Ryan Stone', cost: 1000000, inStock: true, category: 'Shelter', itemQuality: 93, origin: '🇨🇦 Canada', quantity: 3, valueStock: 3000000, repairable: false, supplierName: 'DomeInnovate', restockDate: '2026-01-25', operationalStatus: 'Operational', }, { id: 954632, itemName: 'Oxygen Unit', itemNo: 'FK-87', leadEngineer: 'Dr. Grace Augustine', cost: 600000, inStock: true, category: 'Life Support', itemQuality: 85, origin: '🇺🇸 USA', quantity: 15, valueStock: 9000000, repairable: true, supplierName: 'OxyGenius', restockDate: '2026-03-02', operationalStatus: 'Awaiting Parts', }, { id: 734944, itemName: 'Processing Rig', itemNo: 'LK-13', leadEngineer: 'Jake Sully', cost: 350000, inStock: true, category: 'Mining', itemQuality: 81, origin: '🇦🇺 Australia', quantity: 25, valueStock: 8750000, repairable: true, supplierName: 'RigTech', restockDate: '2026-04-15', operationalStatus: 'Ready for Testing', }, { id: 834662, itemName: 'Navigation Module', itemNo: 'XP-24', leadEngineer: 'Dr. Ellie Arroway', cost: 450000, inStock: true, category: 'Navigation', itemQuality: 89, origin: '🇫🇷 France', quantity: 8, valueStock: 3600000, repairable: false, supplierName: 'NavSolutions', restockDate: '2026-05-30', operationalStatus: 'In Maintenance', }, { id: 714329, itemName: 'Surveyor Arm', itemNo: 'QA-86', leadEngineer: 'Mark Watney', cost: 100000, inStock: true, category: 'Exploration', itemQuality: 78, origin: '🇺🇸 USA', quantity: 40, valueStock: 4000000, repairable: true, supplierName: 'ExploreTech', restockDate: '2026-07-12', operationalStatus: 'Decommissioned', },];
export const data = inputData.map((el) => ({ ...el, // eslint-disable-next-line no-mixed-operators color: `#${ // eslint-disable-next-line no-mixed-operators Math.round(0x1000000 + 0xffffff * Math.random()) .toString(16) .slice(1) .toUpperCase() }`,}));/* end:skip-in-preview */
const colorCellRenderer = rendererFactory(({ td, value }) => { td.innerHTML = `<span class="color-picker-cell"><span class="color-picker-swatch" style="background:${value}"></span></span>`;});
const colorValidator = (value: string, callback: (valid: boolean) => void) => { callback(value.length === 7 && value[0] === '#'); // validate color format};
export const ColorPickerEditor = () => { return ( <EditorComponent> {({ value, setValue }) => ( <div className="color-picker-editor"> <input className="color-picker-editor-input" value={value} readOnly /> <div className="color-picker-editor-popover"> <HexColorPicker color={value || '#000000'} onChange={(color) => setValue(color)} /> </div> </div> )} </EditorComponent> );};
const ExampleComponent = () => { return ( <HotTable data={data} colHeaders={['ID', 'Item Name', 'Item Color', 'Item No.', 'Cost', 'Value in Stock']} autoRowSize={true} rowHeaders={true} height="auto" width="100%" licenseKey="non-commercial-and-evaluation" > <HotColumn data="id" type="numeric" width={80} className="htLeft" /> <HotColumn data="itemName" type="text" width={200} className="htLeft" /> <HotColumn data="color" width={120} editor={ColorPickerEditor} hotRenderer={colorCellRenderer} validator={colorValidator} className="htLeft" /> <HotColumn data="itemNo" type="text" width={100} className="htLeft" /> <HotColumn data="cost" type="numeric" width={70} className="htLeft" /> <HotColumn data="valueStock" type="numeric" width={130} className="htRight" /> </HotTable> );};
export default ExampleComponent;.color-picker-cell { display: flex; align-items: center; justify-content: center;}
.color-picker-swatch { width: 18px; height: 18px; border-radius: 50%; flex-shrink: 0; border: 1px solid rgba(0, 0, 0, 0.15);}
.color-picker-editor { width: 100%; height: 100%; box-sizing: border-box !important; border: none; border-radius: 0; outline: none;}
.color-picker-editor-popover { position: absolute; top: 100%; left: 0; padding-top: 8px;}
.color-picker-editor:focus { outline: none;}
.color-picker-editor-input { width: 100%; height: 100%; box-sizing: border-box !important; border: none; border-radius: 0; outline: none;}Overview
This guide shows how to create a color picker editor cell using react-colorful’s HexColorPicker with React’s EditorComponent. Perfect for selecting colors in design tools, theme builders, or any scenario where users need to choose hex color values.
Difficulty: Beginner Time: ~15 minutes Libraries: react-colorful
What You’ll Build
A cell that:
- Displays an input showing the hex value and a popover with a color picker when editing
- Shows the selected color as a swatch when viewing (custom renderer)
- Stores values as hex color strings (e.g.,
#FF5733) - Validates input so only valid hex values (e.g.
#RRGGBB) are accepted - Works with React’s component-based architecture
- Uses
hotRendererto display color swatches in cells
Prerequisites
npm install @handsontable/react-wrapper react-colorfulWhat you need:
- React 16.8+ (hooks support)
@handsontable/react-wrapperpackagereact-colorfulpackage for the color picker UI- Basic React knowledge (hooks, JSX)
Import Dependencies
import { HotTable, HotColumn, EditorComponent } from '@handsontable/react-wrapper';import { registerAllModules } from 'handsontable/registry';import { rendererFactory } from 'handsontable/renderers';import { HexColorPicker } from 'react-colorful';registerAllModules();What we’re importing:
EditorComponent- React component for creating custom editorsHotTableandHotColumn- React wrapper componentsrendererFactory- For the custom cell renderer (color swatch)HexColorPicker- Lightweight color picker from react-colorful
Create the Editor Component
Create a React component that uses
EditorComponentwith the render prop pattern. The editor shows an input with the current hex value and a popover containing the color picker.export const ColorPickerEditor = () => {return (<EditorComponent>{({ value, setValue }) => (<div className="color-picker-editor"><input className="color-picker-editor-input" value={value} readOnly /><div className="color-picker-editor-popover"><HexColorPicker color={value || '#000000'} onChange={(color) => setValue(color)} /></div></div>)}</EditorComponent>);};What’s happening:
EditorComponentwraps your editor UI- The
childrenprop is a function that receives editor state value- Current cell value (hex color string)setValue- Function to update the value- The input displays the current hex value; the popover shows
HexColorPickerfor picking a color - When the user selects a color,
setValue(color)updates the value; the editor closes on blur (e.g. clicking outside)
Key concepts:
- Render prop pattern:
EditorComponentuses a function as children - Controlled component:
HexColorPickerreceivescolorandonChange - Input + popover: The input shows the hex code; the popover is positioned below and contains the picker
Add Styling
Style the cell (swatch display), the editor container, the input, and the popover. The popover is positioned below the editor so the color picker appears when the cell is in edit mode.
.color-picker-cell {display: flex;align-items: center;justify-content: center;}.color-picker-swatch {width: 18px;height: 18px;border-radius: 50%;flex-shrink: 0;border: 1px solid rgba(0, 0, 0, 0.15);}.color-picker-editor {width: 100%;height: 100%;box-sizing: border-box !important;border: none;border-radius: 0;outline: none;}.color-picker-editor-popover {position: absolute;top: 100%;left: 0;padding-top: 8px;}.color-picker-editor:focus {outline: none;}.color-picker-editor-input {width: 100%;height: 100%;box-sizing: border-box !important;border: none;border-radius: 0;outline: none;}What’s happening:
.color-picker-celland.color-picker-swatchdisplay a circular color swatch in the cell.color-picker-editoris the editor wrapper;.color-picker-editor-inputstyles the hex value input.color-picker-editor-popoverpositions the color picker below the editor (e.g.top: 100%,left: 0)HexColorPickerhas built-in styling from react-colorful
Prepare Sample Data
Use an array of row objects and add a
colorproperty with random hex values. The example uses an inventory-style dataset (with columns likeid,itemName,itemNo,cost,valueStock, and optionally more); you can use any structure as long as each row has acolorfield.const inputData = [{ id: 640329, itemName: 'Lunar Core', itemNo: 'XJ-12', cost: 350000, valueStock: 700000 },{ id: 863104, itemName: 'Zero Thrusters', itemNo: 'QL-54', cost: 450000, valueStock: 0 },// ... more rows];export const data = inputData.map((el) => ({...el,color: `#${Math.round(0x1000000 + 0xffffff * Math.random()).toString(16).slice(1).toUpperCase()}`,}));What’s happening:
- Each row is an object with columns such as
id,itemName,itemNo,cost,valueStock - A
colorproperty is added with a random hex value in#RRGGBBformat
- Each row is an object with columns such as
Use in Handsontable
Use the editor, custom renderer, and validator on the color column. The example table has multiple columns; the color column uses
hotRendererandvalidator.const colorCellRenderer = rendererFactory(({ td, value }) => {td.innerHTML = `<span class="color-picker-cell"><span class="color-picker-swatch" style="background:${value}"></span></span>`;});const colorValidator = (value, callback) => {callback(value.length === 7 && value[0] === '#'); // validate hex format};const ExampleComponent = () => {return (<HotTabledata={data}colHeaders={['ID', 'Item Name', 'Item Color', 'Item No.', 'Cost', 'Value in Stock']}autoRowSize={true}rowHeaders={true}height="auto"width="100%"licenseKey="non-commercial-and-evaluation"><HotColumn data="id" type="numeric" width={80} className="htLeft" /><HotColumn data="itemName" type="text" width={200} className="htLeft" /><HotColumndata="color"width={120}editor={ColorPickerEditor}hotRenderer={colorCellRenderer}validator={colorValidator}className="htLeft"/><HotColumn data="itemNo" type="text" width={100} className="htLeft" /><HotColumn data="cost" type="numeric" width={70} className="htLeft" /><HotColumn data="valueStock" type="numeric" width={130} className="htRight" /></HotTable>);};What’s happening:
editor={ColorPickerEditor}- Assigns the color picker editor to the columnhotRenderer={colorCellRenderer}- Renders a color swatch in the cell instead of the raw hex stringvalidator={colorValidator}- Ensures only valid hex values (e.g.#RRGGBB) are accepteddata="color"- Binds to thecolorproperty in each row; double-click opens the picker
Key features:
- Swatch display via custom renderer, validation, and type-safe editor with TypeScript
How It Works - Complete Flow
- Initial Render: Cell shows a color swatch (custom renderer); value is stored as hex (e.g.
#FF5733) - User Double-Clicks or Enter: Editor opens
- Editor Opens:
EditorComponentpositions container over cell; input shows current hex value; popover withHexColorPickerappears below - User Interaction: Drag sliders or click palette →
setValue(color)updates the value; input and picker stay in sync - Close: User blurs the editor (e.g. clicks outside) → value is saved and editor closes
- Validation:
colorValidatorensures value is valid hex before saving - Save: Hex value saved to cell; cell renderer shows the new swatch
Enhancements
Custom Renderer with Color Swatch (and Optional Hex Label)
The example uses
hotRendererto show only a circular swatch. You can extend it to show the hex code next to the swatch:import { rendererFactory } from 'handsontable/renderers';// Swatch only (as in the example)const colorCellRenderer = rendererFactory(({ td, value }) => {td.innerHTML = `<span class="color-picker-cell"><span class="color-picker-swatch" style="background:${value}"></span></span>`;});// Or swatch + hex labelconst colorRendererWithHex = rendererFactory(({ td, value }) => {const hexColor = value || '#cccccc';td.innerHTML = `<div style="display: flex; align-items: center; gap: 8px; padding: 4px;"><div style="width: 24px; height: 24px; background: ${hexColor}; border: 1px solid #ddd; border-radius: 4px;"></div><span style="font-family: monospace; font-size: 12px;">${hexColor}</span></div>`;});// Use hotRenderer in HotColumn<HotColumndata="color"editor={ColorPickerEditor}hotRenderer={colorCellRenderer}validator={colorValidator}/>What’s happening:
hotRendereris used in the React wrapper for custom cell rendering- Swatch-only keeps the cell compact; adding the hex label improves readability when needed
Explicit “Apply” Button (Optional)
The example closes the editor on blur. If you prefer explicit confirmation, add a button and use
finishEditingfrom the render props:<EditorComponent>{({ value, setValue, finishEditing }) => (<div className="color-picker-editor"><input className="color-picker-editor-input" value={value} readOnly /><div className="color-picker-editor-popover"><HexColorPicker color={value || '#000000'} onChange={(color) => setValue(color)} /></div><button className="button" type="button" onClick={() => finishEditing()}>Apply Colour</button></div>)}</EditorComponent>Using External CSS File
The example uses a separate CSS file for cell and editor styles (e.g.
example1.css). Import it in your component:import './example1.css'; // or your color-picker-editor.cssconst ColorPickerEditor = () => {// ... component code};Keep the
.color-picker-cell,.color-picker-swatch,.color-picker-editor,.color-picker-editor-input, and.color-picker-editor-popoverrules so the swatch and the editor look correct.RGBA or HSL Variants
react-colorfulalso providesRgbaColorPickerandHslColorPicker. For RGBA:import { RgbaColorPicker } from 'react-colorful';// Note: value would need to be stored as rgba string or object// and converted appropriatelyDefault Color
Handle empty cells with a sensible default:
<HexColorPickercolor={value || '#000000'}onChange={(color) => setValue(color)}/>The
|| '#000000'ensures the picker always has a valid color.
Accessibility
The HexColorPicker from react-colorful provides keyboard support. For the editor input, you can add an aria-label so screen readers describe the field:
<input className="color-picker-editor-input" value={value} aria-label="Hex color value"/>Keyboard navigation:
- Tab: Navigate to editor
- Escape: Cancel editing
- Click: Direct selection in color picker
Performance Considerations
Why This Is Fast
- react-colorful: Lightweight (~2KB gzipped), no heavy dependencies
- React Virtual DOM: Efficient updates only when value changes
- Controlled updates:
onChangeonly fires when user interacts - No unnecessary re-renders: Editor unmounts when closed
Bundle Size
react-colorful is tree-shakeable. If you only need hex:
import { HexColorPicker } from 'react-colorful';TypeScript Support
EditorComponent is fully typed. For the color editor, the value is a string (hex). You can type the validator as well:
const colorValidator = (value: string, callback: (valid: boolean) => void) => { callback(value.length === 7 && value[0] === '#'); // validate color format};
export const ColorPickerEditor = () => { return ( <EditorComponent> {({ value, setValue }) => ( <div className="color-picker-editor"> <input className="color-picker-editor-input" value={value} readOnly /> <div className="color-picker-editor-popover"> <HexColorPicker color={value || '#000000'} onChange={(color) => setValue(color)} /> </div> </div> )} </EditorComponent> );};Best Practices
- Provide fallback for empty values - Use
value || '#000000'for HexColorPicker - Use hotRenderer - Display color swatches in cells for better UX
- Store as hex - Hex strings are portable and validate with a single regex
- Input + popover - Showing the hex in an input with the picker in a popover keeps the cell compact and clear
- Optional “Apply” button - For explicit confirmation, add a button that calls
finishEditing()from the render props
What you learned
You integrated the react-colorful library as a Handsontable cell editor in React. You used EditorComponent with the render prop pattern to manage editor state, rendererFactory to display a color swatch, and CSS custom properties to style the editor.
Next steps
- Color Picker (JavaScript) - The same pattern using
editorFactoryand the Pickr library. - Color Picker (Angular) - The Angular version using
HotCellEditorAdvancedComponentand the native HTML5 color input. - Star Rating (React) - Another custom editor using
EditorComponentin React.