Skip to content
TypeScript
/* 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 modules
registerAllModules();
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 */
HTML
<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">
  1. 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().
  2. 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-cell and .color-picker-swatch center and style the swatch; .color-picker-editor styles the editor input.
    • ChangeDetectionStrategy.OnPush is used to optimize performance.
  3. Create the Editor Component

    The editor component uses the native HTML5 color input. When the user selects a color, the value is updated and afterClose calls finishEdit.emit() to save.

    @Component({
    selector: 'example1-color-picker-editor',
    template: `
    <input
    class="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 change
    • afterClose() calls finishEdit.emit() so the value is saved when the editor closes

    Lifecycle flow:

    1. User opens editor (double-click or F2)
    2. Input shows the current color
    3. User selects a color → onColorChange() calls setValue(input.value)
    4. User closes the editor (e.g. click outside) → afterClose() runs → finishEdit.emit() saves the value
  4. 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’s CustomValidatorFn<string> type
    • Uses regex to validate hex color format: # followed by 6 hex characters
    • Returns true for valid colors like “#FF0000”, “#00ff00”
    • Returns false for 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);
  5. 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 modules
    registerAllModules();
    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 HotTableModule for 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
    • HotTableModule provides the <hot-table> component
    • Global config applies to all Handsontable instances in the app
  6. Configure Handsontable

    Use the custom components in your Handsontable column configuration. The example adds a color property to each row (e.g. from inputData) 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 mapped color per row from inputData)
    • [settings]="gridSettings" - passes configuration object
    • editor: ColorPickerEditorComponent - uses custom editor class
    • renderer: 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 headerClassName align with the table layout

Enhancements

  1. 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);
    }
    }
  2. Custom Styling for Invalid Values

    Highlight invalid colors in the renderer:

    @Component({
    selector: "app-color-renderer-validated",
    changeDetection: ChangeDetectionStrategy.OnPush,
    template: `
    <div
    style="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);
    }
    }
  3. 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;
    }
    }
  4. Support RGB Format

    Extend validator to support RGB colors:

    const flexibleColorValidator = (value: string): boolean => {
    // Support both hex and rgb formats
    const 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.

  5. Add Renderer Props

    Pass configuration to renderer via rendererProps:

    @Component({
    selector: "app-color-renderer-configurable",
    changeDetection: ChangeDetectionStrategy.OnPush,
    template: `
    <div
    style="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.