SONAX Application UI

DataGrid

React

Demos

DataGrid (Advanced)

The DataGrid provides advanced table functionality including filtering, sorting, pagination, row selection, and CSV export.

import { DataGrid, Badge, ButtonIcon, Text, Button, Stack, Tooltip, Notification, spacing } from 'sxa-ui'
import { createColumnHelper } from '@tanstack/react-table'
import {
  RiArrowRightSLine,
  RiDeleteBinLine,
  RiDownloadCloudLine,
  RiInformationLine,
  RiImageLine,
} from '@remixicon/react'
import { useState } from 'react'

// Generate sample data
const generateSampleData = (count = 100) => {
  const statuses = ['ACTIVE', 'PENDING', 'BLOCKED', 'INACTIVE']
  const firstNames = ['John', 'Jane', 'Bob', 'Alice', 'Charlie', 'Diana', 'Frank', 'Grace']
  const lastNames = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis']
  const departments = ['Engineering', 'Marketing', 'Sales', 'HR', 'Finance']
  const categories = ['Frontend', 'Backend', 'DevOps', 'Design', 'Product']
  const thumbnails = [
    'https://placehold.co/150x150/4F46E5/FFFFFF?text=User+1',
    'https://placehold.co/150x150/059669/FFFFFF?text=User+2',
    'https://placehold.co/150x150/DC2626/FFFFFF?text=User+3',
    'https://placehold.co/150x150/EA580C/FFFFFF?text=User+4',
  ]

  return Array.from({ length: count }, (_, index) => ({
    id: index + 1,
    thumbnail: thumbnails[Math.floor(Math.random() * thumbnails.length)],
    firstName: firstNames[Math.floor(Math.random() * firstNames.length)],
    lastName: lastNames[Math.floor(Math.random() * lastNames.length)],
    email: `user${index + 1}@example.com`,
    status: statuses[Math.floor(Math.random() * statuses.length)],
    department: departments[Math.floor(Math.random() * departments.length)],
    categories: categories.filter(() => Math.random() > 0.5).slice(0, 3),
    age: Math.floor(Math.random() * 40) + 22,
    salary: Math.floor(Math.random() * 80000) + 40000,
    isActive: Math.random() > 0.3,
    joinedDate: new Date(Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000),
    isPriority: Math.random() > 0.7,
    skills: Math.floor(Math.random() * 10) + 1,
  }))
}

function statusToBadgeState(status) {
  switch (status) {
    case 'ACTIVE':
      return Badge.states.success
    case 'PENDING':
      return Badge.states.info
    case 'BLOCKED':
      return Badge.states.error
    case 'INACTIVE':
      return Badge.states.disabled
    default:
      return Badge.states.disabled
  }
}

