はじめに
業務系のWebアプリを作っていると、「Excelのように一覧をサクサク編集したい」という要望によく出会います。既存のデータグリッドライブラリはどれも高機能ですが、
- 日本語IMEでの入力がうまく動かない(変換確定のEnterでセルが移動してしまう等)
- 多機能すぎて導入・学習コストが高い
という点でしっくり来なかったため、スプレッドシート風のシンプルなデータグリッドライブラリ「MasumeGrid(マス目)」 を自作してnpmに公開しました。
・npm
https://www.npmjs.com/package/react-masume-grid
・github
https://github.com/t92345era/react-masume-grid
MasumeGridとは
軽量・汎用のReact用スプレッドシートコンポーネントです。依存はReactのみ(gzipで約5KB)で、日本語IMEに完全対応していること、基本的なセル型(文字列・数値・選択肢・日付・チェックボックス) に加えて任意のコンポーネントを描画できるテンプレート型を備えつつシンプルであることが特徴です。
主な機能は次のとおりです。
- グリッド表示 — 行番号・ヘッダーの表示切り替え、列幅指定、ドラッグでの列幅リサイズ、行の仮想化描画(数万行でも軽快)
- セル型 — 文字列 / 数値(全角・カンマ正規化)/ 選択肢(プルダウン、コード保存・ラベル表示)/ 日付(カレンダー入力、和式表記の貼り付け正規化)/ チェックボックス(クリック・Spaceでトグル)/ テンプレート(任意のコンポーネントを描画)
- セル編集 — ダブルクリック / F2 / キー入力で編集開始。IMEオンの状態で「A」を打つと、セルが編集状態になり「あ」がそのまま入力されます
- 範囲選択 — マウスドラッグ、Shift+クリック / 矢印キーで拡張、Ctrl(⌘)+クリックで複数範囲選択。行・列ヘッダークリックで行/列選択も可能
- コピー&ペースト — Ctrl(⌘)+C / X / V。ExcelやGoogleスプレッドシートと相互運用できるTSV形式(改行・タブ・引用符を含むセルにも対応)。コピーした範囲はExcelと同じく動く点線枠(マーチングアンツ) で表示されます
デモ
インストール
npm install react-masume-grid
React 18以上(peerDependency)で動作します。ライセンスはMITです。
使い方
最小限の使い方はこれだけです。データは string[][] の2次元配列で渡す制御コンポーネント方式で、onChange で新しい配列を受け取ります。
import { useState } from 'react';
import { MasumeGrid } from 'react-masume-grid';
// ライブラリを import すると CSS も自動で読み込まれます。
// バンドラー設定によっては明示的に: import 'react-masume-grid/styles.css';
function App() {
const [data, setData] = useState<string[][]>([
['りんご', '100', '果物'],
['にんじん', '80', '野菜'],
]);
return (
<MasumeGrid
data={data}
onChange={setData}
columns={[
{ title: '品名', width: 160 },
{ title: '単価', width: 80 },
{ title: 'カテゴリ', width: 120, readOnly: true },
]}
showRowNumbers
style={{ height: 400 }}
/>
);
}
columns を省略すると列数は data から自動で導出され、ヘッダーはスプレッドシートらしく A, B, C… 表記になります。
セル型
ColumnDef.type で列ごとのセル型を指定できます。ポイントは、内部データはすべて文字列のままという設計です。型は編集UI・入力の正規化・表示だけを制御するので、クリップボード経由のExcel連携がシンプルに保てます。
const columns: ColumnDef[] = [
{ title: '商品名' }, // text(既定)
{ title: '単価', type: 'number' },
{ title: 'カテゴリ', type: 'select', options: [
{ value: 'C01', label: '果物' }, // コードを保存、ラベルを表示
{ value: 'C02', label: '青果' },
]},
{ title: '状態', type: 'select', options: ['在庫あり', '取り寄せ'], filterable: false },
{ title: '入荷日', type: 'date' },
{ title: '検品済', type: 'checkbox' },
{ title: '操作', type: 'template', readOnly: true,
template: ({ row }) => <button onClick={() => openDetail(row)}>詳細</button> },
];
| 型 | 編集UI | 動作 |
|---|---|---|
text |
テキスト(IME対応) | 既定。自由入力 |
number |
テキスト(IME対応) | 右寄せ表示。確定時に全角数字→半角、カンマ除去。数値でない入力は拒否(元の値を保持) |
select |
絞り込み付きプルダウン | ↑↓で候補移動、Enter/クリックで確定、文字入力で絞り込み。{value, label} 形式ならコード保存・ラベル表示。strict: false で選択肢外の自由入力も許可 |
date |
ネイティブの日付ピッカー |
YYYY-MM-DD で保存。2026/7/6・2026年7月6日・20260706・全角数字の貼り付けも正規化 |
checkbox |
トグル(テキスト編集なし) | チェック時 'true'・未チェック時 '' を保存。クリックまたはSpaceでトグル |
template |
なし(カスタム描画) | 列定義の template 関数が任意のコンポーネントを描画。ボタン等はそのままクリック可能 |
正規化・検証は編集確定と貼り付けの両方に適用され、無効な値のセルは変更されずスキップされます。
チェックボックス型(v0.2.0で追加)
Excelの「レ点」列をそのまま再現したセル型です。
- チェックボックスのクリック、またはセル選択中のSpaceキーでトグル。範囲選択して Space を押すと、選択中のチェックボックスセルをまとめてトグルできます(一括チェックに便利です)
- データ上はチェック時
'true'・未チェック時''の文字列として保存されるので、「データはすべて文字列」という設計はそのままです - Excelからの貼り付けにも対応しており、
TRUE/FALSE・1/0・yes/noなどの表記を自動で正規化します(解釈できない値は拒否され、セルは元の値を保持) - 正規化ロジックは
normalizeCheckboxInput/isCheckboxCheckedとしてエクスポートしているので、アプリ側の集計処理などにも再利用できます
あわせてv0.2.0では、select型に filterable オプションを追加しました。既定(true)では文字入力で候補が絞り込まれますが、filterable: false にするとネイティブの <select> に近い挙動になります(常に全候補を表示し、文字入力は先頭一致の候補へハイライトを移動)。候補数が少ないステータス列などに向いています。
テンプレート型セル(v0.3.0で追加)
type: 'template' を指定すると、セルの描画を任意のReactコンポーネントに委ねられます。行アクションのボタン、他セルから計算した派生値の表示、バッジやプログレスバーなど、組み込みのセル型では表現できないUIを自由に置けます。
列定義の template 関数は描画対象のセルごとに呼ばれ(行は仮想化されるため画面内のセルのみ)、引数として次のコンテキストを受け取ります。
| フィールド | 意味 |
|---|---|
row |
data 配列上の行インデックス(データソース上のインデックス) |
col |
列インデックス |
value |
セルに保存されている文字列値 |
row にはスクロール位置に関係なく常にデータソース上のインデックスが渡されるので、そのまま data[row] で同じ行の他のセルを参照できます。
const [data, setData] = useState<string[][]>(initialData); // [商品名, 単価, 数量]
const columns = useMemo<ColumnDef[]>(
() => [
{ title: '商品名' },
{ title: '単価', type: 'number' },
{ title: '数量', type: 'number' },
// 派生表示: row でデータ行を参照し 単価×数量 を表示。
// columns が data に依存するので、data の変更時に再計算する
{
title: '金額', width: 100, type: 'template', readOnly: true,
template: ({ row }) => {
const total = Number(data[row]?.[1] || 0) * Number(data[row]?.[2] || 0);
return <span style={{ marginLeft: 'auto', padding: '0 6px' }}>¥{total.toLocaleString()}</span>;
},
},
// 行アクション: テンプレート内のボタンはネイティブにクリックできる
{
title: '操作', width: 90, type: 'template', readOnly: true,
template: ({ row }) => (
<button type="button" onClick={() => openDetail(row)}>詳細</button>
),
},
],
[data],
);
いくつか補足です。
- テンプレート型セルにテキストエディタはありません。文字キー・F2・ダブルクリックで編集は始まりませんが、キーボード移動・範囲選択・コピーは通常どおり動作します
- グリッドは通常、フォーカスを内部の不可視textarea(後述)に固定しますが、テンプレートセル内のインタラクティブ要素(
button・a・inputなど)は例外扱いになっており、フォーカスを奪われずネイティブに操作できます - コピーされるのは描画結果ではなく保存値(
data[row][col])です。貼り付けやDeleteも保存値に作用するため、上の例のような表示専用列にはreadOnly: trueの指定をおすすめします
コピー範囲のマーチングアンツ表示(v0.3.0で追加)
Ctrl(⌘)+C / X でコピー・切り取りすると、対象範囲がExcelやGoogleスプレッドシートでおなじみの動く点線枠(マーチングアンツ) で囲まれます。「いまどこをコピーしたか」がひと目でわかるので、貼り付け先を選ぶ操作が迷いません。
点線枠は次のタイミングで消えます(Excelの挙動に準拠)。
- Escape を押したとき
- 貼り付けしたとき
- セルの編集を開始したとき
実装面の小ネタですが、CSSの破線ボーダーは破線のオフセットをアニメーションできないため、4辺をストライプ状の linear-gradient 背景で描いて background-position を動かすことで「点線が流れる」表現をしています。prefers-reduced-motion 設定時はアニメーションを止めるようにしています。
日本語IME対応のしくみ
このライブラリを作った一番の動機がここです。多くのグリッドライブラリでは、IMEの変換を確定するEnterがセル移動として解釈されてしまったり、変換候補ウィンドウが変な位置に出たりします。
MasumeGridでは、常にフォーカスされた不可視の <textarea> をアクティブセルの上に重ねる方式(Googleスプレッドシートと同じ手法)を採用しています。compositionstart で編集を開始するため、
- IMEオンのままタイプすると、候補ウィンドウがセルの位置に表示される
- 変換確定のEnterがセル移動と誤解釈されない(イベント順序が異なるSafariにも対応)
という、日本語ユーザーにとって自然な編集体験になっています。
主なProps
| Prop | 型 | 説明 |
|---|---|---|
data |
string[][] |
グリッドの内容(必須)。行の長さは不揃いでも可 |
columns |
ColumnDef[] |
列定義。省略時はdataから列数を導出 |
onChange |
(next: string[][]) => void |
編集・貼り付け・削除のたびに新しい2次元配列で呼ばれる |
onCellChange |
(row, col, value) => void |
変更セルごとに呼ばれる |
onSelectionChange |
(ranges) => void |
選択範囲の変更時 |
onColumnResize |
(col, width) => void |
列幅ドラッグの確定時 |
showRowNumbers / showHeader
|
boolean |
行番号・ヘッダーの表示(既定: true) |
readOnly |
boolean |
編集禁止(選択・コピーは可能) |
rowHeight / headerHeight / defaultColumnWidth
|
number |
サイズ調整 |
ColumnDef は { title?, width?, readOnly?, resizable?, type?, options?, strict?, filterable?, template? } を指定できます。
データは完全な制御コンポーネント方式で、onChange を実装しない限りグリッドは変化しません。Reactの流儀に沿った素直なAPIにしています。
スタイルのカスタマイズ
CSS変数を上書きするだけでテーマを変更できます。
.my-grid {
--masume-grid-accent: #0f9d58;
--masume-grid-sel-bg: rgba(15, 157, 88, 0.12);
--masume-grid-header-bg: #f0f4f1;
}
アクセントカラー(--masume-grid-accent)は選択枠だけでなく、マーチングアンツの点線色にも使われます。
現時点の制限
シンプルさを優先しているため、次の機能は現バージョンでは対応していません。
- 内部データは文字列のみ(桁区切り等の表示フォーマットは今後の課題。テンプレート型で代用は可能です)
- 列の仮想化は未対応(数百列を超える場合は注意)
- Undo / Redoなし(
onChangeベースの設計なので、履歴管理はホストアプリ側で実装できます) - セル結合・数式・列幅の自動調整なし
更新履歴
-
v0.2.0 — チェックボックス型セルを追加。select型に
filterableオプションを追加。normalizeCheckboxInput/isCheckboxCheckedをエクスポート - v0.3.0 — テンプレート型セル(任意コンポーネントの描画)を追加。コピー範囲のマーチングアンツ表示を追加
おわりに
「日本語入力が自然に使えて、基本的なセル型があって、それでいてシンプル」なデータグリッドが欲しくて作ったライブラリです。テンプレート型の追加で、行アクションボタン付きの一覧編集画面など、業務アプリの定番UIがこれ1つで組めるようになりました。
使ってみた感想や不具合報告など、GitHubのIssueでフィードバックをいただけると嬉しいです。
