DataTable
ソート・フィルタ・検索・ページネーション対応のデータテーブル。
インポート
'use client'
import { DataTable } from '@caroainc/ui-components/client'
import type { DataTableColumn, FilterItem } from '@caroainc/ui-components/client'個別サブパス(バンドルサイズを抑えたい場合):
'use client'
import { DataTable } from '@caroainc/ui-components/data-table'
import type { DataTableColumn, FilterItem } from '@caroainc/ui-components/data-table'Composition
DataTable(childrenなし)
├─ columns: DataTableColumn[](必須)
├─ data: T[](必須)
├─ filterItems? / customActions?(ツールバー)
├─ rowActions?(各行)
└─ pagination?(フッター)DataTableはサブコンポーネントを子に並べる型ではなく、必須のcolumnsとdata、任意の機能propsで構成します。独自セルはcolumns[].cell、行操作はrowActionsへ渡し、DataTableのchildrenとして誤ってネストしないでください。
基本的な使い方
interface User {
id: number
name: string
email: string
role: string
status: string
}
const columns: DataTableColumn<User>[] = [
{ id: 'name', header: '名前', accessorKey: 'name', sortable: true },
{ id: 'email', header: 'メール', accessorKey: 'email' },
{ id: 'role', header: '役割', accessorKey: 'role' },
{
id: 'status',
header: 'ステータス',
accessorKey: 'status',
cell: (row) => <Badge variant="success">{row.status}</Badge>,
},
]
<DataTable columns={columns} data={data} hideSearch />検索・ソート付き
ヘッダーをクリックしてソート、検索ボックスでフィルタリングできます。
const columns: DataTableColumn<User>[] = [
{ id: 'name', header: '名前', accessorKey: 'name', sortable: true },
{ id: 'email', header: 'メール', accessorKey: 'email', sortable: true },
{ id: 'role', header: '役割', accessorKey: 'role', sortable: true },
{ id: 'status', header: 'ステータス', accessorKey: 'status' },
]
<DataTable
columns={columns}
data={data}
searchPlaceholder="名前やメールで検索..."
/>フィルター付き
フィルターボタンで条件付きフィルタリングができます。
const filterItems: FilterItem<User>[] = [
{
id: 'status',
field: 'status',
label: 'ステータス',
type: 'text',
conditions: [
{ label: '有効', condition: { type: 'equals', value: 'active' } },
{ label: '無効', condition: { type: 'equals', value: 'inactive' } },
{ label: '保留中', condition: { type: 'equals', value: 'pending' } },
],
},
{
id: 'role',
field: 'role',
label: '役割',
type: 'text',
conditions: [
{ label: '管理者', condition: { type: 'equals', value: '管理者' } },
{ label: '編集者', condition: { type: 'equals', value: '編集者' } },
{ label: 'メンバー', condition: { type: 'equals', value: 'メンバー' } },
],
},
]
<DataTable
columns={columns}
data={data}
filterItems={filterItems}
searchPlaceholder="検索..."
/>ページネーション付き
<DataTable
columns={columns}
data={paginatedData}
pagination={{
page: 1,
limit: 10,
total: 100,
totalPages: 10,
}}
onPageChange={(page) => setPage(page)}
/>行アクション付き
各行にアクションボタンを追加できます。
const rowActions = (row: User) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" shape="circle">
<Icon name="MoreHorizontal" size="sm" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>
<Icon name="Eye" size="sm" />
詳細を見る
</DropdownMenuItem>
<DropdownMenuItem>
<Icon name="Pencil" size="sm" />
編集
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-destructive">
<Icon name="Trash2" size="sm" />
削除
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
<DataTable columns={columns} data={data} rowActions={rowActions} />カスタムアクション付き
検索ボックスの横にカスタムボタンを追加できます。
const customActions = (
<Button size="sm">
<Icon name="Plus" size="sm" />
新規追加
</Button>
)
<DataTable
columns={columns}
data={data}
customActions={customActions}
searchPlaceholder="検索..."
/>空状態
データがない場合のカスタム表示。
<DataTable
columns={columns}
data={[]}
emptyState={{
icon: <Icon name="Users" size="xl" className="text-muted-foreground mb-4" />,
title: 'ユーザーがいません',
description: '新しいユーザーを追加してください',
action: (
<Button>
<Icon name="Plus" size="sm" />
ユーザーを追加
</Button>
),
}}
/>ローディング状態
<DataTable columns={columns} data={[]} loading />行選択(チェックボックス)とonRowClickの併用
rowKey + selectedKeys + onSelectionChange を指定するとチェックボックス列が表示され、行選択に対応します。行クリックの挙動はonRowClickの有無で変わります。
| 状態 | 行本体のクリック | チェックボックス |
|---|---|---|
selectableのみ(onRowClick無し) | 選択をトグル | 選択をトグル |
selectable + onRowClickあり | onRowClickのみを呼び出す(選択はトグルしない) | 選択をトグル(行クリックへ伝播しない) |
onRowClickのみ(selectable無し) | onRowClickのみを呼び出す | - |
| どちらも無し | 何も起きない(cursor-pointerも付与されない) | - |
selectable + onRowClickを併用する場合、チェックボックスのクリックは行クリックへ伝播せず、互いに干渉しません。行が選択・クリック操作可能なとき(selectableまたはonRowClickのいずれか)はキーボード操作(Tab移動 + Enter/Space)にも対応します。選択中の行にはdata-state="selected"が付与され、Tableの標準スタイル(data-[state=selected]:bg-muted)で表示されます。
const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set())
// 行クリックで選択をトグル(onRowClickを指定しない)
<DataTable
columns={columns}
data={data}
rowKey={(row) => row.id}
selectedKeys={selectedKeys}
onSelectionChange={setSelectedKeys}
/>
// 行クリックで遷移、選択はチェックボックスのみで行う
<DataTable
columns={columns}
data={data}
rowKey={(row) => row.id}
selectedKeys={selectedKeys}
onSelectionChange={setSelectedKeys}
onRowClick={(row) => router.push(`/users/${row.id}`)}
/>カラム幅指定
DataTableColumn の width / minWidth / maxWidth でカラムごとの幅を指定できます。widthは既存のプリセット('xs'|'sm'|'md'|'lg'|'xl'|'auto')に加え、number(px)や任意のCSS幅文字列('40%'等)も指定できます。いずれか1つでも指定した列があるとcolgroupを自動レンダリングします(何も指定しない場合は従来どおり)。
const columns: DataTableColumn<User>[] = [
{ id: 'name', header: '名前', accessorKey: 'name', width: 120, minWidth: 80 },
{ id: 'email', header: 'メール', accessorKey: 'email', width: '40%' },
{ id: 'role', header: '役割', accessorKey: 'role', width: 100 },
{
id: 'status',
header: 'ステータス',
accessorKey: 'status',
width: 120,
maxWidth: 160,
cell: (row) => <Badge variant="success">{row.status}</Badge>,
},
]
<DataTable columns={columns} data={data} hideSearch />列幅のドラッグリサイズ(resizableColumns)
resizableColumns を指定すると、ヘッダー境界にドラッグ用ハンドルが表示され、列幅を変更できます。キーボード操作(フォーカス後に左右矢印キー、16pxずつ)にも対応しています。幅は内部で管理される非制御実装で、onColumnWidthsChange で変更後の幅(カラムID→px)を受け取れます。minWidth(デフォルト60px)/ maxWidth で下限・上限を指定できます。
const [widths, setWidths] = useState<Record<string, number>>({})
const columns: DataTableColumn<User>[] = [
{ id: 'name', header: '名前', accessorKey: 'name', width: 140, minWidth: 80 },
{ id: 'email', header: 'メール', accessorKey: 'email', width: 220, minWidth: 120 },
{ id: 'role', header: '役割', accessorKey: 'role', width: 100, minWidth: 60, maxWidth: 200 },
{ id: 'status', header: 'ステータス', accessorKey: 'status', width: 120 },
]
<DataTable
columns={columns}
data={data}
resizableColumns
onColumnWidthsChange={setWidths}
/>ドラッグリサイズが不要な静的な幅指定のみでよい場合は、resizableColumnsを省略しwidth/minWidth/maxWidthだけを指定してください。
サーバー駆動のフィルタ・ソート(manualFiltering / manualSorting)
サーバーAPI側で検索・フィルタ・ソートを適用したdataを渡す場合、DataTableはデフォルトでもローカル側に同じ処理を再適用してしまいます(二重適用)。manualFiltering / manualSortingを指定すると、該当するローカル処理をスキップし、渡されたdataをそのまま表示します。検索欄・フィルターUI・ヘッダークリックのUI自体は変わらず動作し、onSearchChange / onFiltersChange / onSortChangeでサーバーへリクエストを送る使い方を想定しています。
const [data, setData] = useState<User[]>([])
const [sort, setSort] = useState<DataTableSortType<'name'>>({ field: 'name', order: 'asc' })
const fetchData = async (search: string, sort: DataTableSortType<'name'>) => {
const res = await api.getUsers({ search, sortField: sort.field, sortOrder: sort.order })
setData(res.items)
}
<DataTable
columns={columns}
data={data}
manualFiltering
manualSorting
onSearchChange={(search) => fetchData(search, sort)}
onSortChange={(newSort) => { setSort(newSort); fetchData('', newSort) }}
/>Props
DataTable
| Prop | Type | Default | Description |
|---|---|---|---|
columns* | DataTableColumn[] | - | カラム定義の配列 |
data* | T[] | - | テーブルに表示するデータ |
filterItems | FilterItem[] | - | フィルター項目の配列 |
onFiltersChange | (filters: ActiveFilter[]) => void | - | フィルター変更時のコールバック |
searchPlaceholder | string | '検索...' | 検索ボックスのプレースホルダー |
onSearchChange | (search: string) => void | - | 検索文字列変更時のコールバック |
hideSearch | boolean | false | 検索ボックスを非表示にする |
pagination | PaginationInfo | - | ページネーション設定 |
onPageChange | (page: number) => void | - | ページ変更時のコールバック |
loading | boolean | false | ローディング状態 |
emptyState | { icon, title, description, action } | - | データがない場合の表示設定 |
customActions | ReactNode | - | 検索ボックス横のカスタムアクション |
onRowClick | (item: T) => void | - | 行クリック時のコールバック |
rowActions | (item: T) => ReactNode | - | 各行のアクションレンダラー |
resizableColumns | boolean | false | ヘッダー境界のドラッグ(またはフォーカス後の左右矢印キー)で列幅を変更できるようにする |
onColumnWidthsChange | (widths: Record<string, number>) => void | - | 列幅が変化した際に、カラムID→px(number)のレコードを通知するコールバック(非制御。保存したい利用側向け) |
manualFiltering | boolean | false | サーバー側で検索・カスタムフィルターを適用済みの場合、ローカルでの再適用をスキップする(二重適用防止) |
manualSorting | boolean | false | サーバー側でソートを適用済みの場合、ローカルでの再適用をスキップする(二重適用防止) |
className | string | - | 追加のクラス名 |
DataTableColumn
| Prop | Type | Default | Description |
|---|---|---|---|
id* | string | - | カラムの一意な識別子 |
header* | string | - | ヘッダーに表示するテキスト |
accessorKey | keyof T | - | データオブジェクトのキー |
cell | (data: T) => ReactNode | - | カスタムセルレンダラー |
width | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'auto' | number | string | - | カラムの幅。プリセットに加え、number(px)や任意のCSS幅文字列(例: '40%')を指定できる |
minWidth | number | string | 60(resizableColumns使用時) | カラムの最小幅(px数値、または px文字列)。resizableColumnsでのリサイズ下限としても使われる |
maxWidth | number | string | - | カラムの最大幅(px数値、または px文字列)。resizableColumnsでのリサイズ上限としても使われる |
sortable | boolean | false | ソート可能にする |
align | 'left' | 'center' | 'right' | 'left' | テキストの配置 |
FilterItem
| Prop | Type | Default | Description |
|---|---|---|---|
id* | string | - | フィルターの一意な識別子 |
label* | string | - | フィルターのラベル |
field* | keyof T | - | フィルター対象のフィールド |
type | 'text' | 'email' | 'phone' | 'url' | 'number' | 'date' | - | フィルタータイプ |
icon | ReactNode | - | カスタムアイコン |
conditions | Array<{ label, condition }> | - | フィルター条件の配列 |
PaginationInfo
| Prop | Type | Default | Description |
|---|---|---|---|
page* | number | - | 現在のページ番号(1始まり) |
limit* | number | - | 1ページあたりの件数 |
total* | number | - | 総件数 |
totalPages* | number | - | 総ページ数 |