export default function DataGridDefault() {
  const data = generateSampleData(50)
  const columnHelper = createColumnHelper()
  const [downloadLoading, setDownloadLoading] = useState(false)
  const [downloadNotification, setDownloadNotification] = useState(false)

  const columns = [
    // Thumbnail column with responsive images
    columnHelper.accessor('thumbnail', {
      id: 'thumbnail',
      header: 'Avatar',
      meta: {
        cellType: DataGrid.cellTypes.thumbnail,
        cardType: DataGrid.cardTypes.thumbnail,
      },
      enableSorting: false,
      enableColumnFilter: false,
      cell: ({ getValue, table }) => {
        const value = getValue()
        const layout = table.getState().layout

        let imageSize = 48
        if (layout === DataGrid.layouts.grid) {
          imageSize = 120
        }

        return (
          <Stack justifyContent={Stack.justifyContent.center} utilClassNames='sxa-w-full'>
            <img
              loading='lazy'
              src={value || 'https://via.placeholder.com/150x150/6B7280/FFFFFF?text=No+Image'}
              style={{
                width: imageSize,
                height: imageSize,
                aspectRatio: '1/1',
                objectFit: 'cover',
                borderRadius: '50%',
              }}
              alt='User avatar'
            />
          </Stack>
        )
      },
    }),

    // Full name with title card type
    columnHelper.accessor((row) => `${row.firstName} ${row.lastName}`, {
      id: 'fullName',
      header: 'Full Name',
      meta: {
        csvHeader: 'Full Name',
        cardType: DataGrid.cardTypes.title,
      },
      cell: ({ row }) => `${row.original.firstName} ${row.original.lastName}`,
    }),

    columnHelper.accessor('email', {
      header: 'Email',
      meta: {
        csvHeader: 'Email Address',
        filterVariant: 'stringlist',
        filterPlaceholder: 'Enter emails separated by commas',
      },
      filterFn: 'arrIncludesSome',
    }),

    // Status with badges card type
    columnHelper.accessor('status', {
      header: 'Status',
      cell: ({ getValue }) => <Badge label={getValue()} state={statusToBadgeState(getValue())} />,
      meta: {
        filterVariant: 'multiselect',
        csvHeader: 'Status',
        csvValueFn: (value) => value,
        cardType: DataGrid.cardTypes.badges,
      },
      enableGlobalFilter: false,
    }),

    // Priority with boolean filter
    columnHelper.accessor('isPriority', {
      header: 'Priority',
      cell: ({ getValue }) => (getValue() ? <Badge label='High Priority' state={Badge.states.error} /> : <></>),
      meta: {
        filterVariant: 'boolean',
        csvHeader: 'Is Priority',
        csvValueFn: (value) => (value ? 'Yes' : 'No'),
        cardType: DataGrid.cardTypes.badges,
      },
      enableGlobalFilter: false,
    }),

    // Categories with multiselect filter
    columnHelper.accessor('categories', {
      header: 'Categories',
      cell: ({ getValue, table }) => {
        const categories = getValue() || []
        const layout = table.getState().layout
        const text = categories.join(', ')

        if (layout === DataGrid.layouts.grid) {
          return <>{text}</>
        }

        return (
          <Text
            title={text}
            style={{
              maxWidth: 150,
              overflow: 'hidden',
              textOverflow: 'ellipsis',
              whiteSpace: 'nowrap',
            }}
          >
            {text}
          </Text>
        )
      },
      meta: {
        filterVariant: 'multiselect',
        csvHeader: 'Categories',
        csvValueFn: (value) => (value || []).join(', '),
      },
      filterFn: 'arrIncludesSome',
      enableGlobalFilter: false,
    }),

    columnHelper.accessor('department', {
      header: 'Department',
      meta: {
        filterVariant: 'select',
        csvHeader: 'Department',
      },
    }),

    // Date column with custom formatting
    columnHelper.accessor('joinedDate', {
      header: 'Joined Date',
      sortingFn: 'datetime',
      enableColumnFilter: false,
      enableGlobalFilter: false,
      meta: {
        csvHeader: 'Joined Date',
        csvValueFn: (value) => new Date(value).toLocaleDateString(),
      },
      cell: ({ getValue }) => <DataGrid.CellText>{new Date(getValue()).toLocaleDateString()}</DataGrid.CellText>,
    }),

    columnHelper.accessor('age', {
      header: 'Age',
      meta: { csvHeader: 'Age' },
    }),

    // Actions column with complex header including tooltip
    columnHelper.display({
      id: 'actions',
      header: () => (
        <Stack alignItems={Stack.alignItems.center} gap={spacing.xxs}>
          <DataGrid.CellText>Actions</DataGrid.CellText>
          <Tooltip
            trigger={<ButtonIcon tiny variant={ButtonIcon.variants.ghost} icon={RiInformationLine} />}
            alignment={Tooltip.alignments.top}
          >
            Available actions for each user
          </Tooltip>
        </Stack>
      ),
      cell: ({ row, table }) => {
        const layout = table.getState().layout

        if (layout === DataGrid.layouts.grid) {
          return (
            <Button
              label='View Profile'
              href={`#user/${row.original.id}`}
              iconEnd={RiArrowRightSLine}
              variant={Button.variants.secondary}
              utilClassNames='sxa-w-full'
            />
          )
        }

        return (
          <Stack direction={Stack.directions.horizontal} gap={spacing.xxs}>
            <ButtonIcon
              variant={ButtonIcon.variants.ghost}
              icon={RiArrowRightSLine}
              onClick={() => alert(`Edit user ${row.original.firstName}`)}
              title='Edit user'
            />
            <ButtonIcon
              variant={ButtonIcon.variants.ghost}
              icon={RiDownloadCloudLine}
              onClick={() => alert(`Download data for ${row.original.firstName}`)}
              title='Download user data'
            />
          </Stack>
        )
      },
      meta: {
        cellType: DataGrid.cellTypes.alignRight,
        cardType: DataGrid.cardTypes.item,
        style: {
          width: 'var(--column-width-actions)',
          minWidth: 'var(--column-width-actions)',
        },
      },
    }),
  ]

  const handleSelectionChange = ({ selectedRows }) => {
    console.log('Selected rows:', selectedRows)
  }

  const handleBulkDownload = async (type, table) => {
    try {
      setDownloadLoading(type)
      const selectedRows = table.getSelectedRowModel().rows

      // Simulate API call
      await new Promise((resolve) => setTimeout(resolve, 2000))

      setDownloadNotification('success')
      console.log(`Bulk ${type} download for ${selectedRows.length} items`)
    } catch (error) {
      setDownloadNotification('error')
      console.error('Download failed:', error)
    } finally {
      setDownloadLoading(false)
    }
  }

  const bulkActions = (table) => (
    <>
      <Button
        label='Export CSV'
        iconEnd={RiDownloadCloudLine}
        variant={Button.variants.secondary}
        disabled={downloadLoading}
        loading={downloadLoading === 'csv'}
        onClick={() => handleBulkDownload('csv', table)}
      />
      <Button
        label='Bulk Email'
        iconEnd={RiDownloadCloudLine}
        variant={Button.variants.secondary}
        disabled={downloadLoading}
        loading={downloadLoading === 'email'}
        onClick={() => handleBulkDownload('email', table)}
      />
      <ButtonIcon
        variant={ButtonIcon.variants.destructive}
        icon={RiDeleteBinLine}
        onClick={() => {
          const selectedRows = table.getSelectedRowModel().rows
          alert(`Delete ${selectedRows.length} users?`)
        }}
        title='Delete selected users'
      />
    </>
  )

  return (
    <>
      {downloadNotification && (
        <>
          {downloadNotification === 'success' && (
            <Notification
              state={Notification.states.info}
              title='Download Successful'
              closable
              onClose={() => setDownloadNotification(false)}
              style={{ marginBottom: 16 }}
            >
              <Text>Your download has been completed successfully.</Text>
            </Notification>
          )}

          {downloadNotification === 'error' && (
            <Notification
              state={Notification.states.danger}
              title='Download Failed'
              closable
              onClose={() => setDownloadNotification(false)}
              style={{ marginBottom: 16 }}
            >
              <Text>An error occurred during download. Please try again later.</Text>
            </Notification>
          )}
        </>
      )}

      <div style={{ height: '600px' }}>
        <DataGrid
          data={data}
          columns={columns}
          t={
            typeof window !== 'undefined'
              ? window.t
              : (key) => {
                  const translations = {
                    'table.filter.filter_cta': 'Filter',
                    'search.placeholder': 'Search...',
                    'table.download.label': 'Download',
                    'form.cancel': 'Cancel',
                    'table.csv_download.select_all': 'Select All',
                    'table.filter.placeholder': 'Select filter',
                    'table.filter.boolean_true': 'Yes',
                    'table.filter.boolean_false': 'No',
                    'table.filter.stringlist_csv_cta': 'Upload CSV',
                    'table.filter.stringlist_csv_example_cta': 'Download Example',
                    'table.filter.add_filter_cta': 'Add Filter',
                    'table.filter.close_filter_cta': 'Close',
                    'table.selection.select_all_cta': 'Select All',
                    'table.sort.placeholder': 'Sort by...',
                    'table.sorting.asc': '(ascending)',
                    'table.sorting.desc': '(descending)',
                    'table.empty.title': 'No data available',
                    'table.empty.copy': 'Try adjusting your filters.',
                    'table.selection.rows_selected': 'rows selected',
                    'table.pagination.page_size_label': 'Show',
                    'table.pagination.page_size_all_label': 'All',
                    'table.pagination.rows_off_total_rows': 'of',
                    'table.change_view': 'Change View',
                  }
                  return translations[key] || key
                }
          }
          style={{
            '--table-body-row-height': '64px',
            '--column-width-actions': 'calc(120px + var(--space-xxs) * 2)',
          }}
          filters={true}
          csvDownload='users-comprehensive-export.csv'
          rowSelection={true}
          layoutChangeable={true}
          onSelectionChange={handleSelectionChange}
          bulkActions={bulkActions}
          paginationConfig={{
            pageIndex: 0,
            pageSize: 10,
            pageSizeOptions: [5, 10, 20, 50],
          }}
        />
      </div>
    </>
  )
}

