Star Rating
This tutorial shows you how to build a star rating cell in React using Handsontable’s EditorComponent, a custom renderer, and a local React component for the stars.
import { HotTable, HotColumn, EditorComponent } from '@handsontable/react-wrapper';import { registerAllModules } from 'handsontable/registry';import './example1.css';
registerAllModules();
function StarRating({ name, value, editing = true, onStarHover, onStarClick }) { return ( <div className="star-rating" aria-label={`Rating: ${value} out of 5`}> {[1, 2, 3, 4, 5].map((star) => ( <span key={`${name}-${star}`} className={star <= value ? 'star filled' : 'star'} onMouseEnter={editing ? () => onStarHover?.(star) : undefined} onClick={editing ? () => onStarClick?.(star) : undefined} > ★ </span> ))} </div> );}
/* start:skip-in-preview */export const data = [ { product: 'Dashboard Pro', category: 'Analytics', rating: 5, reviews: 342, price: 49 }, { product: 'Form Builder', category: 'Tools', rating: 4, reviews: 218, price: 29 }, { product: 'Chart Engine', category: 'Analytics', rating: 3, reviews: 156, price: 39 }, { product: 'Auth Module', category: 'Security', rating: 5, reviews: 89, price: 19 }, { product: 'File Manager', category: 'Storage', rating: 2, reviews: 64, price: 15 }, { product: 'Email Service', category: 'Communication', rating: 4, reviews: 275, price: 25 }, { product: 'Search Index', category: 'Tools', rating: 1, reviews: 31, price: 35 }, { product: 'Cache Layer', category: 'Infra', rating: 4, reviews: 112, price: 20 },];/* end:skip-in-preview */
const RatingCellRenderer = ({ value }) => ( <div className="rating-cell"> <StarRating name="rating-cell" value={Number(value) || 0} editing={false} /> </div>);
const ratingValidator = (value, callback) => { value = parseInt(value); callback(value >= 0 && value <= 100);};
export const RatingEditor = () => { return ( <EditorComponent> {({ value, setValue, finishEditing }) => ( <div className="rating-editor"> <StarRating name="rating" value={Number(value) || 0} onStarHover={(nextValue) => setValue(nextValue)} onStarClick={(nextValue) => { setValue(nextValue); finishEditing(); }} /> </div> )} </EditorComponent> );};
const ExampleComponent = () => { return ( <HotTable data={data} colHeaders={['Product', 'Category', 'Rating', 'Reviews', 'Price']} autoRowSize={true} rowHeaders={true} height="auto" width="100%" autoWrapRow={true} headerClassName="htLeft" licenseKey="non-commercial-and-evaluation" > <HotColumn data="product" type="text" width={200} /> <HotColumn data="category" type="text" width={120} /> <HotColumn data="rating" width={150} editor={RatingEditor} renderer={RatingCellRenderer} validator={ratingValidator} /> <HotColumn data="reviews" type="numeric" width={80} /> <HotColumn data="price" type="numeric" width={80} /> </HotTable> );};
export default ExampleComponent;import { HotTable, HotColumn, EditorComponent } from '@handsontable/react-wrapper';import { registerAllModules } from 'handsontable/registry';import './example1.css';
registerAllModules();
interface StarRatingProps { name: string; value: number; editing?: boolean; onStarHover?: (value: number) => void; onStarClick?: (value: number) => void;}
function StarRating({ name, value, editing = true, onStarHover, onStarClick }: StarRatingProps) { return ( <div className="star-rating" aria-label={`Rating: ${value} out of 5`}> {[1, 2, 3, 4, 5].map((star) => ( <span key={`${name}-${star}`} className={star <= value ? 'star filled' : 'star'} onMouseEnter={editing ? () => onStarHover?.(star) : undefined} onClick={editing ? () => onStarClick?.(star) : undefined} > ★ </span> ))} </div> );}
/* start:skip-in-preview */
export const data = [ { product: "Dashboard Pro", category: "Analytics", rating: 5, reviews: 342, price: 49 }, { product: "Form Builder", category: "Tools", rating: 4, reviews: 218, price: 29 }, { product: "Chart Engine", category: "Analytics", rating: 3, reviews: 156, price: 39 }, { product: "Auth Module", category: "Security", rating: 5, reviews: 89, price: 19 }, { product: "File Manager", category: "Storage", rating: 2, reviews: 64, price: 15 }, { product: "Email Service", category: "Communication", rating: 4, reviews: 275, price: 25 }, { product: "Search Index", category: "Tools", rating: 1, reviews: 31, price: 35 }, { product: "Cache Layer", category: "Infra", rating: 4, reviews: 112, price: 20 },];
/* end:skip-in-preview */
const RatingCellRenderer = ({ value }: { value: unknown }) => ( <div className="rating-cell"> <StarRating name="rating-cell" value={Number(value) || 0} editing={false} /> </div>);
const ratingValidator = (value: string | number, callback: (valid: boolean) => void) => { const parsed = parseInt(String(value)); callback(parsed >= 0 && parsed <= 100);};
export const RatingEditor = () => { return ( <EditorComponent<number>> {({ value, setValue, finishEditing }) => ( <div className="rating-editor"> <StarRating name="rating" value={Number(value) || 0} onStarHover={(nextValue) => setValue(nextValue)} onStarClick={(nextValue) => { setValue(nextValue); finishEditing(); }} /> </div> )} </EditorComponent> );};
const ExampleComponent = () => { return ( <HotTable data={data} colHeaders={["Product", "Category", "Rating", "Reviews", "Price"]} autoRowSize={true} rowHeaders={true} height="auto" width="100%" autoWrapRow={true} headerClassName="htLeft" licenseKey="non-commercial-and-evaluation" > <HotColumn data="product" type="text" width={240} /> <HotColumn data="category" type="text" width={120} /> <HotColumn data="rating" width={150} editor={RatingEditor} renderer={RatingCellRenderer} validator={ratingValidator} /> <HotColumn data="reviews" type="numeric" width={80} /> <HotColumn data="price" type="numeric" width={80} /> </HotTable> );};
export default ExampleComponent;.star-rating { display: inline-flex; gap: 1px;}
.star-rating .star { font-size: 18px; color: #d3d3d3; line-height: 1; cursor: default; user-select: none; transition: color 0.1s;}
.rating-editor .star-rating .star { cursor: pointer;}
.star-rating .star.filled { color: #ffb400;}
.rating-cell { display: flex; align-items: center; margin: 3px 0 0 -1px;}
.rating-editor { display: flex; align-items: center; height: 100%; box-sizing: border-box !important; border: none; border-radius: 0; 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); background-color: var(--ht-cell-editor-background-color, #ffffff); padding: var(--ht-cell-vertical-padding, 4px) var(--ht-cell-horizontal-padding, 8px); font-family: var(--ht-font-family, inherit); font-size: var(--ht-font-size, 14px); line-height: var(--ht-line-height, 1.5);}Overview
This guide shows how to create a star rating editor cell using React’s EditorComponent. Use this pattern for product reviews, feedback forms, or any scenario where users select a numeric rating, such as 1-5 stars.
Difficulty: Beginner Time: ~15 minutes Libraries: No external rating library.
What You’ll Build
A cell that:
- Displays interactive star rating when editing
- Shows stars in view mode via a custom React renderer
- Supports hover preview before selection
- Stores values as numbers (1-5)
- Validates rating range, such as 0-100
- Provides click-to-select functionality
- Works with React’s component-based architecture
Prerequisites
npm install handsontable @handsontable/react-wrapperWhat you need:
- React 16.8+ (hooks support)
@handsontable/react-wrapperpackage- Basic React knowledge (hooks, JSX)
Import Dependencies
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 componentsregisterAllModules()- Registers Handsontable modules (required when using the wrapper)
Create the Star Rating Component
Create a local component that renders five stars and reports hover and click events.
interface StarRatingProps {name: string;value: number;editing?: boolean;onStarHover?: (value: number) => void;onStarClick?: (value: number) => void;}function StarRating({ name, value, editing = true, onStarHover, onStarClick }: StarRatingProps) {return (<div className="star-rating" aria-label={`Rating: ${value} out of 5`}>{[1, 2, 3, 4, 5].map((star) => (<spankey={`${name}-${star}`}className={star <= value ? 'star filled' : 'star'}onMouseEnter={editing ? () => onStarHover?.(star) : undefined}onClick={editing ? () => onStarClick?.(star) : undefined}>★</span>))}</div>);}What’s happening:
- The
nameprop creates stable keys for each star. - The
editingprop disables hover and click handlers in view mode. onStarHoverpreviews the value before the user commits it.onStarClickcommits the selected value.
- The
Create the Editor Component
Create a React component that uses
EditorComponentwith the render prop pattern.export const RatingEditor = () => {return (<EditorComponent<number>>{({ value, setValue, finishEditing }) => (<div className="rating-editor"><StarRatingname="rating"value={Number(value) || 0}onStarHover={(nextValue) => setValue(nextValue)}onStarClick={(nextValue) => {setValue(nextValue);finishEditing();}}/></div>)}</EditorComponent>);};What’s happening:
EditorComponentwraps your editor UI; thechildrenprop is a function that receives editor state.value- Current cell value (numeric rating)setValue- Function to update the valuefinishEditing- Function to save and close the editorStarRating- Renders the local star pickeronStarHover- Updates preview as the user hovers over starsonStarClick- Saves the selected rating and closes the editor- The
rating-editordiv is inside the render prop so styling applies to the visible editor area.
Key concepts:
- Render prop pattern:
EditorComponentuses a function as children - Hover preview:
onStarHoverlets users preview before committing - Click to confirm:
onStarClicksaves and closes the editor
Add a Custom Renderer for View Mode
Use a React component as the cell renderer so stars are shown when not editing.
const RatingCellRenderer = ({ value }: { value: unknown }) => (<div className="rating-cell"><StarRatingname="rating-cell"value={Number(value) || 0}editing={false}/></div>);What’s happening:
- The renderer receives
valueand displays it withStarRating editing={false}keeps the stars non-interactive in view mode- Use a unique
name(for example,"rating-cell") to keep stable React keys for the renderer
- The renderer receives
Add a Validator (Optional)
Validate that the rating is within an allowed range, such as 0-100:
const ratingValidator = (value: string | number, callback: (valid: boolean) => void) => {const parsed = parseInt(String(value));callback(parsed >= 0 && parsed <= 100);};For a strict 1-5 star scale, use
parsed >= 1 && parsed <= 5instead.Add Styling
Style the cell and editor so the star rating fits and matches the grid.
.star-rating {display: inline-flex;gap: 1px;}.star-rating .star {font-size: 18px;color: #d3d3d3;line-height: 1;cursor: default;user-select: none;transition: color 0.1s;}.rating-editor .star-rating .star {cursor: pointer;}.star-rating .star.filled {color: #ffb400;}.rating-cell {display: flex;align-items: center;margin: 3px 0 0 -1px;}.rating-editor {display: flex;align-items: center;height: 100%;box-sizing: border-box !important;border: none;border-radius: 0;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) 0var(--ht-cell-editor-shadow-color, transparent);background-color: var(--ht-cell-editor-background-color, #ffffff);padding: var(--ht-cell-vertical-padding, 4px)var(--ht-cell-horizontal-padding, 8px);font-family: var(--ht-font-family, inherit);font-size: var(--ht-font-size, 14px);line-height: var(--ht-line-height, 1.5);}What’s happening:
.star-ratinglays out the local star component.star-rating .star.filledcolors the selected stars.rating-cellaligns the stars in the cell when not editing.rating-editoruses Handsontable CSS variables for focus border, background, and padding so the editor matches the grid theme
Prepare Sample Data
Use data with a
ratingproperty (and any other columns you need). Example for a product table:export const data = [{ product: "Dashboard Pro", category: "Analytics", rating: 5, reviews: 342, price: 49 },{ product: "Form Builder", category: "Tools", rating: 4, reviews: 218, price: 29 },{ product: "Chart Engine", category: "Analytics", rating: 3, reviews: 156, price: 39 },{ product: "Auth Module", category: "Security", rating: 5, reviews: 89, price: 19 },{ product: "File Manager", category: "Storage", rating: 2, reviews: 64, price: 15 },{ product: "Email Service", category: "Communication", rating: 4, reviews: 275, price: 25 },{ product: "Search Index", category: "Tools", rating: 1, reviews: 31, price: 35 },{ product: "Cache Layer", category: "Infra", rating: 4, reviews: 112, price: 20 },];What’s happening:
- Each row has
product,category,rating,reviews, andprice - The
ratingcolumn uses the star editor and renderer; other columns can be text or numeric
- Each row has
Use in Handsontable
Wire the editor, renderer, and validator to the rating column:
const ExampleComponent = () => {return (<HotTabledata={data}colHeaders={["Product", "Category", "Rating", "Reviews", "Price"]}autoRowSize={true}rowHeaders={true}height="auto"width="100%"autoWrapRow={true}headerClassName="htLeft"licenseKey="non-commercial-and-evaluation"><HotColumn data="product" type="text" width={240} /><HotColumn data="category" type="text" width={120} /><HotColumndata="rating"width={150}editor={RatingEditor}renderer={RatingCellRenderer}validator={ratingValidator}/><HotColumn data="reviews" type="numeric" width={80} /><HotColumn data="price" type="numeric" width={80} /></HotTable>);};What’s happening:
editor={RatingEditor}- Star rating editor when the cell is activerenderer={RatingCellRenderer}- Shows stars in view modevalidator={ratingValidator}- Ensures rating is within the allowed range, such as 0-100data="rating"- Binds to theratingproperty in each row
Key features:
- Stars in both view and edit mode
- Values stored as numbers (1-5)
- Validation and type-safe setup with TypeScript
How It Works - Complete Flow
- Initial Render:
RatingCellRendererdisplays stars for the current rating, such as 3 filled stars. - User Double-Clicks or Enter: Editor opens.
- Editor Opens:
EditorComponentshows the star picker in the cell. - Star Rating Display:
StarRatingshows the current value; empty stars show the remaining scale. - User Interaction:
- Hover over stars →
onStarHoverupdates preview viasetValue - Click a star →
onStarClicksaves value and callsfinishEditing()
- Hover over stars →
- Validation:
ratingValidatorruns, such as checking that the value is 0-100. - Save: Numeric value is saved to the cell.
- Editor Closes:
RatingCellRenderershows the updated stars in view mode.
Enhancements
Custom Star Count
Change the number of stars, such as a 10-point scale:
interface StarRatingProps {name: string;value: number;starCount?: number;editing?: boolean;onStarHover?: (value: number) => void;onStarClick?: (value: number) => void;}function StarRating({ name, value, starCount = 5, editing = true, onStarHover, onStarClick }: StarRatingProps) {const stars = Array.from({ length: starCount }, (_, index) => index + 1);return (<div className="star-rating" aria-label={`Rating: ${value} out of ${starCount}`}>{stars.map((star) => (<spankey={`${name}-${star}`}className={star <= value ? 'star filled' : 'star'}onMouseEnter={editing ? () => onStarHover?.(star) : undefined}onClick={editing ? () => onStarClick?.(star) : undefined}>★</span>))}</div>);}Custom Star Colors
Customize the appearance:
.star-rating .star {color: #e0e0e0;}.star-rating .star.filled {color: #ffd700;}Alternative: HTML-Based Renderer
The main example uses a React component (
RatingCellRenderer) for view mode. If you prefer a non-React renderer, you can userendererFactory:import { rendererFactory } from 'handsontable/renderers';const starRenderer = rendererFactory(({ td, value }) => {const rating = Number(value) || 0;const stars = '★'.repeat(rating) + '☆'.repeat(5 - rating);td.innerHTML = `<div style="font-size: 1.2em;color: #ffb400;letter-spacing: 2px;">${stars}</div>`;});// Use in HotColumn<HotColumndata="rating"width={150}editor={RatingEditor}renderer={starRenderer}/>Read Config from Cell Properties (Advanced)
Use
onPreparefor per-column configuration, such as star count:const RatingEditor = () => {const [starCount, setStarCount] = useState(5);const onPrepare = (_row, _column, _prop, _TD, _originalValue, cellProperties) => {if (cellProperties.starCount != null) {setStarCount(cellProperties.starCount);}};return (<div className="rating-editor"><EditorComponent<number> onPrepare={onPrepare}>{({ value, setValue, finishEditing }) => (<StarRatingname="rating"starCount={starCount}value={Number(value) || 0}onStarHover={(nextValue) => setValue(nextValue)}onStarClick={(nextValue) => {setValue(nextValue);finishEditing();}}/>)}</EditorComponent></div>);};// Use with different star counts per column<HotColumn editor={RatingEditor} starCount={5} data="rating" title="Rating (1-5)" /><HotColumn editor={RatingEditor} starCount={10} data="score" title="Score (1-10)" />Handle Empty Values
Ensure the component handles undefined or null:
value={Number(value) || 0}This displays empty stars when the cell has no value.
Accessibility
The local StarRating component sets aria-label on the rating container. To add keyboard support, render each star as a button:
<button type="button" className={star <= value ? 'star filled' : 'star'} onMouseEnter={editing ? () => onStarHover?.(star) : undefined} onClick={editing ? () => onStarClick?.(star) : undefined} aria-label={`Set rating to ${star}`}> ★</button>Keyboard navigation:
- Tab: Navigate to editor
- Arrow keys: Navigate between stars after you add key handlers
- Enter/Space: Select star
- Escape: Cancel editing
Performance Considerations
Why This Is Fast
- No external rating package: The star UI uses a small local component.
- React Virtual DOM: Updates happen only when the value changes.
- Focused callbacks:
onStarHoverandonStarClickeach do one thing. - No unnecessary re-renders: The editor unmounts when closed.
TypeScript Support
EditorComponent is fully typed. Specify the value type for numeric ratings:
<EditorComponent<number>> {({ value, setValue, finishEditing }) => { // TypeScript knows value is number | undefined // TypeScript knows setValue accepts number return ( <StarRating name="rating" value={Number(value) || 0} onStarHover={(nextValue) => setValue(nextValue)} onStarClick={(nextValue) => { setValue(nextValue); finishEditing(); }} /> ); }}</EditorComponent>Best Practices
- Coerce value to number - Use
Number(value) || 0since cell values may be strings. - Provide
nameprop - Use different names for editor and renderer, such as"rating"and"rating-cell", to keep stable React keys. - Call
finishEditing()on click - Star click confirms the selection and closes the editor. - Use
onStarHoverfor preview - Improves UX by showing the selection before commit. - Use a custom renderer -
RatingCellRendererwithediting={false}shows stars in view mode and keeps the UI consistent. - Add a validator - Use
ratingValidatorto restrict values, such as 0-100 or 1-5, and give immediate feedback.
What you learned
You created a local StarRating component and integrated it as a Handsontable cell editor in React. You used EditorComponent with the render prop pattern to manage hover preview and click-to-confirm selection, and a React component renderer to display stars in view mode.
Next steps
- Star Rating (JavaScript) - The same concept using
editorFactoryand SVG stars with no external library. - Star Rating Editor (Angular) - The Angular version using
HotCellEditorAdvancedComponent. - Colorful Picker (React) - Another React
EditorComponentexample for color selection.