「xlsx viewer online」で検索すると、同じ仕組みのサイトがずらりと並びます — スプレッドシートをアップロードすると、相手のサーバーがそれをパースし、画像にして返してくる。公開用のサンプルファイルならそれで十分です。しかし給与計算表や顧客データのエクスポート、NDA 下のファイルとなると、あなたは自分のデータを第三者に渡したことになります — そしてたいてい、どこかに「保管する権利」を与えるチェックボックスがあります。
そうしたことを一切しないビューアを作れます。ファイル選択 → パース → 描画というパイプライン全体が、ブラウザ内で完結する。バイト列がネットワークに触れることはありません。本記事は、社内ツール・顧客ポータル・ドキュメントサイトのどこにでも組み込める、読み取り専用の XLSX ビューアを React で丸ごと示します。
ビューアの完成形
これがすべてです — ドラッグ&ドロップのドロップゾーン、ファイルピッカー、ファイル情報バー、そして読み取り専用のグリッド。xlsx の インポート は無料の Lite エディションで動くため、ライセンスなしで動作します。
import { Reogrid, type ReogridInstance } from '@reogrid/lite/react';
import { useRef, useState, type DragEvent } from 'react';
export default function XlsxViewer() {
const gridRef = useRef<ReogridInstance>(null);
const [file, setFile] = useState<{ name: string; size: number } | null>(null);
const [dragging, setDragging] = useState(false);
async function open(f: File) {
if (!f.name.toLowerCase().endsWith('.xlsx')) return;
const ws = gridRef.current!.worksheet;
await ws.loadFromFile(f, { chunked: true }); // パース+描画をブラウザ内で
ws.protected = true; // 閲覧専用: 全セルをロック
setFile({ name: f.name, size: f.size });
}
function onDrop(e: DragEvent) {
e.preventDefault();
setDragging(false);
const f = e.dataTransfer.files[0];
if (f) open(f);
}
return (
<div
onDragEnter={(e) => { e.preventDefault(); setDragging(true); }}
onDragLeave={(e) => { e.preventDefault(); setDragging(false); }}
onDragOver={(e) => e.preventDefault()}
onDrop={onDrop}
style={{ position: 'relative', border: '1px solid #e2e8f0', borderRadius: 12, overflow: 'hidden' }}
>
<div style={{
display: 'flex', alignItems: 'center', gap: 12,
padding: '10px 14px', borderBottom: '1px solid #e2e8f0', background: '#f8fafc',
}}>
<label style={{ cursor: 'pointer', fontWeight: 600, color: '#1d4ed8' }}>
.xlsx を開く
<input
type="file"
accept=".xlsx"
hidden
onChange={(e) => e.target.files?.[0] && open(e.target.files[0])}
/>
</label>
<span style={{ fontSize: 13, color: '#64748b' }}>
{file
? `${file.name} · ${(file.size / 1024).toFixed(0)} KB · 読み取り専用`
: 'ファイルなし — ドラッグするか「.xlsx を開く」をクリック'}
</span>
</div>
<Reogrid ref={gridRef} style={{ width: '100%', height: 600 }} />
{dragging && (
<div style={{
position: 'absolute', inset: 0, background: 'rgba(59,130,246,0.08)',
border: '2px dashed #3b82f6',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 16, fontWeight: 600, color: '#1d4ed8', pointerEvents: 'none',
}}>
ドロップして表示
</div>
)}
</div>
);
}
loadFromFile は、input やドロップイベントから受け取った File オブジェクトをそのまま渡せます。FileReader でバイト列を読み、OOXML を展開し、セル・スタイル・結合・罫線・数式を構築して Canvas に描画します。fetch もアップロードも SheetJS のグルーコードもありません。
「読み取り専用」こそがビューアの本質
ビューアはエディタではありません。ユーザーがセルをクリックして入力できてしまうと、「どこに保存するのか?」に答える必要が出てきます — そして設計上、バックエンドは存在しません。だからロックします:
worksheet.protected = true;
これで全セルが保護モードになります。ユーザーはセルの選択・スクロール・コピーといった、ビューアで してほしい 操作は引き続きできますが、セルへの入力は何も起きません。ドキュメントビューアとドキュメントエディタの違いが、たった 1 行です。
新しいファイルを開くとワークシートがリセットされるため、ロードの 後 に設定してください。編集しようとしたときにフィードバックを出したい場合(「このファイルは読み取り専用です」というさりげないトーストなど)は、その試行を購読します:
worksheet.onProtectedCellEdit(({ row, column }) => {
showToast('このファイルは読み取り専用です');
});
シートの大部分はロックしたまま、特定の範囲だけを解除することもできます — 「ビューア」に 1 列だけコメント入力欄を持たせたい、といった場合に便利です:
worksheet.protected = true;
worksheet.setRangeLock(1, 5, 200, 5, 'unlocked'); // F 列だけ編集可能にする
ドロップだけでなく URL からも表示する
ドラッグ&ドロップが目玉のインタラクションですが、「このスプレッドシートを表示」というリンクの多くは、すでに自分のオリジン上にあるファイル — レポート、テンプレート、添付ファイル — を指しています。ファイルピッカーを省いて直接ロードしましょう:
// 自分のオリジン上のファイル(または CORS 対応の任意の URL)
await worksheet.loadFromUrl('/reports/2026-q2.xlsx', { chunked: true });
worksheet.protected = true;
バイト列がすでにメモリ上にある場合 — API から取得した、IndexedDB から取り出した、base64 の blob からデコードした — そのまま渡してディスク経由のラウンドトリップを省けます:
const buf = await fetch('/api/export/4821').then(r => r.arrayBuffer());
await worksheet.loadFromBuffer(buf);
worksheet.protected = true;
これで viewer?src=/reports/2026-q2.xlsx のような共有可能なルートも数行です — クエリパラメータを読み、loadFromUrl を呼ぶだけ。ファイルはあくまでクライアントで描画され、URL はバイト列の出どころにすぎません。
何が正しく描画されるか
実世界の .xlsx ファイルは見た目以上に雑です — テーマカラー、共有文字列、リッチテキストラン、条件付きの数値書式セクション。インポータは現実的な範囲をカバーします: セルの値と数式、フォントと塗りつぶし、辺ごとの罫線、結合セル、通貨/日付/カスタム数値書式、ウィンドウ枠の固定、行/列のグループ化、条件付き書式、画像。既知の非対応はピボットテーブルとグラフです(ピボットはフラットな値として読み込まれ、グラフはスキップされます)。
完全な対応表と巨大ファイルのパフォーマンス — 44 万セルのファイルがチャンク式ロードで最初のフレームを約 40 ms で描画する話 — は、関連記事 ブラウザで本物の Excel ファイルをインポートする で扱っています。上のビューアにある { chunked: true } フラグが、大きなファイルでもタブをフリーズさせない仕組みです。深掘り記事でそのトレードオフを説明しています。
「印刷 / PDF 保存」を追加する
ブラウザの印刷ダイアログは、表示中の内容を紙や PDF で取得させる最もシンプルな方法です。Pro エディションはシートをクリーンな HTML テーブルとして描画し、ダイアログを開きます:
import { printWorksheet } from '@reogrid/pro';
<button onClick={() => printWorksheet(gridRef.current!.worksheet, {
title: file?.name,
orientation: 'landscape',
})}>
印刷 / PDF 保存
</button>
ダイアログからユーザーは実際のプリンタか「PDF として保存」を選びます。サーバー側レンダリングもヘッドレス Chrome も不要 — いま見ているそのページが対象です。
ビューア用途での Lite と Pro
xlsx の インポートと読み取り専用モードは、どちらも無料の Lite エディションで動くため、上のコアビューアはコストゼロです。知っておくべき点が 1 つ: Lite は 100 行 × 26 列を超えるデータを切り詰めます。既知で範囲の決まったファイルなら問題ありません。しかし汎用ビューアは 任意の ユーザーファイルを受け付けます — 誰かが 5,000 行のエクスポートをドロップした瞬間、Lite は黙ってそれを切り落とします。
自分の管理外のファイルを扱うビューアを作るなら、Pro エディション がサイズ上限を解除し、printWorksheet、109 関数の数式ライブラリ、xlsx の エクスポート を追加します。ビューアのコードは同一で、@reogrid/lite/react を @reogrid/pro/react に差し替えるだけです。
| Lite(無料) | Pro | |
|---|---|---|
| xlsx インポート+描画 | ✅ | ✅ |
| 読み取り専用 / 保護モード | ✅ | ✅ |
| グリッドサイズ | 100 × 26 | 無制限 |
| 印刷 / PDF 保存 | — | ✅ |
| xlsx エクスポート | — | ✅ |
クライアントサイドが正しいデフォルトである理由
サーバーサイドのビューアは、数 MB のアップロード、自社マシンでのパース CPU、一時ファイルのライフサイクル、そして自分で書いて守らねばならないプライバシーの説明を意味します。ユーザーに自分のファイルを見せるだけのために、誰も頼んでいない仕事ばかりです。
クライアントサイドなら、ファイルは端末から出ず、ロード時間はユーザーの CPU だけに制限され、保持・記録・漏洩すべきものが何もありません。「.xlsx を開く」はローカルな操作のまま — それこそ人々がビューアに当然期待することであり、そして「online xlsx viewer」サイトが実際にはほぼ決してやらないことです。
試してみてください: ライブの XLSX ビューアデモ にご自身の .xlsx をドロップし、ネットワークタブが空のままなのを確認してください。次は 編集可能な ツールを作りますか? もう半分の物語は 編集可能な数式付き React 製インボイスを構築する をご覧ください。