React Data GridPikaday Cell Type - Step-by-Step Guide
- Overview
- What You'll Build
- Complete Example
- Prerequisites
- Step 1: Import Dependencies
- Step 2: Define Date Formats
- Step 3: Define TypeScript Types
- Step 4: Create the Renderer
- Step 5: Editor - Initialize (init)
- Step 6: Editor - Get Date Picker Config (getDatePickerConfig)
- Step 7: Editor - Show Datepicker (showDatepicker)
- Step 8: Editor - Hide Datepicker (hideDatepicker)
- Step 9: Editor - After Open Hook (afterOpen)
- Step 10: Editor - After Close Hook (afterClose)
- Step 11: Editor - Get Value and Set Value
- Step 12: Editor - Get Date Format (getDateFormat)
- Step 13: Editor - Keyboard Shortcuts
- Step 14: Editor - Portal Positioning
- Step 15: Editor Input Styling (CSS)
- Step 16: Complete Cell Definition
- Step 17: Use in Handsontable
- How It Works - Complete Flow
- Migration from Built-in Date Cell Type
- Enhancements
- Accessibility
- Performance Considerations
Overview
This guide shows how to create a custom date picker cell using Pikaday (opens new window), a lightweight, no-dependencies date picker library. This guide is essential for migration - the built-in date cell type with Pikaday will be removed in the next Handsontable release. Use this recipe to maintain Pikaday functionality in your application.
Difficulty: Intermediate
Time: ~25 minutes
Libraries: @handsontable/pikaday, moment
What You'll Build
A cell that:
- Displays formatted dates (e.g., "12/31/2024" or "31/12/2024")
- Opens a beautiful calendar picker when edited
- Supports per-column configuration (date formats, first day of week, disabled dates)
- Handles keyboard navigation (arrow keys to navigate dates)
- Auto-closes and saves when a date is selected
- Works with portal positioning for better z-index handling
Complete Example
Prerequisites
npm install @handsontable/pikaday moment
Why these libraries?
@handsontable/pikaday- The Pikaday date picker library (Handsontable's fork)moment- Date formatting and parsing (can be replaced with date-fns, dayjs, etc.)
Step 1: Import Dependencies
import Handsontable from 'handsontable/base';
import { registerAllModules } from 'handsontable/registry';
import moment from 'moment';
import Pikaday from '@handsontable/pikaday';
import { CellProperties } from 'handsontable/settings';
import { editorFactory } from 'handsontable/editors';
import { rendererFactory } from 'handsontable/renderers';
registerAllModules();
What we're importing:
- Handsontable core and styles
editorFactoryandrendererFactoryfor creating custom cell type components- Pikaday for date picker functionality
- Moment for date formatting and parsing
Step 2: Define Date Formats
const DATE_FORMAT_US = 'MM/DD/YYYY';
const DEFAULT_DATE_FORMAT = DATE_FORMAT_US;
Why constants?
- Reusability across renderer and column configuration
- Single source of truth
- Easy to add more formats (EU, ISO, custom, etc.)
Step 3: Define TypeScript Types
Define types for editor properties and methods to ensure type safety.
type EditorPropertiesType = {
input: HTMLInputElement;
pickaday: Pikaday;
datePicker: HTMLDivElement;
parentDestroyed: boolean;
};
// Helper type to extract the editor type from factory callbacks
type FactoryEditorType<TProps, TMethods> = Parameters<
Parameters<
typeof editorFactory<TProps, TMethods>
>[0]["init"]
>[0];
type EditorMethodsType = {
showDatepicker: (
editor: FactoryEditorType<EditorPropertiesType, EditorMethodsType>,
event: Event | undefined,
) => void;
hideDatepicker: (
editor: FactoryEditorType<EditorPropertiesType, EditorMethodsType>,
) => void;
getDatePickerConfig: (
editor: FactoryEditorType<EditorPropertiesType, EditorMethodsType>,
) => Pikaday.PikadayOptions;
getDateFormat: (
editor: FactoryEditorType<EditorPropertiesType, EditorMethodsType>,
) => string;
};
What's happening:
EditorPropertiesType: Defines custom properties added to the editor instanceFactoryEditorType: Helper to extract the correct editor type from factory callbacksEditorMethodsType: Defines custom methods that will be available on the editor
Why this matters:
- Provides full TypeScript type safety
- Enables autocomplete in your IDE
- Catches type errors at compile time
Step 4: Create the Renderer
The renderer controls how the cell looks when not being edited.
renderer: rendererFactory(({ td, value, cellProperties }) => {
td.innerText = moment(new Date(value), cellProperties.renderFormat).format(
cellProperties.renderFormat,
);
})
What's happening:
valueis the raw date value (e.g., ISO string "2024-12-31" or formatted "12/31/2024")cellProperties.renderFormatis a custom property we'll set per columnmoment().format()converts to desired format- Display the formatted date
Why use cellProperties?
- Allows different columns to display dates differently
- One cell definition, multiple configurations
Step 5: Editor - Initialize (init)
Create the input element and set up event handling to prevent clicks on the datepicker from closing the editor.
init(editor) {
editor.parentDestroyed = false;
// Create the input element on init. This is a text input that date picker will be attached to.
editor.input = editor.hot.rootDocument.createElement(
'input',
) as HTMLInputElement;
// Use the container as the date picker container
editor.datePicker = editor.container;
/**
* Prevent recognizing clicking on datepicker as clicking outside of table.
*/
editor.hot.rootDocument.addEventListener('mousedown', (event) => {
if (
event.target &&
(event.target as HTMLElement).classList.contains('pika-day')
) {
editor.hideDatepicker(editor);
}
},
);
}
What's happening:
- Initialize
parentDestroyedflag to track editor lifecycle - Create an
inputelement usingeditor.hot.rootDocument.createElement() - Use the editor container as the Pikaday container
- Create event listener to handle clicks on datepicker days
- The
editorFactoryhelper handles container creation and DOM insertion
Key concepts:
The Event Listener pattern
This is crucial! Without it:
- User clicks cell to edit
- Pikaday calendar opens
- User clicks on a calendar day
- Handsontable thinks user clicked "outside" the editor
- Editor closes immediately!
Solution:
editor.hot.rootDocument.addEventListener('mousedown', (event) => {
if ((event.target as HTMLElement).classList.contains('pika-day')) {
editor.hideDatepicker(editor);
}
});
Why editor.hot.rootDocument.createElement()?
- Handsontable might be in an iframe or shadow DOM
editor.hot.rootDocumentensures correct document context- Ensures compatibility across different environments
Step 6: Editor - Get Date Picker Config (getDatePickerConfig)
Build the Pikaday configuration object with proper callbacks.
getDatePickerConfig(editor) {
const htInput = editor.input;
const options: Pikaday.PikadayOptions = {};
// Merge custom config from cell properties
if (editor.cellProperties && editor.cellProperties.datePickerConfig) {
Object.assign(options, editor.cellProperties.datePickerConfig);
}
const origOnSelect = options.onSelect;
const origOnClose = options.onClose;
// Configure Pikaday
options.field = htInput;
options.trigger = htInput;
options.container = editor.datePicker;
options.bound = false;
options.keyboardInput = false;
options.format = options.format ?? editor.getDateFormat(editor);
options.reposition = options.reposition || false;
options.isRTL = false;
// Handle date selection
options.onSelect = function (date) {
let dateStr;
if (!isNaN(date.getTime())) {
dateStr = moment(date).format(editor.getDateFormat(editor));
}
editor.setValue(dateStr);
if (origOnSelect) {
origOnSelect.call(editor.pickaday, date);
}
if (Handsontable.helper.isMobileBrowser()) {
editor.hideDatepicker(editor);
}
};
// Handle date picker close
options.onClose = () => {
if (!editor.parentDestroyed) {
editor.finishEditing(false);
}
if (origOnClose) {
origOnClose();
}
};
return options;
}
What's happening:
- Start with empty options object
- Merge custom config from
cellProperties.datePickerConfig(allows per-column customization) - Store original callbacks to preserve them
- Configure Pikaday with editor-specific settings
- Set up
onSelectto format date and save value - Set up
onCloseto finish editing
Key configuration options:
field: The input element Pikaday attaches totrigger: Element that triggers the picker (same as field)container: Where to render the calendar (editor container)bound: false: Don't position relative to fieldkeyboardInput: false: Disable direct keyboard input (we handle it via shortcuts)reposition: false: Don't auto-reposition (we handle positioning)
Step 7: Editor - Show Datepicker (showDatepicker)
Initialize and display the Pikaday calendar when the editor opens.
showDatepicker(editor, event) {
const dateFormat = editor.getDateFormat(editor);
// @ts-ignore
const isMouseDown = editor.hot.view.isMouseDown();
const isMeta = event && 'keyCode' in event
? Handsontable.helper.isFunctionKey((event as KeyboardEvent).keyCode)
: false;
let dateStr;
editor.datePicker.style.display = 'block';
// Create new Pikaday instance
editor.pickaday = new Pikaday(editor.getDatePickerConfig(editor));
// Configure Moment.js integration if available
// @ts-ignore
if (typeof editor.pickaday.useMoment === 'function') {
// @ts-ignore
editor.pickaday.useMoment(moment);
}
// @ts-ignore
editor.pickaday._onInputFocus = function () {};
// Handle existing value
if (editor.originalValue) {
dateStr = editor.originalValue;
if (moment(dateStr, dateFormat, true).isValid()) {
editor.pickaday.setMoment(moment(dateStr, dateFormat), true);
}
if (editor.getValue() !== editor.originalValue) {
editor.setValue(editor.originalValue);
}
if (!isMeta && !isMouseDown) {
editor.setValue('');
}
} else if (editor.cellProperties.defaultDate) {
dateStr = editor.cellProperties.defaultDate;
if (moment(dateStr, dateFormat, true).isValid()) {
editor.pickaday.setMoment(moment(dateStr, dateFormat), true);
}
if (!isMeta && !isMouseDown) {
editor.setValue('');
}
} else {
editor.pickaday.gotoToday();
}
}
What's happening:
- Get date format for parsing
- Check if mouse is down or function key pressed (for special behavior)
- Show the date picker container
- Create new Pikaday instance with configuration
- Configure Moment.js integration
- Disable input focus handler (we handle focus ourselves)
- Set initial date based on cell value, default date, or today
Key concepts:
Why create Pikaday instance in showDatepicker?
- Pikaday instance is created fresh each time editor opens
- Allows per-edit configuration changes
- Ensures clean state for each edit session
The isMeta and isMouseDown checks
- If function key (F2, etc.) or mouse is down, don't clear the input
- Preserves value when opening via keyboard or programmatically
- Provides better UX for keyboard users
Step 8: Editor - Hide Datepicker (hideDatepicker)
Close the Pikaday calendar.
hideDatepicker(editor) {
editor.pickaday.hide();
}
Step 9: Editor - After Open Hook (afterOpen)
Match the editor input dimensions to the cell and show the datepicker.
afterOpen(editor, event) {
const cellRect = editor.TD.getBoundingClientRect();
editor.input.style.width = `${cellRect.width}px`;
editor.input.style.height = `${cellRect.height}px`;
editor.showDatepicker(editor, event);
}
What's happening:
- Get the cell's bounding rectangle for exact dimensions
- Set the input width and height to match the cell
- Show the datepicker
Why getBoundingClientRect?
- Provides pixel-perfect dimensions matching the cell
- Works correctly regardless of cell padding, borders, or theme
- The CSS file handles the visual styling (borders, padding, focus states)
Step 10: Editor - After Close Hook (afterClose)
Clean up the Pikaday instance when the editor closes.
afterClose(editor) {
if (editor.pickaday.destroy) {
editor.pickaday.destroy();
}
}
Why this matters:
- Pikaday creates DOM elements and event listeners
- Without cleanup, these accumulate over time
- Essential for long-running applications
Step 11: Editor - Get Value and Set Value
Standard value management methods.
getValue(editor) {
return editor.input.value;
}
setValue(editor, value) {
editor.input.value = value;
}
Why simple?
- Pikaday automatically updates
input.valuewhen date is selected - We just read/write the input value
- Formatting is handled by Pikaday and our
onSelectcallback
Step 12: Editor - Get Date Format (getDateFormat)
Helper method to get the date format for the current cell.
getDateFormat(
editor: FactoryEditorType<EditorPropertiesType, EditorMethodsType>,
) {
return editor.cellProperties.dateFormat ?? DEFAULT_DATE_FORMAT;
}
Step 13: Editor - Keyboard Shortcuts
Add keyboard navigation for date selection.
shortcuts: [
{
keys: [['ArrowLeft']],
callback: (editor, _event) => {
editor.pickaday.adjustDate('subtract', 1);
_event.preventDefault();
},
},
{
keys: [['ArrowRight']],
callback: (editor, _event) => {
editor.pickaday.adjustDate('add', 1);
_event.preventDefault();
},
},
{
keys: [['ArrowUp']],
callback: (editor, _event) => {
editor.pickaday.adjustDate('subtract', 7);
_event.preventDefault();
},
},
{
keys: [['ArrowDown']],
callback: (editor, _event) => {
editor.pickaday.adjustDate('add', 7);
_event.preventDefault();
},
}
]
What's happening:
- ArrowLeft: Move back one day
- ArrowRight: Move forward one day
- ArrowUp: Move back one week (7 days)
- ArrowDown: Move forward one week (7 days)
Step 14: Editor - Portal Positioning
Use portal positioning for better z-index handling.
position: 'portal',
Why portal instead of container?
- Datepicker can extend beyond cell boundaries
- Portal ensures it's always on top
- Better for complex layouts
Step 15: Editor Input Styling (CSS)
Style the editor input to match Handsontable's native editor appearance using CSS custom properties.
.ht_editor_visible > input {
width: 100%;
height: 100%;
box-sizing: border-box !important;
border: none;
border-radius: 0;
outline: none;
margin-top: -1px;
margin-left: -1px;
box-shadow: inset 0 0 0 var(--ht-cell-editor-border-width, 2px)
var(--ht-cell-editor-border-color, #1a42e8),
0 0 var(--ht-cell-editor-shadow-blur-radius, 0) 0
var(--ht-cell-editor-shadow-color, transparent) !important;
background-color: var(--ht-cell-editor-background-color, #ffffff) !important;
padding: var(--ht-cell-vertical-padding, 4px)
var(--ht-cell-horizontal-padding, 8px) !important;
border: none !important;
font-family: inherit;
font-size: var(--ht-font-size);
line-height: var(--ht-line-height);
}
.ht_editor_visible > input:focus-visible {
border: none !important;
outline: none !important;
}
What's happening:
- Uses Handsontable's CSS custom properties (
--ht-cell-editor-*) for theme compatibility inset box-shadowreplaces borders for the blue editor highlight-1pxmargins correct portal positioning offset- Inherits font properties from the table for consistency
:focus-visibleoverrides prevent browser default focus styles
Step 16: Complete Cell Definition
Put it all together:
const cellDefinition: Pick<
CellProperties,
'renderer' | 'validator' | 'editor'
> = {
renderer: rendererFactory(({ td, value, cellProperties }) => {
td.innerText = moment(new Date(value), cellProperties.renderFormat).format(
cellProperties.renderFormat,
);
}),
editor: editorFactory<
EditorPropertiesType,
EditorMethodsType
>({
position: 'portal',
shortcuts: [
// ... keyboard shortcuts from Step 13
],
init(editor) {
// ... from Step 5
},
getDatePickerConfig(editor) {
// ... from Step 6
},
hideDatepicker(editor) {
// ... from Step 8
},
showDatepicker(editor, event) {
// ... from Step 7
},
afterClose(editor) {
// ... from Step 10
},
afterOpen(editor, event) {
// ... from Step 9
},
getValue(editor) {
// ... from Step 11
},
setValue(editor, value) {
// ... from Step 11
},
getDateFormat(editor) {
// ... from Step 12
},
}),
};
Step 17: Use in Handsontable
const container = document.querySelector('#example1')!;
const hotOptions: Handsontable.GridSettings = {
data,
colHeaders: ['Item Name', 'Category', 'Lead Engineer', 'Restock Date', 'Cost'],
autoRowSize: true,
rowHeaders: true,
height: 'auto',
width: '100%',
autoWrapRow: true,
headerClassName: 'htLeft',
columns: [
{ data: 'itemName', type: 'text', width: 130 },
{ data: 'category', type: 'text', width: 120 },
{ data: 'leadEngineer', type: 'text', width: 150 },
{
data: 'restockDate',
width: 150,
allowInvalid: false,
...cellDefinition,
renderFormat: DATE_FORMAT_US,
dateFormat: DATE_FORMAT_US,
correctFormat: true,
defaultDate: '01/01/2020',
datePickerConfig: {
firstDay: 0,
showWeekNumber: true,
disableDayFn(date: Date) {
return date.getDay() === 0 || date.getDay() === 6;
},
},
},
{
data: 'cost',
type: 'numeric',
width: 120,
className: 'htRight',
numericFormat: {
pattern: '$0,0.00',
culture: 'en-US',
},
},
],
licenseKey: 'non-commercial-and-evaluation',
};
const hot = new Handsontable(container, hotOptions);
Key configuration:
...cellDefinition- Spreads renderer and editor into the column configrenderFormat- Format for displaying dates in cellsdateFormat- Format for Pikaday date pickerdatePickerConfig- Additional Pikaday configuration optionsdefaultDate- Default date when cell is emptyheaderClassName: 'htLeft'- Left-aligns all column headers
How It Works - Complete Flow
- Initial Load: Cell displays formatted date (e.g., "08/01/2025")
- User Double-Clicks or F2: Editor opens, container positioned in portal
- After Open: Input sized to match cell via
getBoundingClientRect,showDatepickercalled - Show Datepicker: Pikaday instance created, calendar displayed
- User Selects Date:
onSelectcallback fires, formats date, saves value - User Clicks Day: Event listener detects click, hides datepicker, finishes editing
- After Close: Pikaday instance destroyed, memory cleaned up
- Re-render: Cell displays updated formatted date
Migration from Built-in Date Cell Type
If you're currently using the built-in date cell type with Pikaday, here's how to migrate:
Before (Built-in):
columns: [{
data: 'restockDate',
type: 'date',
dateFormat: 'MM/DD/YYYY',
datePickerConfig: {
firstDay: 0,
}
}]
After (Custom Editor):
columns: [{
data: 'restockDate',
...cellDefinition, // Add the custom cell definition
dateFormat: 'MM/DD/YYYY',
renderFormat: 'MM/DD/YYYY', // Add render format
datePickerConfig: {
firstDay: 0,
}
}]
Key differences:
- Replace
type: "date"with...cellDefinition - Add
renderFormatproperty (for cell display) - Keep
dateFormatanddatePickerConfig(they work the same)
Enhancements
1. Different Date Formats per Column
columns: [
{
data: 'restockDate',
...cellDefinition,
renderFormat: 'MM/DD/YYYY', // US format
dateFormat: 'MM/DD/YYYY',
},
{
data: 'restockDate',
...cellDefinition,
renderFormat: 'DD/MM/YYYY', // EU format
dateFormat: 'DD/MM/YYYY',
}
]
2. Date Range Restrictions
datePickerConfig: {
minDate: new Date('2024-01-01'),
maxDate: new Date('2024-12-31'),
disableDayFn(date) {
// Disable weekends
return date.getDay() === 0 || date.getDay() === 6;
}
}
3. Custom Date Formatting
Replace Moment.js with another library:
import { format, parse } from 'date-fns';
// In renderer
renderer: rendererFactory(({ td, value, cellProperties }) => {
td.innerText = format(new Date(value), cellProperties.renderFormat);
})
// In getDatePickerConfig
options.format = 'MM/DD/YYYY'; // Pikaday format string
4. Localization
import 'pikaday/css/pikaday.css';
import moment from 'moment';
import 'moment/locale/fr';
moment.locale('fr');
datePickerConfig: {
i18n: {
previousMonth: 'Mois précédent',
nextMonth: 'Mois suivant',
months: ['Janvier', 'Février', 'Mars', ...],
weekdays: ['Dimanche', 'Lundi', 'Mardi', ...],
weekdaysShort: ['Dim', 'Lun', 'Mar', ...]
}
}
Accessibility
Pikaday has good keyboard support out of the box:
Keyboard navigation:
- Arrow Keys: Navigate dates (via our shortcuts)
- Enter: Select current date
- Escape: Close datepicker
- Tab: Navigate to next field
- Page Up/Down: Navigate months
- Home/End: Navigate to first/last day of month
ARIA attributes: Pikaday automatically adds ARIA attributes for screen readers.
Performance Considerations
Why This Is Fast
- Lazy Initialization: Pikaday instance created only when editor opens
- Efficient Cleanup: Instance destroyed when editor closes
- Portal Positioning: Better z-index handling without performance cost
Congratulations! You've created a production-ready Pikaday date picker cell with full customization options, keyboard navigation, and proper lifecycle management. This recipe ensures you can continue using Pikaday even after the built-in date cell type is removed!