JavaScript Data GridColor Picker Cell Type - Step-by-Step Guide
- Overview
- Complete Example
- What You'll Build
- Prerequisites
- Step 1: Import Dependencies
- Step 2: Add CSS Styling
- Step 3: Create the Renderer
- Step 4: Create the Validator
- Step 5: Editor - Initialize (init)
- Step 6: Editor - After Init Hook (afterInit)
- Step 7: Editor - After Open Hook (afterOpen)
- Step 8: Editor - After Close Hook (afterClose)
- Step 9: Editor - Get Value / Set Value
- Step 10: Editor - Keyboard Shortcuts
- Step 11: Complete Cell Definition
- Step 12: Use in Handsontable
- How It Works - Complete Flow
- Enhancements
Overview
This guide shows how to create a custom color picker cell using the Pickr (opens new window) library. Users can click a cell to open a color picker, select a color, and see it rendered with a colored background.
Difficulty: Beginner
Time: ~15 minutes
Libraries: @simonwep/pickr
Complete Example
What You'll Build
A cell that:
- Displays a colored circle swatch in the cell
- Opens a color picker when edited via an "Open color picker" button
- Shows a styled editor input with Handsontable's native blue border
- Validates hex color format
- Updates the value as you pick a color; closing the picker saves and finishes editing
- Closes the picker on Tab or Escape
Prerequisites
npm install @simonwep/pickr
Step 1: Import Dependencies
import Handsontable from 'handsontable/base';
import { registerAllModules } from 'handsontable/registry';
import { editorFactory } from 'handsontable/editors';
import { rendererFactory } from 'handsontable/renderers';
import Pickr from '@simonwep/pickr';
import '@simonwep/pickr/dist/themes/nano.min.css';
registerAllModules();
Why this matters:
editorFactoryandrendererFactoryare Handsontable helpers for creating custom editors and renderers- Pickr is created per-editor in the
afterInithook and attached to a button - Import Pickr theme CSS (e.g.
nano.min.css) for the color picker UI styling
Step 2: Add CSS Styling
Create a separate CSS file for the cell and editor styles. This uses Handsontable CSS custom properties (tokens) to match the native editor appearance.
.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%;
border: none;
outline: none;
box-sizing: border-box !important;
cursor: pointer;
-webkit-appearance: none;
appearance: none;
}
.pickr {
position: absolute;
top: 0;
left: 0;
opacity: 0;
pointer-events: none;
}
What's happening:
.color-picker-cellcenters the circle swatch inside the cell.color-picker-swatchrenders a small circle withborder-radius: 50%.color-picker-editorremoves default input borders and appearance so the editor can be styled as needed
Step 3: Create the Renderer
The renderer controls how the cell looks when not being edited. It displays a colored circle swatch.
renderer: rendererFactory(({ td, value }) => {
td.innerHTML = `<span class="color-picker-cell"><span class="color-picker-swatch" style="background:${value}"></span></span>`;
})
What's happening:
tdis the table cell DOM elementvalueis the cell's current value (e.g., "#ff0000")- We render a circle swatch with the color as its background
- The swatch is centered inside the cell via CSS flexbox
Step 4: Create the Validator
The validator ensures only valid hex colors are saved.
validator: (value, callback) => {
callback(value.length === 7 && value[0] == '#');
}
What's happening:
- Check if value is exactly 7 characters
- Check if it starts with '#'
- Call
callback(true)for valid,callback(false)for invalid
Improvements for production:
- Use regex:
/^#[0-9A-Fa-f]{6}$/ - Support short format:
#fff - Support RGB/RGBA values
Step 5: Editor - Initialize (init)
Create the input element that will hold the hex value, and apply the editor CSS class. A separate button for opening the picker is added in afterInit.
init(editor) {
editor.input = editor.hot.rootDocument.createElement('INPUT') as HTMLInputElement;
editor.input.setAttribute('aria-label', 'Open color picker');
editor.input.classList.add('color-picker-editor');
}
What's happening:
- Create an
inputelement usingeditor.hot.rootDocument.createElement() - Set an aria-label for accessibility
- Add the
color-picker-editorCSS class for the blue border styling - The
editorFactoryhelper will handle container creation and DOM insertion
Key points:
- Use
editor.hot.rootDocument.createElement()(notdocument.createElement()) for iframe/shadow DOM compatibility - The input stores the current hex value; a button added in
afterInitwill open the Pickr popup - The CSS class styles the editor (no border, full size) so the picker button is the main control
Step 6: Editor - After Init Hook (afterInit)
Create a button, attach Pickr to it, and set up the change and hide event handlers. Setting preventCloseElement keeps the editor open when the user clicks inside the Pickr popup.
afterInit(editor) {
const button = editor.hot.rootDocument.createElement('button');
button.textContent = 'Open color picker';
button.classList.add('color-picker-button');
editor.input.after(button);
editor.pickr = Pickr.create({
el: button,
theme: 'nano',
default: editor.input.value || '#000000',
components: {
preview: true,
hue: true,
}
});
editor.preventCloseElement = editor.pickr._root.app;
editor.pickr.on('change', (color) => {
if (color) {
const hex = color.toHEXA().toString();
editor.input.value = hex;
}
});
editor.pickr.on('hide', () => {
editor.finishEditing();
});
}
What's happening:
- Create a button and insert it after the input so users can open the picker
- Create a Pickr instance with theme
nanoand preview + hue components - Set
editor.preventCloseElementto the Pickr root so clicking inside the popup doesn't close the editor - On
change, update the input value from the selected color (usingcolor.toHEXA().toString()) - On
hide, calleditor.finishEditing()so closing the picker saves the value and closes the editor
Why afterInit instead of init?
afterInitruns after the input is added to the DOM- Pickr needs the button to be in the DOM to attach properly
Step 7: Editor - After Open Hook (afterOpen)
Set the current color and show the Pickr picker.
afterOpen(editor) {
editor.pickr.setColor(editor.input.value || '#000000');
editor.pickr.show();
}
What's happening:
- Sets the picker to the cell's current color
- Calls
show()to open the Pickr popup
Why afterOpen instead of open?
afterOpenruns after positioning is complete- The
editorFactoryhelper handles positioning inopen
Step 8: Editor - After Close Hook (afterClose)
Ensure the Pickr popup is hidden when the editor closes.
afterClose(editor) {
editor.pickr.hide();
}
What's happening:
- Called when the editor is closed (by Tab, Escape, clicking outside, etc.)
- Hides the Pickr picker via
editor.pickr.hide() - Without this, the picker popup could remain visible after the editor closes
Step 9: Editor - Get Value / Set Value
Standard value management hooks.
getValue(editor) {
return editor.input.value;
}
setValue(editor, value) {
editor.input.value = value;
}
What's happening:
getValuereturns the input's current value (hex color code) when Handsontable saves the cellsetValueinitializes the editor with the cell's current color value- On Pickr's
changeevent we seteditor.input.valuefrom the selected color; when the user closes the picker,hidefires and we calleditor.finishEditing()
Step 10: Editor - Keyboard Shortcuts
Add a Tab shortcut to hide the picker (which triggers the hide event and then finishEditing()).
shortcuts: [
{
keys: [['Tab']],
callback: (editor) => {
editor.pickr.hide();
},
},
]
What's happening:
- Pressing Tab hides the Pickr popup, which fires the
hideevent and callseditor.finishEditing() - Uses the
editorFactoryshortcuts API to register key bindings afterClosealso callseditor.pickr.hide()when the editor closes by other means
Step 11: Complete Cell Definition
Put it all together. The editor uses a hidden input for the value and a button that opens the Pickr popup; color is updated on change and editing finishes when the picker is closed (hide).
type ColorPickerEditor = {
input: HTMLInputElement;
pickr: ReturnType<typeof Pickr.create>;
preventCloseElement: HTMLElement;
};
const cellDefinition = {
renderer: rendererFactory(({ td, value }) => {
td.innerHTML = `<span class="color-picker-cell"><span class="color-picker-swatch" style="background:${value}"></span></span>`;
}),
validator: (value, callback) => {
callback(value.length === 7 && value[0] == '#');
},
editor: editorFactory<ColorPickerEditor>({
init(editor) {
editor.input = editor.hot.rootDocument.createElement('INPUT') as HTMLInputElement;
editor.input.setAttribute('aria-label', 'Open color picker');
editor.input.classList.add('color-picker-editor');
},
afterInit(editor) {
const button = editor.hot.rootDocument.createElement('button');
button.textContent = 'Open color picker';
button.classList.add('color-picker-button');
editor.input.after(button);
editor.pickr = Pickr.create({
el: button,
theme: 'nano',
default: editor.input.value || '#000000',
components: { preview: true, hue: true },
});
editor.preventCloseElement = editor.pickr._root.app;
editor.pickr.on('change', (color) => {
if (color) editor.input.value = color.toHEXA().toString();
});
editor.pickr.on('hide', () => editor.finishEditing());
},
afterOpen(editor) {
editor.pickr.setColor(editor.input.value || '#000000');
editor.pickr.show();
},
afterClose(editor) {
editor.pickr.hide();
},
getValue(editor) {
return editor.input.value;
},
setValue(editor, value) {
editor.input.value = value;
},
shortcuts: [
{
keys: [['Tab']],
callback: (editor) => editor.pickr.hide(),
},
],
}),
};
What's happening:
- renderer: Displays a colored circle swatch centered in the cell
- validator: Ensures hex color format (# followed by 6 characters)
- editor: Uses
editorFactoryhelper with:init: Creates styled input element and aria-labelafterInit: Creates button, Pickr on the button (nano theme),preventCloseElement, andchange/hidehandlersafterOpen: Sets current color and shows the pickerafterClose: Hides the Pickr popup when editor closesshortcuts: Tab key hides the picker (which triggershideand thenfinishEditing)getValue/setValue: Standard value management via the input
Note: The editorFactory helper handles container creation, positioning, and lifecycle management automatically.
Step 12: Use in Handsontable
const container = document.querySelector('#example1')!;
const hotOptions: Handsontable.GridSettings = {
data: [
{ id: 1, itemName: 'Lunar Core', color: '#FF5733', itemNo: 'XJ-12', cost: 350000, valueStock: 700000 },
{ id: 2, itemName: 'Zero Thrusters', color: '#33FF57', itemNo: 'QL-54', cost: 450000, valueStock: 0 },
{ id: 3, itemName: 'EVA Suits', color: '#3357FF', itemNo: 'PM-67', cost: 150000, valueStock: 7500000 },
],
colHeaders: ['ID', 'Item Name', 'Item Color', 'Item No.', 'Cost', 'Value in Stock'],
autoRowSize: true,
rowHeaders: true,
height: 'auto',
width: '100%',
columns: [
{ data: 'id', type: 'numeric', width: 80, headerClassName: 'htLeft' },
{ data: 'itemName', type: 'text', width: 200, headerClassName: 'htLeft' },
{ data: 'color', headerClassName: 'htLeft', ...cellDefinition },
{ data: 'itemNo', type: 'text', width: 100, headerClassName: 'htLeft' },
{ data: 'cost', type: 'numeric', width: 70, headerClassName: 'htLeft' },
{ data: 'valueStock', type: 'numeric', width: 130, headerClassName: 'htRight' },
],
licenseKey: 'non-commercial-and-evaluation',
};
const hot = new Handsontable(container, hotOptions);
Key configuration:
...cellDefinition- Spreads renderer, validator, and editor into the color column config- The validator ensures only valid hex colors are saved
- The live example uses more rows and random hex colors; you can use any data that includes a
colorfield
How It Works - Complete Flow
- Initial Render: Cell displays a colored circle swatch
- User Double-Clicks or F2: Editor opens with a styled input and an "Open color picker" button
- Color Picker Opens:
afterOpensets the current color and callspickr.show() - User Selects Color: Pickr updates the preview; the
changeevent updateseditor.input.valuewith the hex fromcolor.toHEXA().toString() - User Closes Picker (or presses Tab): The
hideevent fires (or Tab triggerspickr.hide()), we calleditor.finishEditing() - Validation: Validator checks hex format (# followed by 6 characters)
- Save: If valid, value is saved to cell; if invalid, editor may stay open
- Editor Closes:
afterClosecallseditor.pickr.hide(), cell renderer shows updated swatch
Enhancements
1. Add Color Swatches
Provide preset colors:
editor.pickr = Pickr.create({
el: button,
// ...other options
swatches: [
'#ff0000',
'#00ff00',
'#0000ff',
'#ffff00',
'#ff00ff',
'#00ffff'
],
});
2. Support Alpha Channel
Allow transparency by setting lockOpacity: false and enabling the opacity component:
Pickr.create({
// ...
lockOpacity: false,
components: {
preview: true,
opacity: true,
hue: true,
// ...
},
});
// Update validator for rgba
validator: (value, callback) => {
const rgbaRegex = /^rgba?\(\d+,\s*\d+,\s*\d+(?:,\s*[\d.]+)?\)$/;
callback(rgbaRegex.test(value));
}
3. Use a Different Theme
This recipe uses the nano theme. Pickr also offers classic and monolith. To switch, change the CSS import and the theme option:
import '@simonwep/pickr/dist/themes/classic.min.css';
// In Pickr.create():
theme: 'classic',
Congratulations! You've created a fully functional color picker cell using the Pickr library (nano theme) with the editorFactory helper, a button to open the picker, a circle swatch renderer, and native Handsontable editor styling!