DataGrid Props

PropTypeDefaultDescription
dataArray[]Array of row data objects
columnsArray-TanStack Table column definitions
tFunction-Required Translation function for internationalization
filtersBooleanfalseEnable advanced filtering UI
csvDownloadBoolean|StringfalseEnable CSV download. If string, used as filename
rowSelectionBooleanfalseEnable row selection with checkboxes
layoutChangeableBooleanfalseAllow switching between table and grid layouts
onSelectionChangeFunction-Callback when row selection changes
bulkActionsFunction-Function returning bulk action components
actionsFunction-Function returning action components
paginationConfigObject-Pagination configuration

Advanced Features

The DataGrid component supports a comprehensive set of features for enterprise-level data management:

Column Types and Styling
  • Thumbnail columns: Display images with responsive sizing using cellType: DataGrid.cellTypes.thumbnail
  • Custom cell styling: Apply specific width and styling via meta.style properties
  • Cell alignment: Right-align cells using cellType: DataGrid.cellTypes.alignRight
  • Text overflow handling: Automatic ellipsis for long content with full text in tooltips
Card Layout Types

Configure how columns appear in grid layout using cardType:

  • DataGrid.cardTypes.title: Primary title display in grid cards
  • DataGrid.cardTypes.thumbnail: Image display in grid cards
  • DataGrid.cardTypes.badges: Badge collection display
  • DataGrid.cardTypes.item: Action items in grid cards
