JavaScript Data GridStar Rating Cell Type - Step-by-Step Guide
- Overview
- What You'll Build
- Complete Example
- Prerequisites
- Step 1: Import Dependencies
- Step 2: Define the Star SVG
- Step 3: Add CSS Styling
- Step 4: Create the Renderer
- Step 5: Create the Validator
- Step 6: Editor - Initialize (init)
- Step 7: Editor - After Init Hook (afterInit)
- Step 8: Editor - Render Function (render)
- Step 9: Editor - Keyboard Shortcuts
- Step 10: Complete Cell Definition
- Step 11: Use in Handsontable
- How It Works - Complete Flow
- Enhancements
- Accessibility
Overview
This guide shows how to create an interactive star rating cell using inline SVG stars. Perfect for product ratings, review scores, or any scenario where users need to provide a 1-5 star rating.
Difficulty: Beginner Time: ~15 minutes Libraries: None (pure HTML, SVG and JavaScript)
What You'll Build
A cell that:
- Displays 5 SVG stars both when editing and viewing
- Shows filled stars (gold) and unfilled stars (gray)
- Uses Handsontable CSS tokens for theme-aware editor styling
- Supports mouse hover for preview
- Allows keyboard input (1-5 keys, arrow keys)
- Provides immediate visual feedback
- Works without any external libraries
Complete Example
Prerequisites
None! This uses only native HTML, SVG and JavaScript features.
Step 1: Import Dependencies
import Handsontable from 'handsontable/base';
import { registerAllModules } from 'handsontable/registry';
import { editorFactory } from 'handsontable/editors';
import { rendererFactory } from 'handsontable/renderers';
registerAllModules();
What we're NOT importing:
- No icon libraries
- No UI component libraries
- No external SVG sprite sheets
- Just Handsontable.
Step 2: Define the Star SVG
Create an inline SVG string for the star shape. Using fill="currentColor" allows CSS to control the star color.
const starSvg =
'<svg width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>';
What's happening:
width="1em" height="1em"- Stars scale with the font sizeviewBox="0 0 24 24"- Standard 24x24 coordinate spacefill="currentColor"- Inherits the CSScolorproperty, so active/inactive states are controlled via CSS- The
<path>draws a classic 5-pointed star shape
Why SVG instead of emoji?
- Consistent rendering across all browsers and operating systems
- Full control over color, size, and styling via CSS
- No platform-dependent emoji variations
- Crisp at any resolution
Step 3: Add CSS Styling
Create a separate CSS file for the rating styles. This uses Handsontable CSS custom properties (tokens) so the editor automatically adapts to custom themes and dark mode.
.rating-cell {
display: flex;
align-items: center;
margin: 3px 0 0 -1px;
}
.rating-star {
color: var(--ht-background-secondary-color, #e0e0e0);
cursor: default;
display: inline-flex;
align-items: center;
}
.rating-star.active {
color: #facc15;
}
.rating-editor {
display: flex;
align-items: center;
width: 100%;
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: inherit;
font-size: inherit;
line-height: inherit;
cursor: pointer;
}
.rating-editor .rating-star {
cursor: pointer;
}
.rating-editor .rating-star.current {
color: var(--ht-accent-color, #1a42e8);
}
Cell container:
.rating-cellwraps the stars in the renderer withdisplay: flexto match the editor layoutmargin: 3px 0 0 -1px- Fine-tunes alignment between renderer and editor to prevent visual jump
Star colors:
var(--ht-background-secondary-color, #e0e0e0)- Inactive/unfilled stars (adapts to theme)#facc15(gold) - Active/filled stars- Colors are applied via the CSS
colorproperty, which the SVG inherits throughfill="currentColor"
Current star indicator:
.rating-editor .rating-star.currenthighlights the last active star (the one matching the rating value) using--ht-accent-color- Makes it clear which star is selected while editing — the current star turns blue (accent color) instead of gold
Handsontable tokens used:
--ht-background-secondary-color- Inactive star color (adapts to theme)--ht-accent-color- Current star highlight in editor (blue)--ht-cell-editor-border-color/--ht-cell-editor-border-width- blue border matching native editors--ht-cell-editor-background-color- editor background--ht-cell-editor-shadow-blur-radius/--ht-cell-editor-shadow-color- editor shadow--ht-cell-vertical-padding/--ht-cell-horizontal-padding- consistent cell padding matching the renderer
Step 4: Create the Renderer
The renderer displays 5 SVG stars wrapped in a flex container using CSS classes for color control.
renderer: rendererFactory(({ td, value }) => {
td.innerHTML = `<div class="rating-cell">${Array.from(
{ length: 5 },
(_, index) =>
`<span class="rating-star ${index < value ? 'active' : ''}">${starSvg}</span>`
).join('')}</div>`;
})
What's happening:
- Stars are wrapped in a
<div class="rating-cell">flex container to match the editor's flex layout Array.from({ length: 5 })- Creates an array with 5 elements (indices 0-4)index < value- Stars up to the rating value get theactiveclass (gold color)index >= value- Stars beyond the rating stay gray via CSS- Each span contains the inline SVG star
join('')- Concatenates all star spans into a single string
Step 5: Create the Validator
Ensure values are within the valid range.
validator: (value, callback) => {
value = parseInt(value);
callback(value >= 0 && value <= 100);
}
What's happening:
- Convert value to integer (keyboard input returns strings)
- Validate the value is within the acceptable range
- Call
callback(true)for valid,callback(false)for invalid
Step 6: Editor - Initialize (init)
Create the container div for the star rating editor.
init(editor) {
editor.input = editor.hot.rootDocument.createElement('DIV') as HTMLDivElement;
editor.input.classList.add('rating-editor');
}
What's happening:
- Create a
divcontainer for the star buttons - Add the
rating-editorCSS class (all styling is in the CSS file) - This container will hold the 5 SVG star elements
Key styling (from CSS):
display: flex; align-items: center- Stars aligned verticallybox-shadowwith--ht-cell-editor-border-color- Blue border matching native editorspaddingwith cell padding tokens - Matches renderer to prevent visual jumpfont-family/font-size/line-height: inherit- Consistent sizing with the cell
Step 7: Editor - After Init Hook (afterInit)
Set up mouse events for hover preview and click selection.
afterInit(editor) {
editor.input.addEventListener('mouseover', (event) => {
const star = (event.target as HTMLElement).closest('.rating-star') as HTMLElement | null;
if (
star?.dataset.value &&
parseInt(editor.value) !== parseInt(star.dataset.value)
) {
editor.setValue(star.dataset.value);
}
});
editor.input.addEventListener('mousedown', () => {
editor.finishEditing();
});
}
What's happening:
Mouseover Event:
- User hovers over a star (or its SVG child element)
- Use
closest('.rating-star')to find the parent span — this is important because the hover target may be the SVG<path>element inside the span - Get the hover rating from the span's
dataset.value - If different from current value, update it
- This creates a "preview" effect as user hovers
Mousedown Event:
- User clicks (mousedown) anywhere in the editor
- Finish editing immediately
- Value is saved to the cell
Why closest() instead of checking event.target directly?
- With inline SVGs, the actual hover/click target is often the
<svg>or<path>element, not the parent<span> closest('.rating-star')walks up the DOM tree to find the span with thedata-valueattribute- This ensures reliable star detection regardless of which SVG child element the user interacts with
Step 8: Editor - Render Function (render)
Generate the HTML for the 5 star buttons based on current rating.
render(editor) {
editor.input.innerHTML = Array.from(
{ length: 5 },
(_, index) =>
`<span data-value="${index + 1}" class="rating-star ${
index < editor.value ? 'active' : ''
}${index + 1 === parseInt(editor.value) ? ' current' : ''}">${starSvg}</span>`
).join('');
}
What's happening:
- Create 5 star spans (indices 0-4, values 1-5)
- Each span has
data-valueattribute with rating (1-5) - Stars use the
rating-starclass for base styling (gray color) - Active stars get the
activeclass (gold color) - The star matching the current value gets the
currentclass (accent color) — this highlights the selected rating in the editor - Each span contains the inline SVG star
- Join all spans into a single HTML string
Dynamic rendering:
- Updates whenever
editor.setValue()is called - Automatically called by
editorFactorywhen value changes - Provides live preview as user interacts
Step 9: Editor - Keyboard Shortcuts
Add keyboard support for rating selection.
shortcuts: [
{
keys: [['1'], ['2'], ['3'], ['4'], ['5']],
callback: (editor, _event) => {
editor.setValue((_event as KeyboardEvent).key);
}
},
{
keys: [['ArrowRight']],
callback: (editor, _event) => {
if (parseInt(editor.value) < 5) {
editor.setValue(parseInt(editor.value) + 1);
}
}
},
{
keys: [['ArrowLeft']],
callback: (editor, _event) => {
if (parseInt(editor.value) > 1) {
editor.setValue(parseInt(editor.value) - 1);
}
}
}
]
What's happening:
Number Keys (1-5):
- Press 1-5 to set rating directly
- Fastest way to select a specific rating
- Gets key value from keyboard event
Arrow Keys:
- ArrowRight: Increase rating (max 5)
- ArrowLeft: Decrease rating (min 1)
- Bounded within valid range
- Smooth incremental adjustment
Keyboard navigation benefits:
- Fast selection without mouse
- Accessible for keyboard-only users
- Number keys for direct selection, arrows for adjustment
Step 10: Complete Cell Definition
const starSvg =
'<svg width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>';
const cellDefinition = {
renderer: rendererFactory(({ td, value }) => {
td.innerHTML = `<div class="rating-cell">${Array.from(
{ length: 5 },
(_, index) =>
`<span class="rating-star ${index < value ? 'active' : ''}">${starSvg}</span>`
).join('')}</div>`;
}),
validator: (value, callback) => {
value = parseInt(value);
callback(value >= 0 && value <= 100);
},
editor: editorFactory<{ input: HTMLDivElement }>({
shortcuts: [
{
keys: [['1'], ['2'], ['3'], ['4'], ['5']],
callback: (editor, _event) => {
editor.setValue((_event as KeyboardEvent).key);
},
},
{
keys: [['ArrowRight']],
callback: (editor, _event) => {
if (parseInt(editor.value) < 5) {
editor.setValue(parseInt(editor.value) + 1);
}
},
},
{
keys: [['ArrowLeft']],
callback: (editor, _event) => {
if (parseInt(editor.value) > 1) {
editor.setValue(parseInt(editor.value) - 1);
}
},
},
],
init(editor) {
editor.input = editor.hot.rootDocument.createElement(
'DIV'
) as HTMLDivElement;
editor.input.classList.add('rating-editor');
},
afterInit(editor) {
editor.input.addEventListener('mouseover', (event) => {
const star = (event.target as HTMLElement).closest(
'.rating-star'
) as HTMLElement | null;
if (
star?.dataset.value &&
parseInt(editor.value) !== parseInt(star.dataset.value)
) {
editor.setValue(star.dataset.value);
}
});
editor.input.addEventListener('mousedown', () => {
editor.finishEditing();
});
},
render(editor) {
editor.input.innerHTML = Array.from(
{ length: 5 },
(_, index) =>
`<span data-value="${index + 1}" class="rating-star ${
index < editor.value ? 'active' : ''
}${index + 1 === parseInt(editor.value) ? ' current' : ''}">${starSvg}</span>`
).join('');
},
}),
};
What's happening:
- starSvg: Inline SVG star with
fill="currentColor"for CSS color control - renderer: Displays 5 SVG stars wrapped in a
.rating-cellflex container with CSS class-based coloring (gold/gray) - validator: Ensures rating is within valid range
- editor: Uses
editorFactoryhelper with:- Keyboard shortcuts for 1-5 keys and arrow keys
- Container initialization with
rating-editorCSS class - Mouse events using
closest()for reliable SVG hover detection - Render function with
currentclass to highlight the selected star using accent color
Step 11: Use in Handsontable
const container = document.querySelector('#example1')!;
const hotOptions: Handsontable.GridSettings = {
data,
colHeaders: ['Product', 'Category', 'Rating', 'Reviews', 'Price'],
autoRowSize: true,
rowHeaders: true,
height: 'auto',
width: '100%',
autoWrapRow: true,
headerClassName: 'htLeft',
columns: [
{ data: 'product', type: 'text', width: 240 },
{ data: 'category', type: 'text', width: 120 },
{ data: 'rating', width: 150, ...cellDefinition },
{ data: 'reviews', type: 'numeric', width: 80 },
{ data: 'price', type: 'numeric', width: 80 },
],
licenseKey: 'non-commercial-and-evaluation',
};
const hot = new Handsontable(container, hotOptions);
Key configuration:
...cellDefinition- Spreads the renderer, validator, and editor onto the Rating columnheaderClassName: 'htLeft'- Left-aligns all column headerswidth: '100%'- Table fills the container width
How It Works - Complete Flow
- Initial Render: Cell displays 5 SVG stars — gold for filled, gray for unfilled
- User Double-Clicks or Enter: Editor opens over cell showing interactive stars with Handsontable blue border
- Current Star Indicator: The last active star turns blue (accent color) to clearly show the selected rating
- Mouse Hover: User hovers over stars → preview rating updates in real-time (detected via
closest()) - Click Selection: User clicks → rating selected and editor closes
- Keyboard Input: User presses 1-5 keys → rating set directly
- Arrow Navigation: User presses ArrowLeft/Right → rating increments/decrements
- Validation: Validator checks the value is valid
- Save: Valid value saved to cell
- Editor Closes: Cell shows updated star rating
Enhancements
1. Show Numeric Value
Display the numeric rating alongside stars:
renderer: rendererFactory(({ td, value }) => {
const stars = Array.from({ length: 5 }, (_, index) =>
`<span class="rating-star ${index < value ? 'active' : ''}">${starSvg}</span>`
).join('');
td.innerHTML = `
<div style="display: flex; align-items: center; gap: 8px;">
<span>${stars}</span>
<span style="font-weight: bold; color: #666;">${value}/5</span>
</div>
`;
})
2. Custom Star Count
Configurable number of stars per column:
renderer: rendererFactory(({ td, value, cellProperties }) => {
const maxStars = cellProperties.maxStars || 5;
td.innerHTML = Array.from({ length: maxStars }, (_, index) =>
`<span class="rating-star ${index < value ? 'active' : ''}">${starSvg}</span>`
).join('');
})
// Usage
columns: [{
data: 'rating',
...cellDefinition,
maxStars: 10 // 10-star rating
}]
3. Text Labels
Add text labels like "Excellent", "Good", etc.:
renderer: rendererFactory(({ td, value }) => {
const labels = ['', 'Poor', 'Fair', 'Good', 'Very Good', 'Excellent'];
const label = labels[value] || '';
const stars = Array.from({ length: 5 }, (_, index) =>
`<span class="rating-star ${index < value ? 'active' : ''}">${starSvg}</span>`
).join('');
td.innerHTML = `
<div style="display: flex; align-items: center; gap: 8px;">
<span>${stars}</span>
<span style="font-size: 0.9em; color: #666;">${label}</span>
</div>
`;
})
4. Custom Star Colors
Change colors by overriding CSS for specific columns:
/* Red/green rating */
.custom-rating .rating-star {
color: #e5e7eb;
}
.custom-rating .rating-star.active {
color: #22c55e;
}
Accessibility
Keyboard navigation:
- Number keys (1-5): Direct rating selection
- Arrow Right: Increase rating (max 5)
- Arrow Left: Decrease rating (min 1)
- Enter: Confirm selection and finish editing
- Escape: Cancel editing
Congratulations! You've created a theme-aware SVG star rating editor with hover preview and keyboard support using Handsontable CSS tokens, perfect for intuitive 1-5 star ratings in your data grid!