Color picker
/* file: app.component.ts */import { Component, ChangeDetectionStrategy } from '@angular/core';import { GridSettings, HotCellEditorAdvancedComponent, HotCellRendererAdvancedComponent,} from '@handsontable/angular-wrapper';
export 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', },];
const colorValidator = (value: string): boolean => { return /^#[0-9A-Fa-f]{6}$/.test(value);};
@Component({ selector: 'example1-color-renderer', changeDetection: ChangeDetectionStrategy.OnPush, template: ` <div class="color-picker-cell"> <span class="color-picker-swatch" [style.background]="value"></span> </div>`, styles: ` :host { height: 100%; width: 100%; } .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); } `, standalone: false,})export class ColorRendererComponent extends HotCellRendererAdvancedComponent<string> {}
@Component({ selector: 'example1-color-picker-editor', template: ` <input class="color-picker-editor" type="color" [value]="value" (input)="onColorChange($event)" /> `, styles: ` :host { height: 100%; width: 100%; } .color-picker-editor { width: 100%; height: 100%; box-sizing: border-box !important; cursor: pointer; border: none; outline: none; } `, standalone: false,})export class ColorPickerEditorComponent extends HotCellEditorAdvancedComponent<string> { override afterClose(): void { this.finishEdit.emit(); }
onColorChange(event: Event): void { const input = event.target as HTMLInputElement; this.setValue(input.value); }}
@Component({ selector: 'example1-guide-color-picker-angular', standalone: false, template: ` <div> <hot-table [data]="data" [settings]="gridSettings"></hot-table> </div>`,})export class Example1GuideColorPickerAngularComponent { readonly 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() }`, }));
readonly gridSettings: GridSettings = { autoRowSize: true, rowHeaders: true, autoWrapRow: true, height: 'auto', width: '100%', manualColumnResize: true, manualRowResize: true, colHeaders: ['ID', 'Item Name', 'Item Color', 'Item No.', 'Cost', 'Value in Stock'], columns: [ { data: 'id', type: 'numeric', width: 80, headerClassName: 'htLeft', }, { data: 'itemName', type: 'text', width: 200, headerClassName: 'htLeft', }, { data: 'color', headerClassName: 'htLeft', editor: ColorPickerEditorComponent, renderer: ColorRendererComponent, validator: colorValidator, }, { data: 'itemNo', type: 'text', width: 100, headerClassName: 'htLeft', }, { data: 'cost', type: 'numeric', width: 70, headerClassName: 'htLeft', }, { data: 'valueStock', type: 'numeric', width: 130, headerClassName: 'htRight', }, ], };}/* end-file */
/* file: app.module.ts */import { NgModule, ApplicationConfig } from '@angular/core';import { BrowserModule } from '@angular/platform-browser';import { registerAllModules } from 'handsontable/registry';import { HOT_GLOBAL_CONFIG, HotGlobalConfig, HotTableModule } from '@handsontable/angular-wrapper';import { CommonModule } from '@angular/common';import { NON_COMMERCIAL_LICENSE } from '@handsontable/angular-wrapper';/* start:skip-in-compilation */import { Example1GuideColorPickerAngularComponent, ColorPickerEditorComponent, ColorRendererComponent,} from './app.component';/* end:skip-in-compilation */
// register Handsontable's modulesregisterAllModules();
export const appConfig: ApplicationConfig = { providers: [ { provide: HOT_GLOBAL_CONFIG, useValue: { license: NON_COMMERCIAL_LICENSE, } as HotGlobalConfig, }, ],};
@NgModule({ imports: [BrowserModule, HotTableModule, CommonModule], declarations: [Example1GuideColorPickerAngularComponent, ColorPickerEditorComponent, ColorRendererComponent], providers: [...appConfig.providers], bootstrap: [Example1GuideColorPickerAngularComponent],})export class AppModule {}/* end-file */<div> <example1-guide-color-picker-angular></example1-guide-color-picker-angular></div>Overview
This guide shows how to create a custom color picker cell in Angular using the native HTML5 color input. Users can click a cell to open a color picker, select a color, and see it rendered with a colored circle swatch. No external libraries are required.
Difficulty: Beginner
Time: ~15 minutes
Libraries: None (uses native HTML5 <input type="color">)
What You’ll Build
A cell that:
- Displays a colored circle swatch in the cell
- Opens a native HTML5 color picker when the cell is edited
- Validates hex color format
- Saves the value when a color is selected
Prerequisites
No external libraries required. This example uses:
@handsontable/angular-wrapper- Native HTML5
<input type="color">
Import Dependencies
import { Component, ChangeDetectionStrategy } from '@angular/core';import {GridSettings,HotCellEditorAdvancedComponent,HotCellRendererAdvancedComponent,} from '@handsontable/angular-wrapper';- Handsontable’s modules are registered in the Angular module (see Step 5) via
registerAllModules().
- Handsontable’s modules are registered in the Angular module (see Step 5) via
Create the Renderer Component
The renderer component controls how the cell looks when not being edited. It displays a colored circle swatch.
@Component({selector: 'example1-color-renderer',changeDetection: ChangeDetectionStrategy.OnPush,template: `<div class="color-picker-cell"><span class="color-picker-swatch" [style.background]="value"></span></div>`,styles: `:host {height: 100%;width: 100%;}.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;cursor: pointer;border: none;outline: none;}`,standalone: false,})export class ColorRendererComponent extends HotCellRendererAdvancedComponent<string> {}- The template renders a circle swatch with the cell’s color via
[style.background]="value". .color-picker-celland.color-picker-swatchcenter and style the swatch;.color-picker-editorstyles the editor input.ChangeDetectionStrategy.OnPushis used to optimize performance.
- The template renders a circle swatch with the cell’s color via
Create the Editor Component
The editor component uses the native HTML5 color input. When the user selects a color, the value is updated and
afterClosecallsfinishEdit.emit()to save.@Component({selector: 'example1-color-picker-editor',template: `<inputclass="color-picker-editor"type="color"[value]="value"(input)="onColorChange($event)"/>`,styleUrls: ['./example1.css'],standalone: false,})export class ColorPickerEditorComponent extends HotCellEditorAdvancedComponent<string> {override afterClose(): void {this.finishEdit.emit();}onColorChange(event: Event): void {const input = event.target as HTMLInputElement;this.setValue(input.value);}}What’s happening:
- Extends
HotCellEditorAdvancedComponent<string>- provides editor lifecycle <input type="color">is the native HTML5 color picker; no external library is required[value]="value"binds the current cell value;(input)="onColorChange($event)"updates the value on changeafterClose()callsfinishEdit.emit()so the value is saved when the editor closes
Lifecycle flow:
- User opens editor (double-click or F2)
- Input shows the current color
- User selects a color →
onColorChange()callssetValue(input.value) - User closes the editor (e.g. click outside) →
afterClose()runs →finishEdit.emit()saves the value
- Extends
Add Validator
The validator ensures only valid hex colors are saved to the cell.
const colorValidator = (value: string): boolean => {return /^#[0-9A-Fa-f]{6}$/.test(value);};What’s happening:
- Simple function returning
boolean- this is Angular’sCustomValidatorFn<string>type - Uses regex to validate hex color format:
#followed by 6 hex characters - Returns
truefor valid colors like “#FF0000”, “#00ff00” - Returns
falsefor invalid formats
Why add validation:
- Ensures data consistency
- Native color picker already outputs valid hex, but validation adds extra safety
Alternative validators:
// Support short format (#fff)const flexibleValidator = (value: string): boolean =>/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/.test(value);- Simple function returning
Register Components in Module
Register the custom components in your Angular module.
import { NgModule, ApplicationConfig } from '@angular/core';import { BrowserModule } from '@angular/platform-browser';import { registerAllModules } from 'handsontable/registry';import { HOT_GLOBAL_CONFIG, HotGlobalConfig, HotTableModule } from '@handsontable/angular-wrapper';import { CommonModule } from '@angular/common';import { NON_COMMERCIAL_LICENSE } from '@handsontable/angular-wrapper';import {Example1GuideColorPickerAngularComponent,ColorPickerEditorComponent,ColorRendererComponent,} from './app.component';// Register Handsontable's modulesregisterAllModules();export const appConfig: ApplicationConfig = {providers: [{provide: HOT_GLOBAL_CONFIG,useValue: {license: NON_COMMERCIAL_LICENSE,} as HotGlobalConfig,},],};@NgModule({imports: [BrowserModule, HotTableModule, CommonModule],declarations: [Example1GuideColorPickerAngularComponent, ColorPickerEditorComponent, ColorRendererComponent],providers: [...appConfig.providers],bootstrap: [Example1GuideColorPickerAngularComponent],})export class AppModule {}What’s happening:
- Import
HotTableModulefor Handsontable Angular integration - Declare custom components and the root example component in
declarations - Call
registerAllModules()to enable all Handsontable features - Configure global Handsontable settings via
HOT_GLOBAL_CONFIG - Set license (e.g.
NON_COMMERCIAL_LICENSE)
Key points:
- Custom editor and renderer must be declared in the same module
HotTableModuleprovides the<hot-table>component- Global config applies to all Handsontable instances in the app
- Import
Configure Handsontable
Use the custom components in your Handsontable column configuration. The example adds a
colorproperty to each row (e.g. frominputData) and passes it to the grid.@Component({selector: 'example1-guide-color-picker-angular',standalone: false,template: ` <div><hot-table [data]="data" [settings]="gridSettings"></hot-table></div>`,})export class Example1GuideColorPickerAngularComponent {readonly data = inputData.map((el) => ({...el,color: `#${Math.round(0x1000000 + 0xffffff * Math.random()).toString(16).slice(1).toUpperCase()}`,}));readonly gridSettings: GridSettings = {autoRowSize: true,rowHeaders: true,autoWrapRow: true,height: 'auto',width: '100%',manualColumnResize: true,manualRowResize: true,colHeaders: ['ID', 'Item Name', 'Item Color', 'Item No.', 'Cost', 'Value in Stock'],columns: [{data: 'id',type: 'numeric',width: 80,headerClassName: 'htLeft',},{data: 'itemName',type: 'text',width: 200,headerClassName: 'htLeft',},{data: 'color',headerClassName: 'htLeft',editor: ColorPickerEditorComponent,renderer: ColorRendererComponent,validator: colorValidator,},{data: 'itemNo',type: 'text',width: 100,headerClassName: 'htLeft',},{data: 'cost',type: 'numeric',width: 70,headerClassName: 'htLeft',},{data: 'valueStock',type: 'numeric',width: 130,headerClassName: 'htRight',},],};}What’s happening:
[data]="data"- binds data array to Handsontable (with mappedcolorper row frominputData)[settings]="gridSettings"- passes configuration objecteditor: ColorPickerEditorComponent- uses custom editor classrenderer: ColorRendererComponent- uses custom renderer class (circle swatch)validator: colorValidator- validates hex color format
Key configuration:
- Pass component classes directly (not instances)
- Angular wrapper handles component creation automatically
- Column widths and
headerClassNamealign with the table layout
Enhancements
Add Default Colors
Provide preset color options using a custom dropdown:
@Component({selector: "app-color-picker-editor-enhanced",template: `<div style="display: flex; flex-direction: column; height: 100%;"><input style="width: 100%; flex: 1;" type="color" [value]="value" (input)="onColorChange($event)" /><div style="display: flex; gap: 2px; padding: 2px;">@for (preset of presetColors; track preset) {<button[style.background]="preset"style="width: 20px; height: 20px; border: 1px solid #ccc; cursor: pointer;"(click)="selectPreset(preset)"></button>}</div></div>`,standalone: false,})export class ColorPickerEditorEnhancedComponent extends HotCellEditorAdvancedComponent<string> {presetColors = ["#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF"];override afterClose(): void {this.finishEdit.emit();}onColorChange(event: Event): void {const input = event.target as HTMLInputElement;this.setValue(input.value);}selectPreset(color: string): void {this.setValue(color);}}Custom Styling for Invalid Values
Highlight invalid colors in the renderer:
@Component({selector: "app-color-renderer-validated",changeDetection: ChangeDetectionStrategy.OnPush,template: `<divstyle="height: 100%; width: 100%; display: flex; align-items: center; justify-content: center;"[style.background]="isValid ? value : '#f0f0f0'"[style.color]="isValid ? '#000' : '#ff0000'"><b>{{ isValid ? value : "Invalid Color" }}</b></div>`,styles: `:host { height: 100%; width: 100%; }`,})export class ColorRendererValidatedComponent extends HotCellRendererAdvancedComponent<string> {get isValid(): boolean {return /^#[0-9A-Fa-f]{6}$/.test(this.value);}}Add Color Name Tooltip
Display color name on hover:
@Component({selector: "app-color-renderer-tooltip",changeDetection: ChangeDetectionStrategy.OnPush,template: `<div style="height: 100%; width: 100%;" [style.background]="value" [title]="getColorName()"><b>{{ value }}</b></div>`,styles: `:host { height: 100%; width: 100%; }`,})export class ColorRendererTooltipComponent extends HotCellRendererAdvancedComponent<string> {getColorName(): string {const colorNames: Record<string, string> = {"#FF0000": "Red","#00FF00": "Green","#0000FF": "Blue",// Add more colors...};return colorNames[this.value.toUpperCase()] || this.value;}}Support RGB Format
Extend validator to support RGB colors:
const flexibleColorValidator = (value: string): boolean => {// Support both hex and rgb formatsconst hexRegex = /^#[0-9A-Fa-f]{6}$/;const rgbRegex = /^rgb\(\d{1,3},\s*\d{1,3},\s*\d{1,3}\)$/;return hexRegex.test(value) || rgbRegex.test(value);};// Update column config{data: "color",editor: ColorPickerEditorComponent,renderer: ColorRendererComponent,validator: flexibleColorValidator,}Note: Native
<input type="color">always outputs hex format, so you’d need a custom text input editor to allow RGB input.Add Renderer Props
Pass configuration to renderer via
rendererProps:@Component({selector: "app-color-renderer-configurable",changeDetection: ChangeDetectionStrategy.OnPush,template: `<divstyle="height: 100%; width: 100%;"[style.background]="value"[style.border]="props.showBorder ? '2px solid #333' : 'none'">@if (props.showLabel) {<b>{{ value }}</b>}</div>`,styles: `:host { height: 100%; width: 100%; }`,})export class ColorRendererConfigurableComponent extends HotCellRendererAdvancedComponent<string, { showLabel: boolean; showBorder: boolean }> {get props() {return this.getProps();}}// In column config:{data: "color",editor: ColorPickerEditorComponent,renderer: ColorRendererConfigurableComponent,rendererProps: {showLabel: true,showBorder: false,},}
Congratulations! You’ve created a fully functional color picker cell in Angular using the native HTML5 color input, with a circle swatch renderer and hex validation. For a Pickr-based color picker (button + nano theme), see the JavaScript Color Picker recipe.