Filter Types
  • select: Single-select dropdown filter
  • multiselect: Multi-select dropdown filter with array support
  • stringlist: Text input for comma-separated values + CSV upload capability
  • boolean: True/false dropdown filter
  • Default: Text input filter for global search
Advanced Column Features
  • Custom filter functions: Use filterFn: 'arrIncludesSome' for array-based filtering
  • Date sorting: Special date sorting with sortingFn: 'datetime'
  • Complex headers: Headers with tooltips and additional information
  • Responsive cell rendering: Different content based on table/grid layout
  • CSV customization: Custom headers and value functions for exports
Bulk Actions and State Management
  • Multiple bulk actions: Support for multiple action buttons with individual loading states
  • Async operations: Built-in loading states and error handling
  • Notification integration: Success/error notifications for user feedback
  • Selection tracking: Comprehensive row selection with callbacks
Styling and Layout
  • Custom CSS properties: Override default styling with CSS custom properties
  • Responsive design: Automatic layout adjustments for mobile/desktop
  • Theme integration: Full integration with design system tokens

Code Examples

Thumbnail Column with Responsive Images
columnHelper.accessor('thumbnail', {
  id: 'thumbnail',
  header: 'Avatar',
  meta: {
    cellType: DataGrid.cellTypes.thumbnail,
    cardType: DataGrid.cardTypes.thumbnail,
  },
  enableSorting: false,
  enableColumnFilter: false,
  cell: ({ getValue, table }) => {
    const layout = table.getState().layout
    const imageSize = layout === DataGrid.layouts.grid ? 120 : 48

    return (
      <img
        src={getValue()}
        style={{
          width: imageSize,
          height: imageSize,
          aspectRatio: '1/1',
          objectFit: 'cover',
          borderRadius: '50%'
        }}
        alt='User avatar'
      />
    )
  },
})
StringList Filter with CSV Upload
columnHelper.accessor('email', {
  header: 'Email',
  meta: {
    filterVariant: 'stringlist',
    filterPlaceholder: 'Enter emails separated by commas',
    filterExamplePath: '/path/to/example.csv'
  },
  filterFn: 'arrIncludesSome',
})
Complex Header with Tooltip
columnHelper.display({
  id: 'actions',
  header: () => (
    <Stack alignItems={Stack.alignItems.center} gap={spacing.xxs}>
      <DataGrid.CellText>Actions</DataGrid.CellText>
      <Tooltip
        trigger={<ButtonIcon icon={RiInformationLine} />}
        alignment={Tooltip.alignments.top}
      >
        Available actions for each item
      </Tooltip>
    </Stack>
  ),
  // ... cell configuration
})
Responsive Layout-Aware Cell Rendering
cell: ({ row, table }) => {
  const layout = table.getState().layout

  if (layout === DataGrid.layouts.grid) {
    return (
      <Button
        label='View Details'
        href={`#item/${row.original.id}`}
        utilClassNames='sxa-w-full'
      />
    )
  }

  return (
    <ButtonIcon
      icon={RiArrowRightSLine}
      onClick={() => handleEdit(row.original)}
    />
  )
}
Multiple Bulk Actions with Loading States
const bulkActions = (table) => (
  <>
    <Button
      label='Export CSV'
      iconEnd={RiDownloadCloudLine}
      loading={downloadLoading === 'csv'}
      onClick={() => handleBulkDownload('csv', table)}
    />
    <Button
      label='Send Email'
      iconEnd={RiMailLine}
      loading={downloadLoading === 'email'}
      onClick={() => handleBulkDownload('email', table)}
    />
    <ButtonIcon
      variant={ButtonIcon.variants.destructive}
      icon={RiDeleteBinLine}
      onClick={() => handleBulkDelete(table)}
    />
  </>
)

Translation Keys

Required translation keys for the t function:

const translations = {
  'table.filter.filter_cta': 'Filter',
  'search.placeholder': 'Search...',
  'table.download.label': 'Download',
  'form.cancel': 'Cancel',
  'table.csv_download.select_all': 'Select All',
  'table.filter.placeholder': 'Select filter',
  'table.filter.boolean_true': 'Yes',
  'table.filter.boolean_false': 'No',
  'table.filter.stringlist_csv_cta': 'Upload CSV',
  'table.filter.add_filter_cta': 'Add Filter',
  'table.filter.close_filter_cta': 'Close',
  'table.selection.select_all_cta': 'Select All',
  'table.sort.placeholder': 'Sort by...',
  'table.sorting.asc': '(ascending)',
  'table.sorting.desc': '(descending)',
  'table.empty.title': 'No data available',
  'table.empty.copy': 'Try adjusting your filters.',
  'table.selection.rows_selected': 'rows selected',
  'table.pagination.page_size_label': 'Show',
  'table.pagination.page_size_all_label': 'All',
  'table.pagination.rows_off_total_rows': 'of',
  'table.change_view': 'Change View'
}