Docs
Getting started
FreeGrid is a framework-free TypeScript grid with thin React and Vue wrappers. Install the core package, or the wrapper for your framework.
npm i @freegridjs/core
# React
npm i @freegridjs/core @freegridjs/react
# Vue
npm i @freegridjs/core @freegridjs/vueGive the grid a container with a height, import the stylesheet once, and pass columns and rows. Only the rows on screen are rendered, so the data size barely matters.
import { createGrid } from '@freegridjs/core';
import '@freegridjs/core/styles.css';
const grid = createGrid(document.getElementById('grid'), {
columns: [
{ field: 'name', header: 'Name', pinned: 'left' },
{ field: 'country', header: 'Country', filter: 'set' },
{ field: 'joined', header: 'Joined', type: 'date' },
{ field: 'total', header: 'Total', type: 'number', format: '#,##0' },
],
data: rows,
});Use it from a CDN
No build step? Load the bundle from npm through unpkg with a script tag. It exposes a global FreeGrid and includes all seven languages. @0 follows the newest 0.x release; pin an exact version such as @freegridjs/core@0.3.0 in production. The same files are on jsDelivr: replace unpkg.com with cdn.jsdelivr.net/npm.
<link rel="stylesheet" href="https://unpkg.com/@freegridjs/core@0/dist/cdn/freegrid.css">
<script src="https://unpkg.com/@freegridjs/core@0/dist/cdn/freegrid.min.js"></script>
<div id="grid" style="height: 480px"></div>
<script>
const grid = FreeGrid.createGrid(document.getElementById('grid'), {
columns: [{ field: 'name' }, { field: 'total', type: 'number', format: '#,##0' }],
data: [{ name: 'Kim', total: 1200 }, { name: 'Lee', total: 3400 }],
locale: 'ko', // all 7 languages are bundled
});
</script><link rel="stylesheet" href="https://unpkg.com/@freegridjs/core@0/dist/cdn/freegrid.css">
<div id="grid" style="height: 480px"></div>
<script type="module">
import { createGrid } from 'https://unpkg.com/@freegridjs/core@0/dist/cdn/freegrid.esm.js';
createGrid(document.getElementById('grid'), { columns, data });
</script>Column definitions
Columns are plain objects. The column id is id if you set one, otherwise field; sorting, filters and column state all refer to columns by that id.
const columns = [
{ field: 'id', header: 'ID', type: 'number', format: '0', width: 80, pinned: 'left' },
{ field: 'name', header: 'Name', flex: 1, minWidth: 160 },
{ field: 'price', header: 'Price', type: 'number', format: 'currency:KRW' },
{ field: 'qty', header: 'Qty', type: 'number' },
// computed column
{ id: 'amount', header: 'Amount', type: 'number', format: '#,##0', valueGetter: row => row.price * row.qty },
{ field: 'status', header: 'Status', filter: 'set', cellClass: p => `status-${p.value}` },
];| Name | Type | Description |
|---|---|---|
field | string | Property of the row object shown in this column. |
id | string | Column id. Defaults to field; required when there is no field. |
header | string | Header text. Defaults to field. |
type | 'text' | 'number' | 'date' | 'boolean' | Drives alignment, formatting, sorting and the default filter. |
format | string | Intl options | Number: '#,##0.00', '0.0%', 'currency:KRW', 'integer'. Date: 'date', 'datetime', 'time', 'short', 'medium', 'long'. |
width / minWidth / maxWidth | number | Pixel widths. Default width 120, min 40. |
flex | number | Share of the leftover width. Resizing turns it into a fixed width. |
pinned | 'left' | 'right' | Keep the column visible while scrolling horizontally. |
hide | boolean | Start hidden. Users can show it from the column chooser. |
align | 'left' | 'center' | 'right' | Cell alignment. Numbers default to right. |
sortable | boolean | Default true. |
resizable | boolean | Default true. |
filter | 'text' | 'number' | 'date' | 'set' | false | Filter UI in the column menu. Default depends on type. |
comparator | (a, b, rowA, rowB) => number | Custom sort order. |
valueGetter | (row) => unknown | Computed value, e.g. quantity × price. Used for display, sort, filter and summaries. |
renderer | (params) => string | HTMLElement | Custom cell content. Strings are inserted as HTML — escape untrusted data. |
cellClass | string | (params) => string | Extra CSS class on cells. |
children | ColumnDef[] | Makes this a column group header. |
merge | boolean | (a, b, rowA, rowB) => boolean | Merge consecutive rows with the same value into one tall cell. |
wrapHeaderText | boolean | Let a long header label wrap. |
aggFunc | 'sum' | 'avg' | 'min' | 'max' | 'count' | (values, rows) => value | Aggregate shown in group rows. |
chooser | boolean | false keeps the column out of the column chooser. |
lockVisible | boolean | Users cannot hide this column. |
editable | boolean | Allow editing and pasting (needs field). |
editor | { type: 'select', options } | { type: 'textarea' } | (params) => editor | Editor for this column. options can be a function of the row (dependent selects). |
required | boolean | Reject blank values. |
validate | (value, row) => true | string | Return a message to reject a value. |
Grid options
| Name | Type | Description |
|---|---|---|
columns | ColumnDef[] | Required. Column definitions. |
data | object[] | Rows. |
getRowId | (row) => string | Stable id; keeps selection across setData. |
rowSelection | 'single' | 'multiple' | Adds a checkbox column and click selection. |
rowHeight / headerHeight | number | Pixels. Default follows the font size (36 / 40 at 13px). |
theme | string | 'light', 'dark', 'board', 'erp', 'excel', 'classic' or your own. |
fontSize / fontFamily | number / string | Grid font. Row heights follow the size. |
locale | string | LocalePack | UI language, e.g. 'ko' or an imported pack. Default: the browser's language. |
sideBar | boolean | { open } | Dock the Columns panel on the right. |
summary | SummaryOptions | Summary rows. See Summary rows. |
rangeSelection | boolean | Excel-style cell ranges by drag, Shift+click, Shift+arrows, Ctrl+A. Default true. |
statusBar | boolean | Row counts plus count/sum/average/min/max of the selected cells. |
fillHandle | boolean | Drag the selection corner to fill editable cells. Default true. |
pinnedTopRows / pinnedBottomRows | Row[] | Rows fixed above or below the scrolling rows. |
findBar / wrapHeaderText | boolean | Ctrl+F find bar (default true); wrap all header labels. |
groupBy | string[] | Group rows by these column ids, in order. |
groupDefaultExpanded | number | Levels open at start: 0 none (default), n levels, -1 all. Also applies to tree data. |
groupPanel | boolean | Bar above the header where headers can be dropped to group. |
treeData | { getParentId(row) } | Nest rows under their parent id. Needs getRowId. |
detailRenderer / detailHeight / isRowMaster | (params) => Element | string / number / (row) => boolean | Expandable panel under a row (default height 240); limit which rows can expand. |
rowDrag | boolean | Handle column for dragging rows into a new order (off while sorted, filtered or grouped). |
pagination | boolean | { pageSize?, pageSizes? } | Show one page at a time with a pager (default 100 rows per page). |
datasource | { getRows({ start, end, sort, filters, quickFilter }) } | Load rows from your server in blocks; must resolve to { rows, total? }. |
blockSize / maxBlocks | number | Rows per request (100) and blocks kept in memory (100). |
sideBar | boolean | { open?, panels?: ('columns' | 'filters')[] } | Docked panels for columns and filters. |
keymap | Record<string, action | false> | Remap or disable keys, e.g. { Enter: false, 'Ctrl+E': 'edit' }. |
contextMenu | false | (params) => items | false keeps the browser menu; a function customizes the right-click menu. |
Data and row ids
Rows are ordinary objects. Without getRowId rows are identified by position, so replacing the data clears the selection. With it, the selection survives setData as long as the ids still exist.
const grid = createGrid(el, {
columns,
data: rows,
getRowId: row => String(row.id), // keeps selection and focus stable across setData
});
grid.setData(await fetch('/api/orders').then(r => r.json()));
grid.getData(); // rows as passed in
grid.getDisplayedRowCount(); // rows after filtersSorting
Click a header to cycle ascending, descending and unsorted. Shift-click (or Ctrl/⌘-click) adds the column as another sort key; a small number shows its order. Empty values always sort last. Text uses the active language's collation, so Korean, accents and numbers inside text sort naturally.
grid.setSort([{ colId: 'country', dir: 'asc' }, { colId: 'total', dir: 'desc' }]);
grid.getSort();
grid.setSort([]); // clear
// custom order
{ field: 'grade', comparator: (a, b) => ORDER.indexOf(a) - ORDER.indexOf(b) }Filtering
Each column's ⋮ menu holds a filter that matches its type: text (contains, equals, starts/ends with, blank…), number (=, ≠, <, ≤, >, ≥, between, blank), date (equals, before, after, between) and set (a searchable checklist of the column's values). Text matching ignores case. A filled filter icon marks filtered columns.
grid.setFilter('name', { type: 'text', op: 'contains', value: 'kim' });
grid.setFilter('total', { type: 'number', op: 'between', value: 1000, valueTo: 5000 });
grid.setFilter('joined', { type: 'date', op: 'after', value: '2025-01-01' });
grid.setFilter('country', { type: 'set', values: ['Japan', 'South Korea'] });
grid.setFilter('name', null); // clear one
grid.setQuickFilter('kim seoul'); // every word must match some visible column
grid.on('filterChange', e => console.log(e.filters));Pinning and column groups
Pinned columns stay visible while the middle section scrolls horizontally. Users can pin and unpin from the column menu. Columns with children become group headers, nested as deep as you like; a group split across pinned sections shows its header in each.
const columns = [
{ field: 'name', header: 'Name', pinned: 'left' },
{ header: 'Revenue 2026', children: [
{ header: 'First half', children: [{ field: 'q1' }, { field: 'q2' }] },
{ header: 'Second half', children: [{ field: 'q3' }, { field: 'q4' }] },
] },
{ field: 'total', header: 'Total', pinned: 'right' },
];Column customization
Users can drag headers to reorder, drag header edges to resize, and use the column menu to autosize, hide, pin or reset columns. Choose columns opens a chooser with search, select all, the group tree and drag-to-reorder; the same panel can be docked on the right with sideBar. Store getColumnState() to remember each user's layout.
const grid = createGrid(el, {
columns: [
{ field: 'id', lockVisible: true }, // cannot be hidden
{ field: 'internalCode', chooser: false }, // not listed in the chooser
...rest,
],
data,
sideBar: { open: false }, // docked Columns panel (true = closed, { open: true } = open)
});
grid.openColumnChooser();
grid.setColumnVisible('email', false);
grid.autoSizeColumns(); // all columns
grid.autoSizeColumns(['name']); // one column
// save / restore the user's layout
localStorage.setItem('layout', JSON.stringify(grid.getColumnState()));
grid.applyColumnState(JSON.parse(localStorage.getItem('layout')));
grid.resetColumnState();Summary rows
Summary rows show totals, averages and other aggregates. Put them at the top or bottom; keep them pinned outside the scroll area, or let them scroll with the data. They recalculate when data, filters or (with scope: 'selected') the selection change, and use each column's format unless a cell sets its own.
createGrid(el, {
columns,
data,
summary: {
position: 'bottom', // 'top' | 'bottom'
pinned: true, // true: fixed; false: scrolls with the rows
scope: 'filtered', // 'filtered' | 'all' | 'selected'
rows: [
{ label: 'Total', columns: { qty: 'sum', amount: 'sum', id: 'count' } },
{ label: 'Average', columns: { amount: { agg: 'avg', format: '#,##0.0' } } },
{ columns: {
amount: { agg: 'max', prefix: 'max' },
country: values => `${new Set(values).size} countries`, // custom
} },
],
},
});
grid.setSummary({ position: 'top', rows: [...] }); // change at runtime
grid.setSummary(null); // remove
grid.getSummaryValues(); // [{ qty: 1234, amount: 99000, id: 50 }, …]sum,avg,min,maxuse numeric values and skip blanks;min/maxalso compare dates.countis the number of rows in scope.- A function receives the column's values and the rows; return a number to have it formatted, or a string to show as is.
- The row
labelappears in the first visible column without an aggregate in that row.
Editing
Mark a column editable: true and users can edit its cells: double-click, press Enter or F2, or just start typing. Enter saves and moves down, Tab saves and moves to the next editable cell, Esc cancels. Booleans toggle with Space or a double-click.
The editor follows the column type: text, numbers (1,234 is accepted), dates and booleans. Use a select editor for a fixed list. When its options is a function of the row, you get dependent selects such as cities by country. required and validate keep bad values out; the editor stays open with a message until the value is fixed.
Every edit, paste, insert and removal is tracked and can be undone with Ctrl+Z. Changed cells get a small corner mark. When you're ready to save, read getModifiedRows() or the column-level getChanges(), send them to your server, then call commitChanges().
const COUNTRIES = { KR: ['Seoul', 'Busan'], JP: ['Tokyo', 'Osaka'] };
const grid = createGrid(el, {
columns: [
{ field: 'id', type: 'number' }, // read-only
{ field: 'name', editable: true, required: true },
{ field: 'qty', type: 'number', editable: true, validate: v => v >= 0 || 'Must be 0 or more' },
{ field: 'country', editable: true, editor: { type: 'select', options: Object.keys(COUNTRIES) } },
{ field: 'city', editable: true, editor: { type: 'select', options: row => COUNTRIES[row.country] } },
{ field: 'memo', editable: true, editor: { type: 'textarea' } },
{ field: 'active', type: 'boolean', editable: true }, // Space / double-click toggles
],
data,
});
grid.addRows([{ name: 'New' }], 0);
grid.removeSelectedRows();
grid.undo(); grid.redo();
// send only what changed
const { created, updated, deleted } = grid.getModifiedRows();
await save({ created, updated, deleted, cells: grid.getChanges() });
grid.commitChanges(); // or grid.revertChanges()Spreadsheet tools
Set merge: true on a column and consecutive rows with the same value become one tall cell, which is handy for reports grouped by region or team. Sort by that column first. statusBar: true adds a bar that shows row counts and the count, sum, average, minimum and maximum of the selected cells. Drag the small square at the corner of a selection to fill editable cells: numbers and dates continue their step, text ending in a number counts up, and anything else repeats.
Use pinnedTopRows and pinnedBottomRows for rows that should never scroll away, such as targets or references. Ctrl+F opens a find bar inside the grid; find() does the same from code.
createGrid(el, {
columns: [
{ field: 'region', merge: true }, // identical neighbours become one tall cell
{ field: 'team', merge: (a, b, ra, rb) => a === b && ra.region === rb.region },
{ field: 'q1', type: 'number', editable: true, wrapHeaderText: true },
],
data: rowsSortedByRegion,
statusBar: true, // "1,000 rows · Count 12 Sum 48,000 Average 4,000 …"
fillHandle: true, // drag the corner: 1, 2 → 3, 4, 5 …; Mon → copies; "item 7" → "item 8"
pinnedTopRows: [{ region: 'Target', q1: 50000 }],
});
grid.find('Seoul'); // highlights matches, focuses the first; Ctrl+F opens the same as a bar
grid.findNext();Grouping, trees and details
Pass groupBy with one or more column ids and rows are gathered under group rows that show the value and the number of rows. Give a column an aggFunc such as 'sum' or 'avg' and every group row shows that total. Groups follow the sort direction of their column. Open them with the arrow, a double-click, or Enter, Space and the left and right arrow keys. With groupPanel: true, people can drag a header into the bar above the grid to group by it themselves.
For data that already forms a hierarchy, such as an org chart or a folder list, use treeData with a getParentId function. Filtering keeps the parents of every match, so results stay in context. detailRenderer adds a panel under a row that can hold anything, including another grid. rowDrag: true adds a handle for putting rows in order by hand; it pauses while the rows are sorted, filtered or grouped.
// Row grouping with totals
createGrid(el, {
columns: [
{ field: 'country' },
{ field: 'name' },
{ field: 'sales', type: 'number', aggFunc: 'sum' }, // shown in each group row
],
data: rows,
groupBy: ['country'], // or api.setGroupBy(['country', 'city'])
groupPanel: true, // drag headers above the grid to group
groupDefaultExpanded: 1, // 0 = collapsed, -1 = everything open
});
// Tree data: rows point at their parent
createGrid(el, {
columns: [{ field: 'name' }, { field: 'budget', type: 'number' }],
data: orgRows,
getRowId: r => r.id,
treeData: { getParentId: r => r.parent },
});
// Master / detail and row drag
createGrid(el, {
columns, data,
detailHeight: 160,
detailRenderer: ({ row }) => `<p>Orders of ${row.name}</p>`, // or return an element
rowDrag: true,
});Pages and server data
With pagination the grid shows one page at a time and puts a pager under the rows. People can jump to the first, previous, next or last page, and pick the number of rows per page from pageSizes. Sorting or filtering goes back to the first page.
When the data lives on a server, pass a datasource instead of data. The grid asks getRows only for the rows on screen, in blocks of blockSize, and draws grey placeholders until they arrive. Sorting, filters and the quick search are sent along so the server can apply them. Return total when you know it. Leave it out for infinite scroll, which keeps loading until a block comes back short. Add pagination as well, and every page becomes one request. Grouping and adding or removing rows only work with data in the browser.
// Pages in the browser
createGrid(el, { columns, data, pagination: { pageSize: 50, pageSizes: [20, 50, 100] } });
// Rows from your server, 100 at a time, as people scroll
createGrid(el, {
columns,
datasource: {
async getRows({ start, end, sort, filters, quickFilter }) {
const res = await fetch('/api/orders?' + new URLSearchParams({
start, end, sort: JSON.stringify(sort), filters: JSON.stringify(filters), q: quickFilter,
}));
const { rows, total } = await res.json();
return { rows, total }; // leave out total for infinite scroll
},
},
blockSize: 100,
// pagination: { pageSize: 50 }, // add this for server-side pages
});Export and import
exportXlsx() saves the rows as a real Excel file. Numbers stay numbers with the column's format, dates stay dates, column groups become merged header rows, and the header is frozen with filter buttons. exportCsv() writes CSV with a byte order mark, so Excel opens Korean and other non-Latin text correctly. Text that looks like a formula is prefixed so a spreadsheet never runs it. Both export what is on screen by default: the current sort and filters, and the visible columns. Choose other rows or columns with the options.
importFile() reads an .xlsx or CSV file. The first row is matched to your columns by header or field name, and every value is checked like a paste, so a number column only takes numbers and a date column takes dates such as 2026-03-05 or 2026.3.5. Imported rows are added at the end and can be undone, or they can replace the data. With sideBar, people get a Filters panel next to the Columns panel and can filter several columns at once.
// Download what people see (current sort and filters)
grid.exportXlsx('orders.xlsx');
grid.exportCsv('orders.csv', { separator: ';' });
// Pick columns and rows
grid.exportXlsx('picked.xlsx', { columns: ['name', 'total'], rows: 'selected', sheetName: 'Picked' });
// Read a file chosen by the user
input.addEventListener('change', async () => {
const { rows, skipped } = await grid.importFile(input.files[0]); // { mode: 'replace' } to swap
});
// Filters in a side panel next to the columns panel
createGrid(el, { columns, data, sideBar: { open: true, panels: ['filters', 'columns'] } });
// Without a grid
import { writeXlsx, readXlsx } from '@freegridjs/core';
const bytes = writeXlsx([{ name: 'Report', rows: [['Name', 'Total'], ['Kim', 1200]] }]);Copy, paste and right-click
Select cells like in Excel: drag across them, Shift-click, use Shift with the arrow keys, or press Ctrl+A. Ctrl+C copies the range in a format Excel and Google Sheets understand: numbers stay numbers, dates become YYYY-MM-DD. Ctrl+V pastes cells copied from Excel starting at the selected cell. A single copied value fills the whole selected range.
Pasting only changes columns with editable: true, so ids and computed columns stay safe. Values are converted to the column type: 1,234, ₩12,000, (5) and 12% become numbers; 2026. 3. 5. and 2026년 3월 5일 become dates. Values that can't be converted are skipped and counted in the paste event.
Right-click a cell for Copy, Copy with headers, Paste, autosize, hide and pin. Right-click a header for the column menu. Turn ranges off with rangeSelection: false, or bring back the browser menu with contextMenu: false.
const grid = createGrid(el, {
columns: [
{ field: 'id', type: 'number' }, // read-only
{ field: 'name', editable: true }, // paste allowed
{ field: 'qty', type: 'number', editable: true }, // "1,234", "(5)", "12%" are parsed
{ field: 'due', type: 'date', editable: true }, // "2026-03-05", "2026. 3. 5." …
],
data,
rangeSelection: true, // default: drag / Shift+click / Shift+arrows / Ctrl+A
contextMenu: p => [
...p.defaultItems, // Copy, Paste, Pin, Hide, …
'separator',
{ label: 'Open order', action: () => open(p.row) },
],
});
grid.on('cellValueChanged', e => save(e.row)); // persist edits
grid.on('paste', e => console.log(e.changed, 'cells changed'));
grid.setRange({ start: { rowIndex: 0, colId: 'name' }, end: { rowIndex: 9, colId: 'qty' } });
grid.copySelection({ headers: true }); // TSV, also written to the clipboard
grid.pasteText('Kim 1,234
Lee 56');The menu's Paste item reads the clipboard, which some browsers block or ask about. Ctrl+V always works because it uses the browser's own paste event.
Row selection
With rowSelection the grid adds a checkbox column. Click selects a row, Ctrl/⌘-click toggles, Shift-click selects a range, the header checkbox selects every filtered row, and Space toggles the focused row.
const grid = createGrid(el, { columns, data, rowSelection: 'multiple', getRowId: r => r.id });
grid.on('selectionChange', e => console.log(e.selected.length));
grid.getSelectedRows();
grid.selectAll(); // selects the filtered rows
grid.deselectAll();Keyboard
Click a cell or tab into the grid, then:
| Key | Action |
|---|---|
← ↑ → ↓ | Move the focused cell. |
Tab / Shift+Tab | Next / previous cell, wrapping rows; leaves the grid at the ends. |
Home / End | First / last column. |
Ctrl+Home / Ctrl+End | First / last cell. |
PageUp / PageDown | One screen up / down. |
Shift + ← ↑ → ↓ / Home / End / Page | Extend the cell range. |
Ctrl+A | Select every cell. |
Ctrl+C / Ctrl+V | Copy to / paste from Excel (TSV). |
Ctrl+F | Find in the grid. |
Shift+F10 / ContextMenu | Open the right-click menu. |
Enter / F2 / typing | Start editing an editable cell. |
Enter / Tab / Esc (editing) | Save and move down / save and move right / cancel. |
Delete | Clear editable cells. |
Ctrl+Z / Ctrl+Y | Undo / redo. |
Space | Toggle the focused row’s selection. |
Esc | Close the column menu or chooser. |
Formatting and renderers
Numbers and dates are formatted with the browser's Intl APIs in the grid language, so 1234.5 becomes 1,234.5 in English and 1.234,5 in German. Use renderer for anything else; it gets value, formatted, row, rowIndex and column.
{ field: 'total', type: 'number', format: '#,##0' } // 1,234
{ field: 'rate', type: 'number', format: '0.0%' } // 12.5%
{ field: 'price', type: 'number', format: 'currency:KRW' } // ₩12,000
{ field: 'price', type: 'number', format: { maximumSignificantDigits: 3 } } // Intl options
{ field: 'joined', type: 'date', format: 'long' } // 2026년 3월 5일 (ko)
// element renderer
{ field: 'status', renderer: p => {
const b = document.createElement('span');
b.className = `badge badge-${p.value}`;
b.textContent = p.formatted;
return b;
} }A renderer that returns a string is inserted as HTML. Escape anything that comes from users, or return an element.
Themes and fonts
Built-in themes: light, dark, and four simple Korean-style themes — board (bulletin board: thick top rule, horizontal lines), erp (every cell bordered, compact), excel (gridlines, green focus) and classic (navy header). Font size and family are one option each; row heights follow the size unless you set them.
createGrid(el, { columns, data, theme: 'erp', fontSize: 12, fontFamily: 'Pretendard, sans-serif' });
grid.setTheme('board');
grid.setFont({ size: 14 });
grid.setFont({ family: null }); // back to the theme fontMake your own theme
/* any class named fg-theme-<name> is a theme */
.fg-theme-brand {
--fg-font: "Pretendard", sans-serif;
--fg-font-size: 13px;
--fg-bg: #fff; --fg-fg: #1d2433; --fg-muted: #6b7385;
--fg-header-bg: #f2f5fa; --fg-header-fg: #1d2433;
--fg-border: #d9dfe8; --fg-row-border: #edf0f5;
--fg-col-line: inset -1px 0 #edf0f5;
--fg-row-odd: #fff; --fg-row-hover: #f5f8ff; --fg-row-selected: #e6eeff;
--fg-accent: #3056d3; --fg-focus: #3056d3;
--fg-header-text-align: center; --fg-header-label-flex: 1 1 auto;
}| Variable | Kind | Controls |
|---|---|---|
--fg-font / --fg-font-size | font | Font family and size. |
--fg-bg / --fg-fg / --fg-muted | color | Background, text and secondary text. |
--fg-header-bg / --fg-header-fg / --fg-header-weight | color / weight | Header colors and weight. |
--fg-border / --fg-row-border | color | Outer border and row lines. |
--fg-col-line | box-shadow | Vertical cell lines, e.g. 'inset -1px 0 #ddd'. Default none. |
--fg-row-odd / --fg-row-hover / --fg-row-selected | color | Zebra, hover and selected rows. |
--fg-accent / --fg-focus | color | Buttons, sort icons and the focused cell. |
--fg-radius / --fg-cell-px | length | Corner radius and cell padding. |
--fg-header-text-align / --fg-top-border | keyword / border | Center header labels; thick top rule. |
Languages
English is built in. Korean, Simplified Chinese, Japanese, Spanish, French and German are separate imports so you only ship what you use. Without a locale option the grid picks the browser language when that pack is registered, otherwise English.
import ko from '@freegridjs/core/locales/ko';
createGrid(el, { columns, data, locale: ko });
// or register once and use codes
import { registerLocale } from '@freegridjs/core';
import ja from '@freegridjs/core/locales/ja';
registerLocale(ja);
grid.setLocale('ja');
// override a few messages
createGrid(el, { columns, data, locale: { ...ko, noRows: '조회 결과가 없습니다' } });React
@freegridjs/react exports <FreeGrid>. Every grid option is a prop and every event is an on… prop. The ref is the grid API. Changing data, columns, theme, locale, font or summary updates the grid; keep those objects stable with useMemo.
import { FreeGrid } from '@freegridjs/react';
import type { GridApi } from '@freegridjs/core';
import '@freegridjs/core/styles.css';
export function Orders({ rows }) {
const grid = useRef<GridApi>(null);
const columns = useMemo(() => [...], []); // keep props stable
return (
<FreeGrid
ref={grid}
columns={columns}
data={rows}
rowSelection="multiple"
theme="erp"
locale="ko"
summary={summary}
onCellClick={e => console.log(e.row)}
onSelectionChange={e => setCount(e.selected.length)}
style={{ height: 520 }}
/>
);
}
// grid.current?.setSort([{ colId: 'total', dir: 'desc' }])Vue
@freegridjs/vue exports <FreeGrid> for Vue 3.3+. Events are kebab-case (@cell-click, @selection-change…), and the component ref exposes getApi().
<script setup lang="ts">
import { FreeGrid } from '@freegridjs/vue';
import '@freegridjs/core/styles.css';
const grid = ref();
const sort = () => grid.value.getApi().setSort([{ colId: 'total', dir: 'desc' }]);
</script>
<template>
<FreeGrid
ref="grid"
:columns="columns"
:data="rows"
row-selection="multiple"
theme="erp"
locale="ko"
:height="520"
:summary="summary"
@cell-click="e => console.log(e.row)"
@selection-change="e => (count = e.selected.length)"
/>
</template>API reference
createGrid(element, options) returns this API. Calls after destroy() do nothing.
| Method | Returns | Description |
|---|---|---|
setData(rows) | void | Replace all rows. |
getData() | Row[] | Current rows (unfiltered, unsorted). |
setColumns(defs) | void | Replace column definitions. |
getColumnState() / applyColumnState(state) | ColumnState[] | Save and restore order, width, visibility and pinning. |
resetColumnState() | void | Back to the column definitions. |
setColumnVisible(colId, visible) | void | Show or hide a column. |
autoSizeColumns(colIds?) | void | Fit widths to header and visible cells. |
openColumnChooser() / closeColumnChooser() | void | Floating column chooser. |
toggleSideBar(open?) | void | Open or close the docked Columns panel. |
setSort(items) / getSort() | SortItem[] | e.g. [{ colId: 'total', dir: 'desc' }]. |
setFilter(colId, model | null) / getFilterModel() | FilterModel | Set or clear one column filter. |
setQuickFilter(text) | void | Search all visible columns. |
getSelectedRows() / selectAll() / deselectAll() | Row[] | Selection helpers. selectAll selects the filtered rows. |
setSummary(options | null) / getSummaryValues() | object[] | Change summary rows; read raw totals. |
getRange() / setRange(range | null) | GridRange | Read or set the selected cell range. |
copySelection({ headers? }) | string | Copy the range as Excel TSV and return it. |
pasteText(text) | { changed, skipped } | Paste TSV/Excel text at the range or focused cell. |
startEdit(rowIndex, colId) / stopEdit(cancel?) | boolean / void | Open or close the editor. |
addRows(rows, index?) / removeRows(rows | fn) / removeSelectedRows() | Row[] | Insert or remove rows (tracked, undoable). |
getModifiedRows() / getChanges() / hasChanges() | object | Created/updated/deleted rows and column-level diffs. |
commitChanges() / revertChanges() | void | Accept changes as the new baseline, or undo all of them. |
undo() / redo() / canUndo() / canRedo() | void / boolean | Undo history for edits, pastes and row changes. |
fillRange(target) | void | Fill from the selection into a larger range (series aware). |
find(text) / findNext() / findPrevious() / clearFind() | number / void | Highlight and step through matching cells. |
setPinnedRows({ top?, bottom? }) | void | Change pinned rows. |
setGroupBy(colIds) / getGroupBy() | void / string[] | Change or read the grouping. |
expandAll() / collapseAll() / setGroupExpanded(key, open) | void | Open or close groups and tree rows. Group keys look like 'country:KR|city:Seoul'; tree keys are row ids. |
setDetailExpanded(row, open) / isDetailExpanded(row) | void / boolean | Open, close or check a detail panel. |
moveRow(row, toIndex) | boolean | Move a row in the source data (undoable). False while sorting, filtering or grouping. |
setPage(n) / getPage() / getPageCount() | void / number | Move between pages (0-based). |
setPageSize(n) / getPageSize() | void / number | Change rows per page. |
refreshRows() | void | Server data: drop cached rows and load again. |
exportXlsx(filename?, options?) / getXlsx(options?) | void / Uint8Array | Excel file of the rows. Options: columns, rows (filtered | all | selected), headers, sheetName. |
exportCsv(filename?, options?) / getCsv(options?) | void / string | CSV with a BOM so Excel reads Korean; same options plus separator. |
importFile(file, { mode?, sheet? }) | Promise<{ rows, skipped }> | Read .xlsx or CSV; headers are matched to columns; append (default) or replace. |
toggleSideBar(open?, panel?) | void | Open or close the side bar, optionally on 'columns' or 'filters'. |
version | string | Exported constant with the package version, e.g. import { version }. |
getDisplayedRowCount() | number | Rows after filtering. |
scrollToRow(index) | void | Scroll a displayed row into view. |
setTheme(name) | void | Switch theme. |
setFont({ size?, family? }) | void | Change font; family null restores the theme font. |
setLocale(codeOrPack) | void | Switch UI language. |
on(event, handler) | () => void | Subscribe; returns an unsubscribe function. |
destroy() | void | Remove the grid and its listeners. |
Events
| Event | Payload | When |
|---|---|---|
cellClick / cellDoubleClick | { row, rowIndex, colId, value, event } | A data cell was clicked. |
selectionChange | { selected: Row[] } | Selected rows changed. |
sortChange | { sort } | Sort changed. |
filterChange | { filters } | A column filter changed. |
columnResize | { colId, width } | Resize or autosize finished. |
columnMove | { colId, toIndex } | A column was dragged to a new position. |
columnVisible | { colId, visible } | A column was shown or hidden. |
rangeChange | { range } | The cell range changed. |
cellValueChanged | { row, rowIndex, colId, oldValue, newValue } | A paste changed a cell. |
paste | { changed, skipped } | A paste finished. |
editStart / editStop | { rowIndex, colId, committed? } | An editor opened or closed. |
validationFailed | { rowIndex, colId, value, message } | An edited value was rejected. |
rowsAdded / rowsRemoved | { rows } | Rows were inserted or removed. |
groupChange / groupToggle | { groupBy } / { key, expanded } | Grouping changed; a group or tree row opened or closed. |
detailToggle | { row, expanded } | A detail panel opened or closed. |
rowDragEnd | { row, fromIndex, toIndex } | A row was dragged to a new position. |
pageChange | { page, pageSize, pageCount } | The page or page size changed. |
rowsLoaded / rowsLoadFailed | { start, end, total? } / { start, end, error } | A server block arrived or failed. |
focusChange | { rowIndex, colId } | The focused cell moved. |