Caroa UI

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'

基本的な使い方

Loading...
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 />

検索・ソート付き

ヘッダーをクリックしてソート、検索ボックスでフィルタリングできます。

Loading...
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="名前やメールで検索..."
/>

フィルター付き

フィルターボタンで条件付きフィルタリングができます。

Loading...
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="検索..."
/>

ページネーション付き

Loading...
<DataTable
  columns={columns}
  data={paginatedData}
  pagination={{
    page: 1,
    limit: 10,
    total: 100,
    totalPages: 10,
  }}
  onPageChange={(page) => setPage(page)}
/>

行アクション付き

各行にアクションボタンを追加できます。

Loading...
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} />

カスタムアクション付き

検索ボックスの横にカスタムボタンを追加できます。

Loading...
const customActions = (
  <Button size="sm">
    <Icon name="Plus" size="sm" />
    新規追加
  </Button>
)
 
<DataTable
  columns={columns}
  data={data}
  customActions={customActions}
  searchPlaceholder="検索..."
/>

空状態

データがない場合のカスタム表示。

Loading...
<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>
    ),
  }}
/>

ローディング状態

Loading...
<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}`)}
/>

カラム幅指定

DataTableColumnwidth / minWidth / maxWidth でカラムごとの幅を指定できます。widthは既存のプリセット('xs'|'sm'|'md'|'lg'|'xl'|'auto')に加え、number(px)や任意のCSS幅文字列('40%'等)も指定できます。いずれか1つでも指定した列があるとcolgroupを自動レンダリングします(何も指定しない場合は従来どおり)。

Loading...
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 で下限・上限を指定できます。

Loading...
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

PropTypeDefaultDescription
columns*DataTableColumn[]-カラム定義の配列
data*T[]-テーブルに表示するデータ
filterItemsFilterItem[]-フィルター項目の配列
onFiltersChange(filters: ActiveFilter[]) => void-フィルター変更時のコールバック
searchPlaceholderstring'検索...'検索ボックスのプレースホルダー
onSearchChange(search: string) => void-検索文字列変更時のコールバック
hideSearchbooleanfalse検索ボックスを非表示にする
paginationPaginationInfo-ページネーション設定
onPageChange(page: number) => void-ページ変更時のコールバック
loadingbooleanfalseローディング状態
emptyState{ icon, title, description, action }-データがない場合の表示設定
customActionsReactNode-検索ボックス横のカスタムアクション
onRowClick(item: T) => void-行クリック時のコールバック
rowActions(item: T) => ReactNode-各行のアクションレンダラー
resizableColumnsbooleanfalseヘッダー境界のドラッグ(またはフォーカス後の左右矢印キー)で列幅を変更できるようにする
onColumnWidthsChange(widths: Record<string, number>) => void-列幅が変化した際に、カラムID→px(number)のレコードを通知するコールバック(非制御。保存したい利用側向け)
manualFilteringbooleanfalseサーバー側で検索・カスタムフィルターを適用済みの場合、ローカルでの再適用をスキップする(二重適用防止)
manualSortingbooleanfalseサーバー側でソートを適用済みの場合、ローカルでの再適用をスキップする(二重適用防止)
classNamestring-追加のクラス名

DataTableColumn

PropTypeDefaultDescription
id*string-カラムの一意な識別子
header*string-ヘッダーに表示するテキスト
accessorKeykeyof T-データオブジェクトのキー
cell(data: T) => ReactNode-カスタムセルレンダラー
width'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'auto' | number | string-カラムの幅。プリセットに加え、number(px)や任意のCSS幅文字列(例: '40%')を指定できる
minWidthnumber | string60(resizableColumns使用時)カラムの最小幅(px数値、または px文字列)。resizableColumnsでのリサイズ下限としても使われる
maxWidthnumber | string-カラムの最大幅(px数値、または px文字列)。resizableColumnsでのリサイズ上限としても使われる
sortablebooleanfalseソート可能にする
align'left' | 'center' | 'right''left'テキストの配置

FilterItem

PropTypeDefaultDescription
id*string-フィルターの一意な識別子
label*string-フィルターのラベル
field*keyof T-フィルター対象のフィールド
type'text' | 'email' | 'phone' | 'url' | 'number' | 'date'-フィルタータイプ
iconReactNode-カスタムアイコン
conditionsArray<{ label, condition }>-フィルター条件の配列

PaginationInfo

PropTypeDefaultDescription
page*number-現在のページ番号(1始まり)
limit*number-1ページあたりの件数
total*number-総件数
totalPages*number-総ページ数