Feedback
Overview
This guide shows how to create a feedback editor cell using emoji buttons (👍, 👎, 🤷) with React’s EditorComponent. The example uses a feature-roadmap table with a Feedback column; the editor supports quick selection, keyboard navigation (Arrow keys, Tab), and per-column configuration via external CSS.
Difficulty: Beginner Time: ~15 minutes Libraries: None
What You’ll Build
A cell that:
- Displays emoji feedback buttons (👍, 👎, 🤷) when editing
- Shows the selected emoji when viewing
- Supports keyboard navigation (Arrow Left/Right and Tab to cycle options)
- Provides click-to-select and closes the editor on choice
- Uses React’s
EditorComponentwith render prop and external CSS (e.g.feedback-editorclass) - Reads per-column options from
cellProperties.configinonPrepare
Prerequisites
npm install @handsontable/react-wrapperWhat you need:
- React 16.8+ (hooks support)
@handsontable/react-wrapperpackage- Basic React knowledge (hooks, JSX)
Import Dependencies
import { useState, useEffect, useCallback, ComponentProps } from 'react';import { HotTable, HotColumn, EditorComponent } from '@handsontable/react-wrapper';import { registerAllModules } from 'handsontable/registry';registerAllModules();What we’re importing:
EditorComponent- React component for creating custom editorsHotTableandHotColumn- React wrapper components- React hooks for state management
- Handsontable styles
Create the Editor Component
Create a React component that uses
EditorComponentwith the render prop pattern.type EditorComponentProps = ComponentProps<typeof EditorComponent<string>>;const FeedbackEditor = () => {const [config, setConfig] = useState<string[]>(['👍', '👎', '🤷']);return (<EditorComponent<string>>{({ value, setValue, finishEditing }) => (<div className="editor">{config.map((item) => (<buttonkey={item}className={`button ${value === item ? 'active' : ''}`}onClick={() => {setValue(item);finishEditing();}}>{item}</button>))}</div>)}</EditorComponent>);};What’s happening:
EditorComponentwraps your editor UI- The
childrenprop is a function that receives editor state value- Current editor valuesetValue- Function to update the valuefinishEditing- Function to save and close the editor- Render buttons for each option in the config
- Highlight the active button based on current value
Key concepts:
- Render prop pattern:
EditorComponentuses a function as children - State management:
valueandsetValueare provided byEditorComponent - React components: Use standard React patterns (JSX, className, onClick)
Add Styling
Style the editor container and buttons using CSS or inline styles.
const FeedbackEditor = () => {const [config, setConfig] = useState<string[]>(['👍', '👎', '🤷']);return (<EditorComponent<string>>{({ value, setValue, finishEditing }) => (<><style>{`.editor {box-sizing: border-box;display: flex;gap: 3px;padding: 3px;background: rgb(238, 238, 238);border: 1px solid rgb(204, 204, 204);border-radius: 4px;height: 100%;width: 100%;}.button.active {background: #007bff;color: white;}.button:hover {background: #f0f0f0;}.button {background: #fff;color: black;border: none;padding: 0;margin: 0;height: 100%;width: 100%;font-size: 16px;font-weight: bold;text-align: center;cursor: pointer;}`}</style><div className="editor">{config.map((item, _index, _array) => (<buttonkey={item}className={`button ${value === item ? 'active' : ''}`}onClick={() => {setValue(item);finishEditing();}}style={{width: `${100 / _array.length}%`}}>{item}</button>))}</div></>)}</EditorComponent>);};What’s happening:
- Container uses flexbox for horizontal button layout
- Buttons dynamically size based on config length
- Active button has blue background
- Hover effects for better UX
Key styling:
display: flex- Horizontal button layoutgap: 3px- Space between buttonswidth: ${100 / _array.length}%- Dynamic button width.activeclass - Highlights selected button
Read Config from Cell Properties
Use
onPrepareto read per-column configuration.const FeedbackEditor = () => {const [config, setConfig] = useState<string[]>(['👍', '👎', '🤷']);const onPrepare: EditorComponentProps['onPrepare'] = (_row,_column,_prop,_TD,_originalValue,cellProperties) => {// Read config from column definitionif (cellProperties.config) {setConfig(cellProperties.config as string[]);}};return (<EditorComponent<string> onPrepare={onPrepare}>{({ value, setValue, finishEditing }) => (// ... editor UI)}</EditorComponent>);};What’s happening:
onPrepareis called before the editor openscellPropertiescontains column-specific configuration- Read
configfromcellProperties.config - Update state to reflect column-specific options
Why this matters:
- Different columns can have different options
- One editor component, multiple configurations
- Dynamic options based on column settings
Add Keyboard Shortcuts
Add keyboard navigation using the
shortcutsprop.const FeedbackEditor = () => {const [config, setConfig] = useState<string[]>(['👍', '👎', '🤷']);const [shortcuts, setShortcuts] = useState<EditorComponentProps['shortcuts']>([]);const getNextValue = useCallback((value: string) => {const index = config.indexOf(value);return index === config.length - 1 ? config[0] : config[index + 1];}, [config]);const getPrevValue = useCallback((value: string) => {const index = config.indexOf(value);return index === 0 ? config[config.length - 1] : config[index - 1];}, [config]);useEffect(() => {setShortcuts([{keys: [['ArrowRight'], ['Tab']],callback: ({ value, setValue }, _event) => {setValue(getNextValue(value));return false; // Prevent default Tab behavior}},{keys: [['ArrowLeft']],callback: ({ value, setValue }, _event) => {setValue(getPrevValue(value));}}]);}, [config, getNextValue, getPrevValue]);return (<EditorComponent<string> shortcuts={shortcuts}>{({ value, setValue, finishEditing }) => (// ... editor UI)}</EditorComponent>);};What’s happening:
- ArrowRight/Tab: Move to next option (wraps to first if at end)
- ArrowLeft: Move to previous option (wraps to last if at start)
callbackreceives{ value, setValue, finishEditing }as first parameter- Return
falseto prevent default behavior (e.g., Tab moving to next cell)
Keyboard navigation benefits:
- Fast selection without mouse
- Accessible for keyboard-only users
- Intuitive left/right navigation
- Tab cycles through options instead of moving cells
Complete Editor Component
Put it all together:
type EditorComponentProps = ComponentProps<typeof EditorComponent<string>>;const FeedbackEditor = () => {const [config, setConfig] = useState<string[]>(['👍', '👎', '🤷']);const [shortcuts, setShortcuts] = useState<EditorComponentProps['shortcuts']>([]);const onPrepare: EditorComponentProps['onPrepare'] = (_row,_column,_prop,_TD,_originalValue,cellProperties) => {if (cellProperties.config) {setConfig(cellProperties.config as string[]);}};const getNextValue = useCallback((value: string) => {const index = config.indexOf(value);return index === config.length - 1 ? config[0] : config[index + 1];}, [config]);const getPrevValue = useCallback((value: string) => {const index = config.indexOf(value);return index === 0 ? config[config.length - 1] : config[index - 1];}, [config]);useEffect(() => {setShortcuts([{keys: [['ArrowRight'], ['Tab']],callback: ({ value, setValue }, _event) => {setValue(getNextValue(value));return false;}},{keys: [['ArrowLeft']],callback: ({ value, setValue }, _event) => {setValue(getPrevValue(value));}}]);}, [config, getNextValue, getPrevValue]);return (<EditorComponent<string> onPrepare={onPrepare} shortcuts={shortcuts}>{({ value, setValue, finishEditing }) => (<><style>{`.editor {box-sizing: border-box;display: flex;gap: 3px;padding: 3px;background: rgb(238, 238, 238);border: 1px solid rgb(204, 204, 204);border-radius: 4px;height: 100%;width: 100%;}.button.active:hover,.button.active {background: #007bff;color: white;}.button:hover {background: #f0f0f0;}.button {background: #fff;color: black;border: none;padding: 0;margin: 0;height: 100%;width: 100%;font-size: 16px;font-weight: bold;text-align: center;cursor: pointer;}`}</style><div className="editor">{config.map((item, _index, _array) => (<buttonkey={item}className={`button ${value === item ? 'active' : ''}`}onClick={() => {setValue(item);finishEditing();}}style={{width: `${100 / _array.length}%`}}>{item}</button>))}</div></>)}</EditorComponent>);};What’s happening:
- State management:
configandshortcutsmanaged with React hooks - onPrepare: Reads column-specific config
- shortcuts: Keyboard navigation handlers
- Render prop: Renders buttons based on config
- Styling: CSS-in-JS for editor appearance
- State management:
Use in Handsontable
Use the editor component in your
HotTable:const ExampleComponent = () => {return (<HotTableautoRowSize={true}rowHeaders={true}autoWrapRow={true}licenseKey="non-commercial-and-evaluation"height="auto"data={data}colHeaders={true}><HotColumnwidth={250}editor={FeedbackEditor}config={['👍', '👎', '🤷']}data="feedback"title="Feedback"/><HotColumnwidth={250}editor={FeedbackEditor}config={['1', '2', '3', '4', '5']}data="stars"title="Rating (1-5)"/></HotTable>);};What’s happening:
editor={FeedbackEditor}- Assigns the editor component to the columnconfig={['👍', '👎', '🤷']}- Column-specific options- Same editor component, different configurations per column
Key features:
- Reusable editor component
- Per-column configuration
- Type-safe with TypeScript
How It Works - Complete Flow
- Initial Render: Cell displays the emoji value (👍, 👎, or 🤷)
- User Double-Clicks or Enter: Editor opens,
onPreparereads column config - Editor Opens:
EditorComponentpositions container over cell - Button Display: All options visible, current value highlighted
- User Interaction:
- Click a button →
setValue(item)andfinishEditing()called - Press ArrowLeft/Right → Shortcut callback updates value
- Press Tab → Cycles through options (prevents default cell navigation)
- Click a button →
- Visual Feedback: Selected button highlighted in blue
- User Confirms: Press Enter, click button, or click away
- Save: Value saved to cell
- Editor Closes: Cell shows selected emoji
Enhancements
Custom Renderer with Styling
Add a custom renderer to style the emoji display:
import { rendererFactory } from 'handsontable/renderers';const cellDefinition = {renderer: rendererFactory(({ td, value }) => {td.innerHTML = `<div style="text-align: center; font-size: 1.5em; padding: 4px;">${value || '🤷'}</div>`;})};// Use in HotColumn<HotColumneditor={FeedbackEditor}renderer={cellDefinition.renderer}config={['👍', '👎', '🤷']}data="feedback"/>What’s happening:
- Center-aligns the emoji
- Increases font size for better visibility
- Adds padding for spacing
More Feedback Options
Add more emoji options:
<HotColumneditor={FeedbackEditor}config={['👍', '👎', '🤷', '❤️', '🔥', '⭐']}data="feedback"/>The editor automatically adjusts button widths based on config length.
Custom Button Styling
Enhanced button appearance with CSS:
<style>{`.button {padding: 8px;border: 2px solid #ddd;background: white;color: #333;border-radius: 4px;cursor: pointer;font-size: 1.2em;transition: all 0.2s;}.button.active {border-color: #007bff;background: #007bff;color: white;}.button:hover {transform: scale(1.05);box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);}`}</style>Dynamic Config from Cell Properties
The
onPreparehook already handles this! Just pass different configs:<HotColumneditor={FeedbackEditor}config={['👍', '👎', '❤️', '🔥']}data="feedback"/>Tooltip on Hover
Add tooltips to buttons:
{config.map((item) => {const tooltips: Record<string, string> = {'👍': 'Positive feedback','👎': 'Negative feedback','🤷': 'Neutral feedback'};return (<buttonkey={item}className={`button ${value === item ? 'active' : ''}`}onClick={() => {setValue(item);finishEditing();}}title={tooltips[item] || ''}>{item}</button>);})}Text Labels Instead of Emojis
Use text buttons for clarity:
<HotColumneditor={FeedbackEditor}config={['Positive', 'Negative', 'Neutral']}data="feedback"/>The editor works with any string values, not just emojis.
Using External CSS File
Move styles to a separate CSS file:
feedback-editor.css .editor {box-sizing: border-box;display: flex;gap: 3px;padding: 3px;background: rgb(238, 238, 238);border: 1px solid rgb(204, 204, 204);border-radius: 4px;height: 100%;width: 100%;}.button.active {background: #007bff;color: white;}.button:hover {background: #f0f0f0;}.button {background: #fff;color: black;border: none;padding: 0;margin: 0;height: 100%;width: 100%;font-size: 16px;font-weight: bold;text-align: center;cursor: pointer;}import './feedback-editor.css';const FeedbackEditor = () => {// ... component code without <style> tag};
Accessibility
React buttons are inherently accessible, but you can enhance them:
{config.map((item, index) => ( <button key={item} className={`button ${value === item ? 'active' : ''}`} onClick={() => { setValue(item); finishEditing(); }} aria-label={`${item} feedback option`} aria-pressed={value === item} tabIndex={value === item ? 0 : -1} > {item} </button>))}Keyboard navigation:
- Tab: Navigate to editor (focuses active button)
- Arrow Left/Right: Cycle through options (via shortcuts)
- Enter: Select current option and finish editing
- Escape: Cancel editing
- Click: Direct selection
ARIA attributes:
aria-label: Describes each buttonaria-pressed: Indicates selected statetabIndex: Controls keyboard focus order
Performance Considerations
Why This Is Fast
- React Virtual DOM: Efficient updates only when value changes
- No External Libraries: Zero overhead beyond React
- Efficient Re-renders: Only re-renders when config or value changes
- Native Events: Browser-optimized click handlers
React Hooks Optimization
The useCallback and useEffect hooks ensure shortcuts are only recreated when config changes:
const getNextValue = useCallback((value: string) => { const index = config.indexOf(value); return index === config.length - 1 ? config[0] : config[index + 1];}, [config]); // Only recreate if config changes
useEffect(() => { setShortcuts([...]);}, [config, getNextValue, getPrevValue]); // Only update when dependencies changeTypeScript Support
EditorComponent is fully typed. You can specify the value type:
<EditorComponent<string>> {({ value, setValue, finishEditing }) => { // TypeScript knows value is string | undefined // TypeScript knows setValue accepts string return ( // ... editor UI ); }}</EditorComponent>For number-based feedback:
<EditorComponent<number>> {({ value, setValue, finishEditing }) => { // TypeScript knows value is number | undefined return ( // ... editor UI ); }}</EditorComponent>Best Practices
- Use
onPreparefor per-cell configuration - AccesscellPropertiesto read custom options - Handle keyboard events properly - Use shortcuts for navigation
- Call
finishEditing()appropriately - When user confirms changes (Enter, blur, button click) - Keep render prop function simple - Extract complex logic into separate components or hooks
- Use
useCallbackfor helper functions - Prevents unnecessary re-renders - Update shortcuts in
useEffect- Ensures shortcuts match current config
Congratulations! You’ve created a simple feedback editor with emoji buttons using React’s EditorComponent, perfect for quick feedback selection in your data grid!