Spans, wrapping and layout
colSpan and rowSpan decide per cell how many columns or rows it covers: a note row across several columns, or a region name spanning the rows your data…
Live demoSpans, wrapping & layout
Cells spanning rows and columns, wrapped text with growing rows, header buttons, right-to-left and card view.
Code for this demo
import { createGrid, type ColumnDef, type GridApi, type Row } from '@freegridjs/core';
import { separator, text, toggle, tr, type DemoContext } from '../ui';
export default function mount(ctx: DemoContext) {
const { lang } = ctx;
const h = (en: string, ko: string) => tr(lang, { en, ko });
const regions = [
{ region: h('Seoul', '서울'), items: [[h('Monitor', '모니터'), h('Pending payment for the March order; follow up with the buyer before the end of the week.', '3월 주문 입금 대기 중입니다. 이번 주가 끝나기 전에 구매 담당자에게 다시 연락해야 합니다.')], [h('Keyboard', '키보드'), h('Shipped.', '출고 완료.')], [h('Desk', '책상'), h('Assembly service booked for Friday afternoon, customer asked for a call 30 minutes before arrival.', '금요일 오후 조립 서비스 예약. 도착 30분 전에 전화해 달라는 요청이 있었습니다.')]] },
{ region: h('Busan', '부산'), items: [[h('Chair', '의자'), h('Back-ordered: the supplier expects new stock in two weeks.', '재고 없음: 공급처 입고까지 2주 예상.')], [h('Lamp', '조명'), h('Delivered.', '배송 완료.')]] },
{ region: h('Tokyo', '도쿄'), items: [[h('Monitor arm', '모니터 암'), h('Customs paperwork done.', '통관 서류 처리 완료.')], [h('Cable kit', '케이블 세트'), h('Gift wrap requested.', '선물 포장 요청.')]] },
];
const data: Row[] = [];
for (const r of regions) {
r.items.forEach(([item, memo], i) => data.push({
region: r.region, regionRows: i === 0 ? r.items.length : 1, item, memo,
qty: 1 + ((data.length * 7) % 9), price: 30 + ((data.length * 37) % 400),
}));
data.push({ kind: 'note', item: h(`${r.region}: shipping cut-off is 3 pm`, `${r.region}: 오후 3시 이후 주문은 다음 날 출고`) });
}
const columns: ColumnDef[] = [
// the first row of each region spans all of its rows
{ field: 'region', header: h('Region', '지역'), width: 110, rowSpan: ({ row }) => row.regionRows ?? 1 },
// note rows span the item, memo and qty columns
{ field: 'item', header: h('Item', '품목'), width: 150, colSpan: ({ row }) => (row.kind === 'note' ? 3 : 1),
cellClass: ({ row }) => (row.kind === 'note' ? 'demo-note' : undefined) },
// long memos wrap; autoRowHeight grows the row
{ field: 'memo', header: h('Memo', '메모'), width: 280, wrapText: true },
{ field: 'qty', header: h('Qty', '수량'), type: 'number', width: 90 },
{
field: 'price', header: h('Price', '단가'), type: 'number', format: '#,##0', width: 120, headerClass: 'demo-money',
// a help button in the header; pressing it does not sort
headerRenderer: ({ header }) => {
const span = document.createElement('span');
const b = document.createElement('button');
b.type = 'button';
b.textContent = '?';
b.className = 'demo-help';
b.addEventListener('click', () => ctx.log(h('Prices are in USD, before tax.', '단가는 달러, 세전 금액입니다.')));
span.append(`${header} `, b);
return span;
},
},
];
let grid: GridApi;
let rtl = false;
let cards = false;
const create = () => {
grid?.destroy();
grid = createGrid(ctx.host, {
columns, data, locale: lang, rangeSelection: true,
autoRowHeight: true,
direction: rtl ? 'rtl' : 'ltr',
// cardView: a width in px; below it rows turn into cards (phones). 99999 forces cards here.
cardView: cards ? 99999 : false,
});
};
create();
text(ctx, h('Region cells span their rows, note rows span three columns, long memos wrap and the row grows. Press ? in the Price header.',
'지역 셀은 여러 행을, 안내 행은 세 칸을 차지합니다. 긴 메모는 줄바꿈되고 행 높이가 늘어납니다. 단가 헤더의 ? 버튼도 눌러 보세요.'));
separator(ctx);
toggle(ctx, h('Right to left (RTL)', '오른쪽→왼쪽 (RTL)'), false, on => { rtl = on; create(); ctx.log(`direction: ${on ? 'rtl' : 'ltr'}`); });
toggle(ctx, h('Card view (phones)', '카드 보기 (휴대폰)'), false, on => { cards = on; create(); ctx.log(`cardView: ${on}`); });
return grid!;
}Live demoRow numbers, filter row & saved views
Row numbers, a filter row, tooltips, collapsible column groups, sparklines, row spacing and saved views per person.
Code for this demo
import { createGrid, type GridApi } from '@freegridjs/core';
import { makeRows } from '../data';
import { button, select, separator, text, tr, type DemoContext } from '../ui';
const KEY = 'freegrid-demo-view';
export default function mount(ctx: DemoContext) {
const { lang } = ctx;
const h = (en: string, ko: string) => tr(lang, { en, ko });
const grid: GridApi = createGrid(ctx.host, {
columns: [
{ field: 'name', header: h('Name', '이름'), width: 150, headerTooltip: h('Customer name', '고객 이름'), tooltip: true },
{ field: 'email', header: h('Email', '이메일'), width: 150 }, // cut off: hover to read it all
{ field: 'country', header: h('Country', '국가'), width: 120 },
{
header: h('Sales', '매출'), children: [
{ field: 'total', header: h('Year', '연간'), type: 'number', format: '#,##0', width: 110, columnGroupShow: 'closed' },
{ field: 'q1', header: 'Q1', type: 'number', format: '#,##0', width: 90, columnGroupShow: 'open' },
{ field: 'q2', header: 'Q2', type: 'number', format: '#,##0', width: 90, columnGroupShow: 'open' },
{ field: 'q3', header: 'Q3', type: 'number', format: '#,##0', width: 90, columnGroupShow: 'open' },
{ field: 'q4', header: 'Q4', type: 'number', format: '#,##0', width: 90, columnGroupShow: 'open' },
],
},
// four quarters as a small line chart
{ id: 'trend', header: h('Trend', '추이'), width: 110, valueGetter: r => [r.q1, r.q2, r.q3, r.q4], sparkline: true, sortable: false, filter: false },
{ field: 'joined', header: h('Joined', '가입일'), type: 'date', width: 120 },
],
data: makeRows(500),
locale: lang,
rowNumbers: true,
filterRow: true,
rowSelection: 'multiple',
getRowId: r => String(r.id),
});
text(ctx, h('Type in the boxes under the header: Kim, >50000, 10000~20000, >2025-01-01. Press + on Sales to see the quarters. Hover a cut-off email.',
'헤더 아래 칸에 입력해 보세요: Kim, >50000, 10000~20000, >2025-01-01. 매출 옆 + 버튼으로 분기를 펼치고, 잘린 이메일 위에 마우스도 올려 보세요.'));
separator(ctx);
select(ctx, h('Spacing', '행 간격'), [['compact', h('Compact', '좁게')], ['normal', h('Normal', '보통')], ['comfortable', h('Comfortable', '넓게')]], 'normal',
d => grid.setDensity(d));
button(ctx, h('Save view', '화면 저장'), () => {
localStorage.setItem(KEY, JSON.stringify(grid.getState()));
ctx.log(h('saved: columns, sort, filters, groups, selection', '저장: 컬럼, 정렬, 필터, 그룹, 선택'));
});
button(ctx, h('Restore view', '화면 복원'), () => {
const saved = localStorage.getItem(KEY);
if (!saved) return ctx.log(h('nothing saved yet', '저장한 화면이 없습니다'));
grid.setState(JSON.parse(saved));
ctx.log(h('restored', '복원했습니다'));
});
button(ctx, h('Reset', '초기화'), () => {
grid.setState({ sort: [], filters: {}, quickFilter: '', columnGroups: {}, selected: [] });
grid.resetColumnState();
});
return grid;
}colSpan and rowSpan decide per cell how many columns or rows it covers: a note row across several columns, or a region name spanning the rows your data says. Spanned cells stay one cell for selection, copying and Excel export. wrapText: true wraps long text, and autoRowHeight: true grows each row to fit.
headerRenderer puts icons or help buttons in a header; pressing them does not sort. direction: 'rtl' lays the grid out right to left for Arabic or Hebrew, and cardView turns rows into cards on narrow screens. On phones, tap a focused cell again to edit and long-press for the menu.
createGrid(el, {
columns: [
// one cell spans the row's first three columns when it is a note row
{ field: 'title', colSpan: ({ row }) => (row.kind === 'note' ? 3 : 1) },
{ field: 'region', rowSpan: ({ row }) => row.regionRows ?? 1 },
// long text wraps; the row grows to fit
{ field: 'memo', width: 220, wrapText: true },
// icons or buttons in the header; sorting still works
{ field: 'amount', type: 'number', headerClass: 'money',
headerRenderer: ({ header }) => `${header} <button type="button" data-fg-no-sort>?</button>` },
],
data,
autoRowHeight: true,
direction: 'rtl', // Arabic, Hebrew: first column on the right
cardView: 640, // narrower than 640px: rows become cards (phones)
});Viewing aids: row numbers, filter row, tooltips, density, saved views
rowNumbers adds a 1, 2, 3… column on the left, and filterRow puts boxes under the header that filter as you type: >10 or 10~20 for numbers, >2026-01-01 for dates. tooltip and headerTooltip add hover text, and cut-off text shows in full on hover. columnGroupShow makes column groups collapsible, and sparkline draws an array of numbers as a small chart in the cell. density changes the row spacing, and autoHeight lets the grid grow with its rows. Save and restore each person's view with getState() and setState().
// rows numbered, a filter row, tooltips, compact rows
const grid = createGrid(el, {
columns: [
{ field: 'name', headerTooltip: 'Customer name', tooltip: true },
{ field: 'note', width: 160 }, // cut-off text shows in full on hover
{ header: 'Sales', children: [
{ field: 'total', columnGroupShow: 'closed' },
{ field: 'q1', columnGroupShow: 'open' },
{ field: 'q2', columnGroupShow: 'open' },
] },
{ id: 'trend', valueGetter: r => [r.q1, r.q2, r.q3, r.q4], sparkline: true },
],
data, rowNumbers: true, filterRow: true, density: 'compact',
});
// save each person's view, restore it next time
localStorage.setItem('orders-view', JSON.stringify(grid.getState()));
grid.setState(JSON.parse(localStorage.getItem('orders-view') ?? '{}'));