data-table について
@virtuoso.dev/data-tableは、柔軟な列設定とローカルまたはリモートのデータを扱うReact向け仮想化データテーブルライブラリです。ヘッドレス版とshadcn/uiラッパーがあり、グループ行やテーブル状態の設定にも対応します。
主な機能
- 01行と列の仮想化
- 02ローカル・リモートデータモデル
- 03グループ行と固定列
- 04列幅変更、並べ替え、表示切替
- 05テーブルとモデル状態の保存
- 06ヘッドレス版とshadcn/ui対応
活用シーン
- 01大規模なReactデータテーブルの表示
- 02API連携テーブルの構築
- 03ユーザーによる列レイアウトのカスタマイズ
- 04セッションをまたいだテーブル設定の保持
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ virtuoso-dev/ skills --skill data-tableインストール後、Claude Code で/data-tableを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/4.grouped-rows.md5.state-persistence.md6.controlling-the-table.mdreferences/1.installation/01.shadcn.md02.headless.mdreferences/2.data-model/01.local-data-model.md02.remote-data-model.md03.row-keys.mdindex.mdreferences/3.columns/01.defining-columns.md02.cell-and-header-renderers.md03.column-groups.md04.sticky-columns.md05.column-visibility.md06.column-resizing.md07.column-reordering.md08.runtime-columns.md09.column-layout.mdindex.mdreferences/7.customization/01.styling.md02.replacing-internals.md03.empty-and-loading-states.md04.scroll-containers.md05.ambient-context.md06.header-slots.md07.shadcn-wrapper.md08.card-mode.mdreferences/8.examples/01.basic-table.md03.grouped-data.md04.sticky-columns.md詳細
@virtuoso.dev/data-table
グループ化された行、固定列、列のサイズ変更/並べ替え/表示切り替え、状態の永続化、リモートデータに対応した、仮想化 React データテーブル(行と列)です。テーブル形式の課題に対応する TableVirtuoso の後継です。行レンダラーではなく、データソース(モデル)を渡し、列を JSX として宣言します。
インストール:2 つの方法
Shadcn(スタイル設定済みのラッパー) — shadcn/ui の規約を使用するプロジェクト向けです。
npx shadcn@latest add petyosi/react-virtuoso/data-table
これにより、DataTable、DataTableColumn、DataTableColumnHeader、DataTableCell をエクスポートするスタイル設定済みのラッパーが @/components/ui/data-table にインストールされます。
オプションの機能 UI パーツは、それぞれ別のレジストリー項目です。
npx shadcn@latest add petyosi/react-virtuoso/data-table-resize-handle
npx shadcn@latest add petyosi/react-virtuoso/data-table-sort-header-button
レジストリー項目名にはハイフンが使われています。インストール後のインポートパスはネストされたままです(@/components/ui/data-table/column-resize、@/components/ui/data-table/column-sort)。data-table/column-resize や data-table/column-sort のようなパス形式の shadcn コマンドは実行しないでください。shadcn が解決するのはレジストリー項目名であり、インポートパスではありません。
ヘッドレス — 独自のデザインシステム向けです。
npm install @virtuoso.dev/data-table
構造用スタイル(@import '@virtuoso.dev/data-table/styles.css')をインポートし、スタイルなしの VirtuosoDataTable、Column、ColumnHeader、Cell を使用します。インストールする前に、どちらの方法がプロジェクトに適しているか確認してください。Tailwind/shadcn のコードベースでは、shadcn ラッパーのほうが手早く始められます。
最小限の例(shadcn ラッパー)
import { DataTable, DataTableCell, DataTableColumn, DataTableColumnHeader } from '@/components/ui/data-table'
import { localModel } from '@virtuoso.dev/data-table'
const model = localModel({ data: products })
export default function App() {
return (
<DataTable model={model} style={{ height: 360 }}>
<DataTableColumn field="name">
<DataTableColumnHeader>Product</DataTableColumnHeader>
<DataTableCell className="font-medium">{({ cellValue }) => String(cellValue)}</DataTableCell>
</DataTableColumn>
</DataTable>
)
}
列は設定オブジェクトではなく、JSX で記述します。field は行データの検索キーであり、列の公開識別子(表示切り替え、並べ替え、永続化で使用)でもあります。すべての Virtuoso コンポーネントと同様、テーブルには実際の高さが必要です。
field と id は、ユーザー向けラベルではなく、安定した列識別子として扱ってください。表示される列にはすべて、明示的な DataTableColumnHeader を追加してください。これは actions のような表示専用の列で特に重要です。製品として意図的にアイコンのみ、またはヘッダーなしの列にする場合を除き、Actions のような表示ラベルを使用してください。その場合は、アクセシビリティのために sr-only テキストを使用します。
テーブルをページ、パネル、カードの残りの高さに合わせる場合は、固定ピクセル高ではなく、サイズを計測するフレックス列を使用してください。計測対象のコンテナーからテーブルまでのフレックス祖先要素にはすべて min-h-0 が必要です。テーブル以外の要素には shrink-0 を指定し、テーブルには style={{ height: '100%' }} を指定して、伸長する子要素にします。
<section className="flex h-full min-h-0 flex-col">
<PageHeader className="shrink-0" />
<div className="flex min-h-0 flex-1 flex-col gap-3">
<Toolbar className="shrink-0" />
<DataTable className="min-h-0 flex-1" model={model} style={{ height: '100%' }}>
{/* columns */}
</DataTable>
</div>
</section>
これは任意です。親要素に明確な高さがない場合は、固定の高さ(style={{ height: 360 }})を引き続き使用するか、useWindowScroll/customScrollParent を意図的に選択してください。
列幅は、ヘッダーの計測を通じてテーブルが管理します。w-*、min-w-*、basis-* などの基本幅クラスは DataTableCell ではなく、DataTableColumnHeader に指定してください。セルはヘッダーの計測値から設定されたトラック内にレンダリングされます。セルの幅ユーティリティを使うと、本文のコンテンツがそのトラックからはみ出し、狭い幅で列が重なることがあります。セルの className は、文字体裁、配置、パディング、切り詰め、色に使用してください。複雑なセルコンテンツには、セルの幅を広げるのではなく、内側のラッパーに min-w-0 を指定してください。
伸長するヘッダーには、計測用と表示用の面が別々にあります。テーブルはまずヘッダーの固有サイズを計測し、その後、grow に応じて余分な幅を追加します。同じ計測対象の要素をレンダリングされた列幅いっぱいに伸ばすと、ResizeObserver が伸長後の幅を次の基本幅として報告するため、ビューポートが狭くなっても列を縮められなくなります。
DataTableColumnHeader.classNameは、固定幅または最小幅の基準幅、パディング、タイポグラフィ、基準幅内での配置など、固有の表示領域に使用します。DataTableColumnHeader.containerPropsは、レンダリングされた列全体に適用する背景、ホバー/フォーカス状態、アウトライン、ボックスシャドウ、カーソル、ARIA 属性、イベントハンドラーなどの描画や操作に使用します。containerPropsでは、width、flex、margin、padding、border、display、justify-*のスタイルを使用しないでください。外側のジオメトリはテーブルが管理します。ここにレイアウトスタイルを指定すると、コンテンツが切り取られたり、HeaderEndやHeaderEdgeのスロットがずれたりすることがあります。全幅の区切り線にはbox-shadowまたはoutlineを使用してください。containerPropsのイベントには、ソートボタン、リサイズハンドル、並べ替えコントロール、その他のネストされた UI からバブリングしたイベントが届きます。列全体に対する操作を実行する前に、ネストされたコントロールで伝播を止めるか、コンテナーのハンドラーに条件を設けてください。
<DataTableColumn field="name" grow={1}>
<DataTableColumnHeader className="min-w-72" containerProps={{ className: 'cursor-pointer hover:bg-muted/50 focus-within:outline' }}>
Name
</DataTableColumnHeader>
<DataTableCell>{NameCell}</DataTableCell>
</DataTableColumn>
全幅のトラックを描画するために、className で w-full や flex の拡張を使用しないでください。これらのスタイルは、設計上、固有の測定境界内で適用されます。全幅の表示には containerProps を使用してください。レンダリングされた列の端に合わせる必要があるコントロールには HeaderEnd を使用してください。
テーブルを作成するときは、表示するデータに応じて、固定列にするか拡張列にするかを選択してください。
- コンパクトな列は
growを省略して固定幅にします。対象は、ID、スラッグ、チェックボックス、アクションメニュー、アイコンボタン、ステータスラベル、バッジ、バージョン、数値、日付、タイムスタンプ、短い列挙値やメタデータの列です。 - テキスト量の多い列は、
DataTableColumn grow={number}を指定して拡張列にします。対象は、名前、タイトル、説明、概要、メッセージ、メモ、パスなど、水平方向のスペースが増えることで一覧性が向上したり、表示の切り詰めが減ったりするコンテンツです。 growには、有限の正の値を指定してください。grow={0}はgrowを省略した場合と同じです。固定列では、生成された設定の形式上、明示的な値が必要な場合を除き、省略してください。- テーブルの内容を理解したうえで選択してください。
name列は拡張することが多いですが、短いコードのような名前なら固定にする場合もあります。label列は固定にすることが多いですが、ユーザーが作成したラベルなら拡張する場合もあります。 - すべての列を拡張しないでください。
growを使うと、コンパクトな列が余ったスペースを占有せず、必要なテキスト列にスペースを割り当てられます。
例:
<DataTableColumn field="name" grow={1}>
<DataTableColumnHeader className="min-w-72">Name</DataTableColumnHeader>
<DataTableCell>{NameCell}</DataTableCell>
</DataTableColumn>
<DataTableColumn field="description" grow={3}>
<DataTableColumnHeader className="min-w-80">Description</DataTableColumnHeader>
<DataTableCell>{DescriptionCell}</DataTableCell>
</DataTableColumn>
<DataTableColumn id="actions">
<DataTableColumnHeader className="w-16 justify-center">Actions</DataTableColumnHeader>
<DataTableCell>{ActionsCell}</DataTableCell>
</DataTableColumn>
データモデルの選び方
| 状況 | モデル |
|---|---|
| メモリ上の行をクライアント側でフィルタリング/ソート/グループ化する | localModel({ data, pipeline?, actions?, groups? }) |
| API ベースで、合計件数が既知(取得中はプレースホルダー行を表示) | オフセットモードの remoteModel(defaultOffsetViewportHandler);fetch は { rows, totalCount } を返します |
| API ベースで、カーソルページネーション/無限追加を行う | 追加モードの remoteModel(defaultAppendViewportHandler);fetch は { rows } を返します |
遅延初期化を使って useState にモデルを保持します — const [model] = useState(() => localModel({ data }))。useMemo は使用しないでください。React はメモ化された値を破棄する場合があり、モデルのインスタンスを保持する必要があります。静的なシングルトンテーブルであれば、モジュールスコープでも問題ありません。
ローカルのフィルタリング/ソート/グループ化は、名前付きステージのパイプラインを通じて実行します。pipeline: ['filter', 'sort'] と actions を宣言し、model.send({ action: 'filter', payload }) でディスパッチします。local-data-model と local-filter-sort-group example を参照してください。
リモートのソート/フィルタリング/検索コントロールでは、アクションのペイロードを React の状態に複製せず、モデル内に保持します。initialActions でデフォルト値を設定し、model.send() または dispatchModelAction$ で変更をディスパッチして、modelActionState$ を読み取ってアクティブなコントロールを描画します。これは、modelStatePersistenceAdapter() が保存済みのアクション状態を復元する場合に特に重要です。
行の順序が変わる可能性がある場合(ソート、フィルタリング、リモート更新)は、computeRowKey={({ data }) => data.id} を指定してください。指定しない場合、行が再マウントされ、ローカル状態が失われます。
カラム機能
- 固定カラム:
<DataTableColumn field="name" sticky="left" />(または"right")。複数の固定カラムは積み重なります。 - 表示/非表示:
visible={false}で宣言的に設定するか、@virtuoso.dev/data-table/column-visibilityのsetColumnVisibility$/columnVisibilityState$を使って実行時に設定します。 - サイズ変更:
HeaderEdgeスロットにResizeHandleを配置します。プログラムからは@virtuoso.dev/data-table/column-resizeのresizeColumn$を使用します。 - 並べ替え:
HeaderStartにReorderGrip、HeaderOverlayにReorderDropZoneを配置します。プログラムからは@virtuoso.dev/data-table/column-reorderのreorderColumns$を使用します。 - ヘッダースロット:
HeaderStart(ラベルの前)、HeaderEnd(ラベルの後)、HeaderEdge(カラム境界に固定)、HeaderOverlay(ヘッダーを覆う) — header-slots を参照してください。 - グループ化された行:
dataと併せてgroups: [{ index, level }]を渡し、GroupHeaderCellでヘッダーを描画します — grouped-rows を参照してください。
状態の永続化:機能サブパスから取得したアダプター(columnVisibilityPersistenceAdapter()、columnOrderPersistenceAdapter()、columnWidthPersistenceAdapter()、modelStatePersistenceAdapter())を指定して、<DataTableStatePersistence adapters={[...]} storageKey="my-table" /> を配置します。state-persistence を参照してください。
テーブルを外部から制御する
テーブルの状態は内部のリアクティブエンジンに保持されます。このパッケージでは、外部から制御できるよう、セル(読み取り可能な状態、末尾に $ が付く)とストリーム(アクション)を意図的にエクスポートしています。状態を props に引き上げることはありません。
import { scrollLocation$, scrollToRow$, useEngineRef, useRemoteCellValue, useRemotePublisher } from '@virtuoso.dev/data-table'
const engineRef = useEngineRef()
const scrollToRow = useRemotePublisher(scrollToRow$, engineRef)
const location = useRemoteCellValue(scrollLocation$, engineRef)
<DataTable engineRef={engineRef} model={model}>...</DataTable>
<button onClick={() => scrollToRow({ index: 100, align: 'start' })}>Go to row 100</button>
テーブルから離れた場所にある UI から操作するには、engineId="orders-table" を渡し、文字列 ID を使って同じフックを呼び出します。便利なノードは次のとおりです。scrollToRow$、scrollIntoView$、setColumnVisibility$、dispatchModelAction$(アクション)、scrollLocation$、columns$、columnVisibilityState$、modelActionState$、loadingState$(状態)。controlling-the-table を参照してください。
カスタマイズ
- スタイル設定には、ラッパーコンポーネントの
classNameとセマンティックなデータ属性を使用してください。スタイル設定のフックとしてdata-testidは使用しないでください。 - 列ヘッダーでは、固有のサイズ設定とコンテンツのレイアウトに
classNameを使用し、描画された列全体の見た目とインタラクションにはcontainerPropsを使用してください。レイアウトを変更するスタイルはcontainerPropsに設定しないでください。 - shadcn ラッパーは、
DataTableにアプリレベルのテーブルフレーム(rounded-md border)をすでに描画します。各テーブルインスタンスにrounded-md borderを追加しないでください。classNameは、rounded-xl、border-0、border-2、シャドウ、テーブル変数の上書きなど、意図的にフレームを変更する場合にのみ使用してください。 - shadcn ラッパーはテーブルレベルの CSS 変数(
--data-table-bg、--data-table-fg、--data-table-border、--data-table-muted、--data-table-muted-fg、--data-table-row-hover、--data-table-sticky-hover)を公開し、固定ヘッダー、固定列、行、読み込み時の表示に使用します。ホストのデザインシステムに合わせる場合は、固定セルごとにスタイルを設定するのではなく、DataTableまたはコピーしたラッパーのデフォルト値でこれらの変数を一度だけ上書きしてください。 componentsprop で内部コンポーネントを置き換えます。Row、StickyColumnContainer、LoadingPlaceholder、LoadingOverlay、LoadingFooterを指定できます(コンポーネントの上書きでは ref を転送する必要があります)。トップレベルのコンポーネントはEmptyPlaceholder、ScrollElementです。context={{ ... }}はcomputeRowKey、EmptyPlaceholder、読み込み用スロット、コンポーネントの上書きに渡されますが、セルやヘッダーのレンダラーには渡されません(その場合は React context を使用してください)。ambient-context を参照してください。- スクロールモードは、デフォルトの内部スクローラー、
useWindowScroll、customScrollParentのいずれかを選択してください。必ず一つだけ選んでください。
TableVirtuoso からの移行
| TableVirtuoso | data-table |
|---|---|
data 配列 | model として渡す localModel({ data }) |
itemContent | DataTableColumn + DataTableCell |
fixedHeaderContent | DataTableColumnHeader |
| グループ化された行 | groups + GroupHeaderCell |
| 固定列(CSS) | sticky="left" / sticky="right" |
ref + scrollToIndex | engineRef + scrollToRow$ |
rangeChanged | onRenderedDataChange、viewportRange$、scrollLocation$ |
詳しいガイド:migrating-from-table-virtuoso。
トラブルシューティング
| 症状 | 対処 |
|---|---|
| テーブルが空白、またはヘッダーだけ表示される | テーブルに測定可能な高さを指定します |
| テーブルがパネル全体に広がらない | オプションのフレックス高さパターンを使用します。親要素に測定可能な高さを設定し、祖先要素には min-h-0 を指定し、枠となる要素には shrink-0 を指定して、DataTable に flex-1 と height: '100%' を設定します |
| Shadcn コンポーネントのインポートに失敗する | レジストリのインストールを実行するか、@virtuoso.dev/data-table からヘッドレスコンポーネントをインポートします |
Shadcn が文字どおりの @/ ディレクトリを作成する | Shadcn が読み取るルート設定からエイリアスを解決できるようにします。ソリューション形式の tsconfig を使用する場合は、ルート tsconfig.json に @/* のパスを複製します |
| ページとテーブルの両方がスクロールする | スクロールモードは1つだけ使用します |
| リモートの行が表示されない | 正しい取得形式(オフセットモードでは { rows, totalCount })を返し、signal をそのまま渡します |
| ソート後に行が再マウントされる/状態が失われる | computeRowKey を追加します |
| 本文セルが列に重なる | 幅のクラスを DataTableCell から DataTableColumnHeader に移動します。幅を追加するには DataTableColumn grow={...} を使用します |
| 全幅ヘッダーのスタイルでコンテンツが切り取られる | レイアウトはヘッダーの className に指定します。containerProps は、パディング、境界線、幅、フレックス、配置を指定せず、描画と操作にのみ使用します |
| アクション/表示列にヘッダーがない | Actions など、明示的に表示される DataTableColumnHeader ラベルを追加します。field/id は識別子であり、UI ラベルではありません |
| 外側の枠線/フレームが二重になる | 呼び出し元の rounded-md border を削除します。デフォルトのテーブルフレームは shadcn ラッパーがすでに管理しています |
| 固定列/ヘッダーの色が本文と一致しない | DataTable 上、またはコピーしたラッパーのデフォルト値で、shadcn ラッパーの --data-table-* 変数を上書きします |
| 固定列が切り取られる | 親要素の overflow とヘッダーの最小幅を確認します |
| 横スクロール時に空のセルがちらつく | columnOverscanCount を増やします |
shadcn テーブルをリリースする前に、セルに幅のユーティリティが使われていないか grep で確認し、ヘッダーに移動します。
rg 'DataTableCell.*className=.*(w-|min-w|max-w|basis-|grow|shrink|flex-(none|auto|initial|1|\[))'
テストでは、VirtuosoDataTableTestingContext.Provider value={{ itemHeight, viewportHeight }} でラップします(JSDOM にはレイアウトがありません)。また、DOM の行数そのものではなく、動作を検証してください。オーバースキャンによって追加の行がレンダリングされます。固定列、サイズ変更、ドラッグ操作には実ブラウザーでのテストを使用します。テストを参照してください。
参考資料
- references/README.md — 概要
references/1.installation/— shadcn、ヘッドレスreferences/2.data-model/— ローカル、リモート、行キーreferences/3.columns/— 列の定義、セルとヘッダーのレンダラー、列グループ、固定列、表示設定、サイズ変更、並べ替え、実行時の列、列レイアウト- 機能:グループ化された行、状態の永続化、テーブルの制御
references/7.customization/— スタイル設定、内部実装の置き換え、空の状態と読み込み中の状態、スクロールコンテナー、アンビエントコンテキスト、ヘッダースロット、shadcn ラッパーreferences/8.examples/— 基本的なテーブルからリモートページネーション、ダッシュボード、永続化までの実例references/9.guides/— テスト、パフォーマンス、トラブルシューティング、table-virtuoso からの移行、デバッグ用インストルメンテーション
API リファレンス全文:https://virtuoso.dev/data-table/