Skip to content
JavaScript
import Handsontable from 'handsontable/base';
import { registerAllModules } from 'handsontable/registry';
import { editorFactory } from 'handsontable/editors';
import { rendererFactory } from 'handsontable/renderers';
// Register all Handsontable's modules.
registerAllModules();
/* start:skip-in-preview */
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 */
// Get the DOM element with the ID 'example1' where the Handsontable will be rendered
const container = document.querySelector('#example1');
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({
shortcuts: [
{
keys: [['1'], ['2'], ['3'], ['4'], ['5']],
callback: (editor, _event) => {
editor.setValue(_event.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');
editor.input.classList.add('rating-editor');
},
afterInit(editor) {
editor.input.addEventListener('mouseover', (event) => {
const star = event.target.closest('.rating-star');
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('');
},
}),
};
// Define configuration options for the Handsontable
const hotOptions = {
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',
};
// Initialize the Handsontable instance with the specified configuration options
// eslint-disable-next-line no-unused-vars
const hot = new Handsontable(container, hotOptions);
TypeScript
import Handsontable from "handsontable/base";
import { registerAllModules } from "handsontable/registry";
import { CellProperties } from "handsontable/settings";
import { editorFactory } from "handsontable/editors";
import { rendererFactory } from "handsontable/renderers";
// Register all Handsontable's modules.
registerAllModules();
/* start:skip-in-preview */
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 */
// Get the DOM element with the ID 'example1' where the Handsontable will be rendered
const container = document.querySelector("#example1")!;
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: Pick<
CellProperties,
"renderer" | "validator" | "editor"
> = {
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("");
},
}),
};
// Define configuration options for the Handsontable
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",
};
// Initialize the Handsontable instance with the specified configuration options
// eslint-disable-next-line no-unused-vars
const hot = new Handsontable(container, hotOptions);
CSS
.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);
}

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

Prerequisites

None! This uses only native HTML, SVG and JavaScript features.

  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.
  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 size
    • viewBox="0 0 24 24" - Standard 24x24 coordinate space
    • fill="currentColor" - Inherits the CSS color property, 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
  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-cell wraps the stars in the renderer with display: flex to match the editor layout
    • margin: 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 color property, which the SVG inherits through fill="currentColor"

    Current star indicator:

    • .rating-editor .rating-star.current highlights 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
  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 the active class (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
  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
  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:

    1. Create a div container for the star buttons
    2. Add the rating-editor CSS class (all styling is in the CSS file)
    3. This container will hold the 5 SVG star elements

    Key styling (from CSS):

    • display: flex; align-items: center - Stars aligned vertically
    • box-shadow with --ht-cell-editor-border-color - Blue border matching native editors
    • padding with cell padding tokens - Matches renderer to prevent visual jump
    • font-family/font-size/line-height: inherit - Consistent sizing with the cell
  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:

    1. User hovers over a star (or its SVG child element)
    2. 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
    3. Get the hover rating from the span’s dataset.value
    4. If different from current value, update it
    5. This creates a “preview” effect as user hovers

    Mousedown Event:

    1. User clicks (mousedown) anywhere in the editor
    2. Finish editing immediately
    3. 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 the data-value attribute
    • This ensures reliable star detection regardless of which SVG child element the user interacts with
  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:

    1. Create 5 star spans (indices 0-4, values 1-5)
    2. Each span has data-value attribute with rating (1-5)
    3. Stars use the rating-star class for base styling (gray color)
    4. Active stars get the active class (gold color)
    5. The star matching the current value gets the current class (accent color) — this highlights the selected rating in the editor
    6. Each span contains the inline SVG star
    7. Join all spans into a single HTML string

    Dynamic rendering:

    • Updates whenever editor.setValue() is called
    • Automatically called by editorFactory when value changes
    • Provides live preview as user interacts
  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
  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-cell flex container with CSS class-based coloring (gold/gray)
    • validator: Ensures rating is within valid range
    • editor: Uses editorFactory helper with:
      • Keyboard shortcuts for 1-5 keys and arrow keys
      • Container initialization with rating-editor CSS class
      • Mouse events using closest() for reliable SVG hover detection
      • Render function with current class to highlight the selected star using accent color
  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 column
    • headerClassName: 'htLeft' - Left-aligns all column headers
    • width: '100%' - Table fills the container width

How It Works - Complete Flow

  1. Initial Render: Cell displays 5 SVG stars — gold for filled, gray for unfilled
  2. User Double-Clicks or Enter: Editor opens over cell showing interactive stars with Handsontable blue border
  3. Current Star Indicator: The last active star turns blue (accent color) to clearly show the selected rating
  4. Mouse Hover: User hovers over stars → preview rating updates in real-time (detected via closest())
  5. Click Selection: User clicks → rating selected and editor closes
  6. Keyboard Input: User presses 1-5 keys → rating set directly
  7. Arrow Navigation: User presses ArrowLeft/Right → rating increments/decrements
  8. Validation: Validator checks the value is valid
  9. Save: Valid value saved to cell
  10. 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!