Skip to content

DataTable

The DataTable component simplifies rendering of tabular data. It uses CSS grid for layout and doesn't rely on native tables.

Primary table

To render a table, provide the list of columns with their titles using columns={columns}, and the list of rows using data={data}.

tsx
import type { DataTableData } from '@semcore/ui/data-table';
import { DataTable } from '@semcore/ui/data-table';
import React from 'react';

const Demo = () => {
  return (
    <DataTable
      data={data}
      aria-label='Basic table example'
      defaultGridTemplateColumnWidth='auto'
      wMax='800px'
      headerProps={{
        sticky: true,
      }}
      columns={[
        {
          name: 'keyword',
          children: 'Keyword',
        },
        {
          name: 'kd',
          children: 'KD %',
        },
        {
          name: 'cpc',
          children: 'CPC',
        },
        {
          name: 'hiddenColumn',
          children: 'Empty',
        },
        {
          name: 'vol',
          children: 'Vol.',
        },
      ]}
    />
  );
};

const data: DataTableData = [
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
  {
    keyword: 'ebay buy',
    kd: null,
    cpc: '$0',
    vol: 'n/a',
  },
  {
    keyword: 'ebay buy',
    kd: 75.89,
    cpc: '$0',
    vol: '21,644,290',
  },
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
  {
    keyword: 'ebay buy',
    kd: null,
    cpc: '$0',
    vol: 'n/a',
  },
  {
    keyword: 'ebay buy',
    kd: '75.89',
    cpc: '$0',
    vol: '21,644,290',
  },
];

export default Demo;

Secondary table

Use the secondary table to display small amounts of data in a compact layout.

tsx
import { DataTable } from '@semcore/ui/data-table';
import React from 'react';

const Demo = () => {
  return (
    <DataTable
      data={data}
      use='secondary'
      sort={['kd', 'desc']}
      aria-label='Secondary'
      columns={[
        { name: 'keyword', children: 'Keyword' },
        { name: 'kd', children: 'KD %' },
        { name: 'cpc', children: 'CPC' },
        { name: 'vol', children: 'Vol.' },
      ]}
    />
  );
};

const data = [
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
  {
    keyword: 'ebay buy',
    kd: '-',
    cpc: '$0',
    vol: 'n/a',
  },
  {
    keyword: 'ebay buy',
    kd: '75.89',
    cpc: '$0',
    vol: '21,644,290',
  },
];

export default Demo;

Styles

Compact

Cell paddings can be reduced by adding the compact property.

tsx
import { DataTable } from '@semcore/ui/data-table';
import React from 'react';

const Demo = () => {
  return (
    <DataTable
      data={data}
      compact
      aria-label='Compact'
      columns={[
        { name: 'keyword', children: 'Keyword', gtcWidth: 'max-content' },
        { name: 'kd', children: 'KD %', gtcWidth: 'max-content' },
        { name: 'cpc', children: 'CPC', gtcWidth: 'max-content' },
        { name: 'vol', children: 'Vol.', gtcWidth: 'max-content' },
      ]}
    />
  );
};

const data = [
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
  {
    keyword: 'ebay buy',
    kd: '-',
    cpc: '$0',
    vol: 'n/a',
  },
  {
    keyword: 'ebay buy',
    kd: '75.89',
    cpc: '$0',
    vol: '21,644,290',
  },
];

export default Demo;

Table in card

Use variant="card" when displaying a table in a card. Refer to the Card layout for tables example.

Borders

Add borders to specific columns using the borders property.

tsx
import { DataTable } from '@semcore/ui/data-table';
import React from 'react';

const Demo = () => {
  return (
    <DataTable
      data={data}
      aria-label='Borders'
      headerProps={{
        sticky: true,
      }}
      columns={[
        {
          name: 'keyword',
          children: 'Keyword',
        },
        {
          name: 'bordersGroup',
          borders: 'both',
          children: 'Organic Sessions',
          columns: [
            {
              name: 'kd',
              children: 'KD %',
            },
            {
              name: 'cpc',
              children: 'CPC',
            },
            {
              name: 'vol',
              children: 'Vol.',
            },
          ],
        },
        {
          name: 'other',
          children: 'Other',
        },
      ]}
    />
  );
};

const data = [
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '1.25',
    vol: '32,500,000',
    other: 'ebay buy',
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '3.4',
    vol: '65,457,920',
    other: 'ebay buy',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '0.65',
    vol: '47,354,640',
    other: 'ebay buy',
  },
  {
    keyword: 'ebay buy',
    kd: '-',
    cpc: '0',
    vol: '2,456,789',
    other: 'ebay buy',
  },
  {
    keyword: 'ebay buy',
    kd: '75.89',
    cpc: '0',
    vol: '21,644,290',
    other: 'ebay buy',
  },
];

export default Demo;

Themes

You can use different themes for cells and rows.

tsx
import { Box } from '@semcore/ui/base-components';
import { DataTable } from '@semcore/ui/data-table';
import React from 'react';

const styles = ['success', 'info', 'muted', 'warning', 'danger'];

const Demo = () => {
  const [data] = React.useState(generateData);

  return (
    <Box wMax={800}>
      <DataTable
        data={data}
        aria-label='Example with themed rows'
        columns={[
          { name: 'col_1', children: 'Theme', gtcWidth: '100px' },
          { name: 'col_2', children: 'Column 2', gtcWidth: '100px' },
          { name: 'col_3', children: 'Column 3', gtcWidth: '100px' },
          { name: 'col_4', children: 'Column 4', gtcWidth: '100px' },
          { name: 'col_5', children: 'Column 5', gtcWidth: '100px' },
        ]}
        // @ts-ignore
        rowProps={(_, index) => {
          return {
            theme: styles[index],
          };
        }}
      />
    </Box>
  );
};

const generateData = () =>
  Array.from({ length: 5 }, (_, i) => ({
    col_1: styles[i],
    col_2: i,
    col_3: i,
    col_4: i,
    col_5: i,
  }));

export default Demo;

Use the sticky and top props to make the table header sticky.

Scroll in the table header is useful for long tables, allowing users to scroll horizontally without having to scroll to the bottom of the table.

tsx
import { Box } from '@semcore/ui/base-components';
import { DataTable } from '@semcore/ui/data-table';
import React from 'react';

const Demo = () => {
  const top = 0; // the height of the UI that should stick alongside the table header

  return (
    <>
      <DataTable
        data={data}
        aria-label='Fixed header'
        wMax={800}
        hMax={200}
        headerProps={{ sticky: true, top }}
        columns={[
          { name: 'keyword', children: 'Keyword' },
          { name: 'kd', children: 'KD %' },
          { name: 'cpc', children: 'CPC' },
          { name: 'vol', children: 'Vol.' },
        ]}
      />
      <h4>With horizontal scroll</h4>
      <DataTable
        data={data}
        aria-label='Fixed header with scroll'
        wMax={800}
        hMax={200}
        headerProps={{ sticky: true, top, withScrollBar: true }}
        columns={[
          { name: 'keyword', children: 'Keyword', gtcWidth: '340px' },
          { name: 'kd', children: 'KD %', gtcWidth: '340px' },
          { name: 'cpc', children: 'CPC', gtcWidth: '340px' },
          { name: 'vol', children: 'Vol.', gtcWidth: '340px' },
        ]}
      />
    </>
  );
};

const data = [
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
  {
    keyword: 'ebay buy',
    kd: '-',
    cpc: '$0',
    vol: 'n/a',
  },
  {
    keyword: 'ebay buy',
    kd: '75.89',
    cpc: '$0',
    vol: '21,644,290',
  },
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
  {
    keyword: 'ebay buy',
    kd: '-',
    cpc: '$0',
    vol: 'n/a',
  },
  {
    keyword: 'ebay buy',
    kd: '75.89',
    cpc: '$0',
    vol: '21,644,290',
  },
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
  {
    keyword: 'ebay buy',
    kd: '-',
    cpc: '$0',
    vol: 'n/a',
  },
  {
    keyword: 'ebay buy',
    kd: '75.89',
    cpc: '$0',
    vol: '21,644,290',
  },
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
  {
    keyword: 'ebay buy',
    kd: '-',
    cpc: '$0',
    vol: 'n/a',
  },
  {
    keyword: 'ebay buy',
    kd: '75.89',
    cpc: '$0',
    vol: '21,644,290',
  },
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
  {
    keyword: 'ebay buy',
    kd: '-',
    cpc: '$0',
    vol: 'n/a',
  },
  {
    keyword: 'ebay buy',
    kd: '75.89',
    cpc: '$0',
    vol: '21,644,290',
  },
];

export default Demo;

Header customization

You can insert tooltips, selects, and other components into the table header using children and tag.

tsx
import { LinkTrigger } from '@semcore/ui/base-trigger';
import { DataTable } from '@semcore/ui/data-table';
import Select from '@semcore/ui/select';
import Tooltip from '@semcore/ui/tooltip';
import { Text } from '@semcore/ui/typography';
import React from 'react';

const Demo = () => {
  return (
    <DataTable
      data={data}
      aria-label='Customizing header'
      columns={[
        {
          name: 'keyword',
          tag: Tooltip,
          title: 'Jesus Christ, Joe, fucking forget about it. I\'m Mr. Pink. Let\'s move on.',
          tabIndex: 0,
          children: (
            <Text noWrap>
              Keyword
              {' '}
              <Text color='text-secondary'>(1 - 100)</Text>
            </Text>
          ),
        },
        {
          name: 'kd',
          children: () => {
            const [isVisible, setIsVisible] = React.useState(false);
            const selectOptions = [
              { value: 'kd', children: 'KD %', label: 'KD %' },
              { value: 'Traffic', children: 'Traffic', label: 'Traffic' },
            ];

            return (
              <Select
                tag={LinkTrigger}
                aria-label='Column'
                color='text-primary'
                style={{ fontSize: '12px' }}
                visible={isVisible}
                onVisibleChange={setIsVisible}
                options={selectOptions}
                defaultValue='kd'
                onKeyDown={(e) => {
                  if (!isVisible && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
                    return false;
                  }
                  if (
                    (e.key === 'ArrowLeft' ||
                      e.key === 'ArrowRight' ||
                      e.key === 'ArrowDown' ||
                      e.key === 'ArrowUp') &&
                      isVisible
                  ) {
                    e.stopPropagation();
                  }
                }}
              />
            );
          },
        },
        {
          name: 'cpc',
          tag: Tooltip,
          title: 'Jesus Christ, Joe, fucking forget about it. I\'m Mr. Pink. Let\'s move on.',
          tabIndex: 0,
          children: 'CPC',
        },
        {
          name: 'vol',
          tag: Tooltip,
          title: 'Jesus Christ, Joe, fucking forget about it. I\'m Mr. Pink. Let\'s move on.',
          tabIndex: 0,
          children: 'Vol.',
        },
      ]}
    />
  );
};

const data = [
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
  {
    keyword: 'ebay buy',
    kd: '-',
    cpc: '$0',
    vol: 'n/a',
  },
  {
    keyword: 'ebay buy',
    kd: '75.89',
    cpc: '$0',
    vol: '21,644,290',
  },
];

export default Demo;

Multi-level header

Create a multi-level header by nesting columns within each other.

TIP

name property isn't applicable for group columns.

tsx
import { DataTable } from '@semcore/ui/data-table';
import React from 'react';

const Demo = () => {
  return (
    <DataTable
      data={data}
      aria-label='Multi level header'
      columns={[
        { name: 'keyword', children: 'Keyword' },
        {
          name: 'group',
          children: 'Organic Sessions',
          borders: 'both',
          columns: [
            { name: 'kd', children: 'KD %', gtcWidth: 'max-content' },
            { name: 'cpc', children: 'CPC', gtcWidth: 'max-content' },
            { name: 'vol', children: 'Vol.', gtcWidth: 'max-content' },
          ],
        },
      ]}
    />
  );
};

const data = [
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
  {
    keyword: 'ebay buy',
    kd: '-',
    cpc: '$0',
    vol: 'n/a',
  },
  {
    keyword: 'ebay buy',
    kd: '75.89',
    cpc: '$0',
    vol: '21,644,290',
  },
];

export default Demo;

Columns

Column width

Control the column width with the gtcWidth prop.

tsx
import { DataTable } from '@semcore/ui/data-table';
import React from 'react';

const Demo = () => {
  return (
    <DataTable
      data={data}
      aria-label='Column size'
      columns={[
        { name: 'keyword', children: 'Keyword', gtcWidth: 'minmax(min-content, 100px)' },
        { name: 'kd', children: 'KD %', gtcWidth: 'minmax(min-content, 100px)' },
        { name: 'cpc', children: 'CPC' },
        { name: 'vol', children: 'Vol.' },
      ]}
    />
  );
};

const data = [
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
  {
    keyword: 'ebay buy',
    kd: '-',
    cpc: '$0',
    vol: 'n/a',
  },
  {
    keyword: 'ebay buy',
    kd: '75.89',
    cpc: '$0',
    vol: '21,644,290',
  },
];

export default Demo;

Column alignment

You can use justifyContent, alignItems, alignContent, and textAlign props to align content in columns.

tsx
import { DataTable } from '@semcore/ui/data-table';
import React from 'react';

const Demo = () => {
  return (
    <DataTable
      data={data}
      aria-label='Column alignment'
      columns={[
        { name: 'keyword', children: 'Keyword' },
        {
          name: 'kd',
          children: 'KD %',
          justifyContent: 'flex-end',
          textAlign: 'end',
        },
        {
          name: 'cpc',
          children: 'CPC',
          justifyContent: 'flex-end',
          textAlign: 'end',
        },
        {
          name: 'vol',
          children: 'Vol.',
          justifyContent: 'flex-end',
          textAlign: 'end',
        },
      ]}
    />
  );
};

const data = [
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
  {
    keyword: 'ebay buy',
    kd: '-',
    cpc: '$0',
    vol: 'n/a',
  },
  {
    keyword: 'ebay buy',
    kd: '75.89',
    cpc: '$0',
    vol: '21,644,290',
  },
];

export default Demo;

Fixed column

To fix table columns, use the fixed property.

TIP

If fixed columns aren't visible in the following example, try reducing the window width.

tsx
import { DataTable } from '@semcore/ui/data-table';
import React from 'react';

const Demo = () => {
  return (
    <DataTable
      data={data}
      aria-label='Fixed columns'
      wMax={800}
      hMax={400}
      headerProps={{ sticky: true }}
      columns={[
        {
          name: 'keyword',
          children: 'keyword',
          gtcWidth: '300px',
          fixed: 'left',
        },
        {
          name: 'kd',
          children: 'KD %',
          gtcWidth: '300px',
        },
        {
          name: 'cpc',
          children: 'CPC',
          gtcWidth: '300px',
        },
        {
          name: 'vol',
          children: 'Vol.',
          gtcWidth: '300px',
          fixed: 'right',
        },
      ]}
    />
  );
};

const data = [
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
  {
    keyword: 'ebay buy',
    kd: '-',
    cpc: '$0',
    vol: 'n/a',
  },
  {
    keyword: 'ebay buy',
    kd: '75.89',
    cpc: '$0',
    vol: '21,644,290',
  },
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
  {
    keyword: 'ebay buy',
    kd: '-',
    cpc: '$0',
    vol: 'n/a',
  },
  {
    keyword: 'ebay buy',
    kd: '75.89',
    cpc: '$0',
    vol: '21,644,290',
  },
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
  {
    keyword: 'ebay buy',
    kd: '-',
    cpc: '$0',
    vol: 'n/a',
  },
  {
    keyword: 'ebay buy',
    kd: '75.89',
    cpc: '$0',
    vol: '21,644,290',
  },
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
  {
    keyword: 'ebay buy',
    kd: '-',
    cpc: '$0',
    vol: 'n/a',
  },
  {
    keyword: 'ebay buy',
    kd: '75.89',
    cpc: '$0',
    vol: '21,644,290',
  },
];

export default Demo;

Column grouping

Merge cells by combining column keys in the data. You can merge cells in a specific row, as shown in the following example, or in all rows.

tsx
import { DataTable } from '@semcore/ui/data-table';
import React from 'react';

const data = [
  {
    'keyword': 'ebay buy',
    'kd/cpc/vol': 'These three columns are grouped.',
  },
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
  {
    keyword: 'ebay buy',
    kd: '75.89',
    cpc: '$0',
    vol: '21,644,290',
  },
];

const Demo = () => {
  return (
    <DataTable
      data={data}
      aria-label='Columns merging'
      columns={[
        { name: 'keyword', children: 'Keyword' },
        { name: 'kd', children: 'KD %' },
        { name: 'cpc', children: 'CPC' },
        { name: 'vol', children: 'Vol.' },
      ]}
    />
  );
};

export default Demo;

Rows

Row grouping

Merge cells across rows using the [ROW_GROUP] key in the data.

tsx
import { DataTable, ROW_GROUP } from '@semcore/ui/data-table';
import React from 'react';

const data = [
  {
    keyword: 'ebay buy',
    [ROW_GROUP]: [
      {
        kd: '77.8',
        cpc: '$1.25',
        vol: '32,500,000',
      },
      {
        kd: '-',
        cpc: '$0',
        vol: 'n/a',
      },
      {
        kd: '75.89',
        cpc: '$0',
        vol: '21,644,290',
      },
    ],
  },
  {
    keyword: 'www.ebay.com',
    [ROW_GROUP]: [
      {
        kd: '11.2',
        cpc: '$3.4',
        vol: '65,457,920',
      },
      {
        kd: '10',
        cpc: '$0.65',
        vol: '47,354,640',
      },
    ],
  },
];

const Demo = () => {
  return (
    <DataTable
      data={data}
      aria-label='Rows grouping'
      columns={[
        { name: 'keyword', children: 'Keyword' },
        { name: 'kd', children: 'KD %' },
        { name: 'cpc', children: 'CPC' },
        { name: 'vol', children: 'Vol.' },
      ]}
    />
  );
};

export default Demo;

Checkboxes and action bar

You can enable selecting rows with checkboxes with the selectedRows and onSelectedRowsChange props.

tsx
import { Box, Flex, Collapse, ScreenReaderOnly } from '@semcore/ui/base-components';
import Button from '@semcore/ui/button';
import { DataTable } from '@semcore/ui/data-table';
import Pagination from '@semcore/ui/pagination';
import { Text } from '@semcore/ui/typography';
import React from 'react';

type CheckboxExampleProps = { animationDuration: number; loading: boolean; sideIndents?: 'wide'; compact?: boolean };

const Demo = (props: CheckboxExampleProps) => {
  const [selectedRows, setSelectedRows] = React.useState<string[]>([]);
  const [selectedRowsDisplay, setSelectedRowsDisplay] = React.useState(0);
  const [ariaMessage, setAriaMessage] = React.useState('');
  const [currentPage, setCurrentPage] = React.useState(0);
  const tableRef = React.useRef<HTMLDivElement>(null);

  const handleChangeSelectedRows = (value: string[]) => {
    setSelectedRows(value);
    if (!selectedRows.length) setAriaMessage('Action bar appeared before the table');
    if (value.length) setSelectedRowsDisplay(value.length);
  };

  const handleDeselectAll = () => {
    setSelectedRows([]);
    tableRef.current?.focus();
  };

  React.useEffect(() => {
    const timer = setTimeout(() => setAriaMessage(''), 300);
    return () => clearTimeout(timer);
  }, [ariaMessage]);

  const limit = 5;
  const tableData = data.slice(currentPage * limit, currentPage * limit + limit);

  return (
    <>
      <Box
        // need this for FF
        tabIndex={-1}
        wMax={800}
        h={250}
        style={{ overflow: 'auto', scrollPaddingTop: selectedRows.length ? '44px' : undefined }}
      >
        <ScreenReaderOnly role='status' aria-live='polite'>
          {ariaMessage}
        </ScreenReaderOnly>
        <Collapse
          visible={!!selectedRows.length}
          duration={props.animationDuration}
          style={{ position: 'sticky', top: 0, zIndex: 50 }}
        >
          <Flex
            role='region'
            aria-label='Table action bar'
            alignItems='center'
            gap={6}
            py={2}
            px={3}
            style={{
              backgroundColor: 'var(--intergalactic-bg-primary-neutral, #ffffff)',
            }}
          >
            <Text size={200}>
              Selected rows:
              {' '}
              <Text bold>{selectedRowsDisplay}</Text>
            </Text>
            <Button use='tertiary' onClick={handleDeselectAll}>
              Deselect all
            </Button>
          </Flex>
        </Collapse>
        <DataTable
          data={tableData}
          aria-label='Table example with selectable rows'
          defaultGridTemplateColumnWidth='auto'
          selectedRows={selectedRows}
          onSelectedRowsChange={handleChangeSelectedRows}
          ref={tableRef}
          sideIndents={props.sideIndents}
          loading={props.loading}
          compact={props.compact}
          headerProps={{
            sticky: true,
            top: selectedRows.length ? 44 : 0,
            animationDuration: props.animationDuration,
          }}
          columns={[
            { name: 'keyword', children: 'Keyword' },
            { name: 'kd', children: 'KD %' },
            { name: 'cpc', children: 'CPC' },
            { name: 'vol', children: 'Vol.' },
          ]}
          uniqueRowKey='id'
        />
      </Box>
      <Pagination
        mt={4}
        totalPages={Math.ceil(data.length / limit)}
        currentPage={currentPage + 1}
        onCurrentPageChange={(page) => setCurrentPage(page - 1)}
        aria-label='Table with selectable rows pagination'
      />
    </>
  );
};

const data = [
  { id: '1', keyword: 'ebay buy', kd: '31.2', cpc: '$1.15', vol: '22,000' },
  { id: '2', keyword: 'amazon shoes', kd: '47', cpc: '$2.95', vol: '48,000' },
  { id: '3', keyword: 'www.nike.com', kd: '66.4', cpc: '$3.80', vol: 'n/a' },
  { id: '4', keyword: 'buy iphone 13', kd: '59', cpc: '$5.20', vol: '71,000' },
  { id: '5', keyword: 'adidas sale', kd: '40.2', cpc: '$1.85', vol: '19,500' },
  { id: '6', keyword: 'cheap flights expedia', kd: '52', cpc: '$4.10', vol: '35,800' },
  { id: '7', keyword: 'booking.com hotels', kd: '73', cpc: '$6.45', vol: 'n/a' },
  { id: '8', keyword: 'ubereats promo code', kd: '38', cpc: '$2.10', vol: '11,700' },
  { id: '9', keyword: 'buy ps5 online', kd: '64', cpc: '$5.95', vol: '44,200' },
  { id: '10', keyword: 'shopify login', kd: '25.8', cpc: '$0.65', vol: '13,600' },
  { id: '11', keyword: 'h&m online store', kd: '36', cpc: '$1.70', vol: '10,300' },
  { id: '12', keyword: 'buy macbook air', kd: '57.4', cpc: '$4.90', vol: '28,400' },
  { id: '13', keyword: 'www.zara.com', kd: '45', cpc: '$3.20', vol: 'n/a' },
  { id: '14', keyword: 'target clearance', kd: '33', cpc: '$1.25', vol: '12,900' },
  { id: '15', keyword: 'asos men jackets', kd: '41', cpc: '$2.55', vol: '6,800' },
  { id: '16', keyword: 'best buy coupons', kd: '48', cpc: '$3.70', vol: '17,100' },
  { id: '17', keyword: 'walmart near me', kd: '60.1', cpc: '$0.95', vol: '50,000' },
  { id: '18', keyword: 'netflix gift card', kd: '39', cpc: '$2.20', vol: '8,900' },
  { id: '19', keyword: 'www.apple.com', kd: '71', cpc: '$6.90', vol: 'n/a' },
  { id: '20', keyword: 'nike running shoes men', kd: '44', cpc: '$3.60', vol: '21,700' },
  { id: '21', keyword: 'download spotify premium', kd: '58', cpc: '$4.75', vol: '26,800' },
  { id: '22', keyword: 'buy dell laptop', kd: '53.1', cpc: '$5.40', vol: '19,600' },
  { id: '23', keyword: 'gap kids sale', kd: '34', cpc: '$1.10', vol: '5,300' },
];

export const defaultProps: CheckboxExampleProps = {
  animationDuration: 200,
  loading: false,
  sideIndents: undefined,
  compact: undefined,
};

Demo.defaultProps = defaultProps;

export default Demo;

Custom row rendering

tsx
import { DataTable } from '@semcore/ui/data-table';
import React from 'react';

const Demo = () => {
  return (
    <DataTable
      data={data}
      aria-label='Custom rows rendering'
      hMax='500px'
      totalRows={data.length}
      sort={['keyword', 'asc']}
      columns={[
        { name: 'keyword', children: 'Keyword', sortable: true },
        { name: 'tags', children: 'Tags' },
        { name: 'cpc', children: 'CPC' },
        { name: 'vol', children: 'Vol.' },
      ]}
      virtualScroll={true}
      renderCell={(props) => {
        if (props.dataKey === 'tags') {
          const tags = props.row[props.dataKey];

          if (Array.isArray(tags)) {
            return (
              <div>
                {tags.map((_, i) => {
                  return (
                    <div key={i}>
                      tag
                      {i + 1}
                    </div>
                  );
                })}
              </div>
            );
          }

          return null;
        }
        return props.defaultRender();
      }}
    />
  );
};

const data = Array(100)
  .fill(0)
  .map((_, i) => ({
    keyword: `keyword ${i}`,
    tags: Array(Math.floor(Math.random() * 4))
      .fill(0),
    // .map((_, i) => <div key={i}>tag {i + 1}</div>),
    cpc: Math.round(Math.random() * 10),
    vol: Math.round(Math.random() * 1000000),
  }));

export default Demo;

Cells

Access to cells

To customize the content of a table cell, use the renderCell prop. It receives props described in CellRenderProps.

You can return either a custom React element to override the rendering entirely, or an object that will be applied as props to the cell. If the returned object includes a children property, it will override the default cell content—otherwise, you can use it to apply custom attributes such as theming or data attributes.

tsx
import { ButtonLink } from '@semcore/ui/button';
import { DataTable } from '@semcore/ui/data-table';
import React from 'react';

const Demo = () => {
  return (
    <DataTable
      data={data}
      aria-label='Access to cells'
      columns={[
        { name: 'keyword', children: 'Keyword' },
        { name: 'kd', children: 'KD,%' },
        { name: 'cpc', children: 'CPC' },
        { name: 'vol', children: 'Vol.' },
      ]}
      renderCell={(props) => {
        if (props.dataKey === 'keyword') {
          return (
            <ButtonLink
              onClick={() => {
                alert(`Click row
                  props: ${JSON.stringify(Object.keys(props), null, '  ')};
                  row: ${JSON.stringify(props.row, null, '  ')};
                  index: ${props.rowIndex};`);
              }}
            >
              {props.value}
            </ButtonLink>
          );
        }

        if (props.dataKey === 'kd') {
          return {
            'data-test-id': 'kd cell',
          };
        }

        return props.defaultRender();
      }}
    />
  );
};

const data = [
  {
    keyword: 'it must be link ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
  {
    keyword: 'ebay buy',
    kd: '-',
    cpc: '$0',
    vol: 'n/a',
  },
  {
    keyword: 'ebay buy',
    kd: '75.89',
    cpc: '$0',
    vol: '21,644,290',
  },
  {
    'keyword/kd/cpc': '434',
    'vol': 'ebay buy',
  },
];

export default Demo;

Access to set of cells

tsx
import { DataTable } from '@semcore/ui/data-table';
import Spin from '@semcore/ui/spin';
import React from 'react';

const Demo = () => {
  return (
    <DataTable
      data={data}
      aria-label='Access to set of cells'
      columns={[
        { name: 'keyword', children: 'Keyword' },
        { name: 'kd', children: 'KD %' },
        { name: 'cpc', children: 'CPC' },
        { name: 'vol', children: 'Vol.' },
      ]}
      renderCell={({ dataKey, row, defaultRender }) => {
        const value = row[dataKey].toString();
        return ['-', '$0', 'n/a'].includes(value) ? <Spin /> : defaultRender();
      }}
    />
  );
};

const data = [
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
  {
    keyword: 'ebay buy',
    kd: '-',
    cpc: '$0',
    vol: 'n/a',
  },
  {
    keyword: 'ebay buy',
    kd: '75.89',
    cpc: '$0',
    vol: '21,644,290',
  },
];

export default Demo;

Sorting

To enable column sorting:

  1. Set the sortable property on the column.
  2. Subscribe to the onSortChange event.
  3. Pass the sort property to the table.
  4. Sort the data provided in the data property.
tsx
import type { DataTableSort } from '@semcore/ui/data-table';
import { DataTable } from '@semcore/ui/data-table';
import Ellipsis from '@semcore/ui/ellipsis';
import React from 'react';

type SortableColumn = Exclude<keyof typeof data[0], 'keyword'>;

const Demo = () => {
  const [sort, setSort] = React.useState<DataTableSort<keyof typeof data[0]>>(['kd', 'desc']);
  const sortedData = React.useMemo(
    () =>
      [...data].sort((aRow, bRow) => {
        const [prop, sortDirection] = sort;
        const a = aRow[prop as SortableColumn];
        const b = bRow[prop as SortableColumn];
        if (a === b) return 0;
        if (sortDirection === 'asc') return a > b ? 1 : -1;
        else return a > b ? -1 : 1;
      }),
    [sort],
  );
  const numberFormat = React.useMemo(() => new Intl.NumberFormat('en-US'), []);
  const currencyFormat = React.useMemo(
    () => new Intl.NumberFormat('en-US', { currency: 'USD', style: 'currency' }),
    [],
  );
  const handleSortChange: (sort: DataTableSort<string>, e?: React.SyntheticEvent) => void = (
    newSort,
  ) => {
    setSort(newSort as DataTableSort<SortableColumn>);
  };

  return (
    <DataTable
      data={sortedData}
      sort={sort}
      onSortChange={handleSortChange}
      aria-label='Sorting'
      columns={[
        { name: 'keyword', children: 'Keyword', justifyContent: 'left', sortable: true },
        {
          name: 'kd',
          children: <Ellipsis>KD % and some another text long</Ellipsis>,
          justifyContent: 'right',
          gtcWidth: 'minmax(0, 68px)',
          sortable: true,
        },
        { name: 'cpc', children: 'CPC', gtcWidth: 'minmax(0, 60px)', sortable: 'asc' },
        {
          name: 'vol',
          children: 'Vol.',
          gtcWidth: 'minmax(0, 120px)',
          justifyContent: 'left',
          sortable: 'desc',
        },
      ]}
      renderCell={(props) => {
        if (props.columnName === 'keyword') {
          return props.defaultRender();
        }

        const rawValue = props.row[props.columnName as SortableColumn];

        return typeof rawValue === 'number' && rawValue !== -1
          ? props.columnName === 'cpc'
            ? currencyFormat.format(rawValue)
            : numberFormat.format(rawValue)
          : 'n/a';
      }}
    />
  );
};

export default Demo;

const data = [
  {
    keyword: 'ebay buy',
    kd: 77.8,
    cpc: 1.25,
    vol: 32500000,
  },
  {
    keyword: 'www.ebay.com',
    kd: 11.2,
    cpc: 3.4,
    vol: 65457920,
  },
  {
    keyword: 'www.ebay.com',
    kd: 10,
    cpc: 0.65,
    vol: 47354640,
  },
  {
    keyword: 'ebay buy',
    kd: -1,
    cpc: 0,
    vol: -1,
  },
  {
    keyword: 'ebay buy',
    kd: 75.89,
    cpc: 0,
    vol: 21644290,
  },
];

Expanding column

changeSortSize allows the sorted column to grow in width to fit the sort icon.

tsx
import { DataTable } from '@semcore/ui/data-table';
import type { DataTableSort, DataTableProps } from '@semcore/ui/data-table';
import React from 'react';

type SortableColumn = Exclude<keyof typeof data[0], 'keyword'>;

export type SortTableProps = {
  use: DataTableProps<typeof data, any, any>['use'];
};

const Demo = (props: SortTableProps) => {
  const [sort, setSort] = React.useState<DataTableSort<keyof typeof data[0]>>(['kd', 'desc']);
  const sortedData = React.useMemo(
    () =>
      [...data].sort((aRow, bRow) => {
        const [prop, sortDirection] = sort;
        const a = aRow[prop as SortableColumn];
        const b = bRow[prop as SortableColumn];
        if (a === b) return 0;
        if (sortDirection === 'asc') return a - b;
        else return b - a;
      }),
    [sort],
  );
  const numberFormat = React.useMemo(() => new Intl.NumberFormat('en-US'), []);
  const currencyFormat = React.useMemo(
    () => new Intl.NumberFormat('en-US', { currency: 'USD', style: 'currency' }),
    [],
  );

  return (
    <DataTable
      data={sortedData}
      sort={sort}
      use={props.use}
      onSortChange={setSort}
      aria-label='Expanding sortable column'
      columns={[
        { name: 'keyword', children: 'Keyword', justifyContent: 'left', sortable: true },
        {
          name: 'kd',
          children: 'KD %',
          justifyContent: 'right',
          gtcWidth: 'minmax(0, 68px)',
          style: { whiteSpace: 'nowrap' },
          borders: 'both',
          sortable: true,
          changeSortSize: true,
        },
        {
          name: 'cpc',
          children: 'CPC',
          gtcWidth: 'minmax(60px, 66px)',
          borders: 'right',
          sortable: true,
          changeSortSize: true,
        },
        {
          name: 'vol',
          children: 'Vol.',
          gtcWidth: 'minmax(0, 120px)',
          justifyContent: 'left',
          sortable: true,
        },
      ]}
      renderCell={(props) => {
        if (props.columnName === 'keyword') {
          return props.defaultRender();
        }

        const rawValue = props.row[props.columnName as SortableColumn];

        return typeof rawValue === 'number' && rawValue !== -1
          ? props.columnName === 'cpc'
            ? currencyFormat.format(rawValue)
            : numberFormat.format(rawValue)
          : 'n/a';
      }}
    />
  );
};
export const defaultTableProps: SortTableProps = {
  use: 'primary',
};

Demo.defaultProps = defaultTableProps;

export default Demo;

const data = [
  {
    keyword: 'ebay buy',
    kd: 77.8,
    cpc: 1.25,
    vol: 32500000,
  },
  {
    keyword: 'www.ebay.com',
    kd: 11.2,
    cpc: 3.4,
    vol: 65457920,
  },
  {
    keyword: 'www.ebay.com',
    kd: 10,
    cpc: 0.65,
    vol: 47354640,
  },
  {
    keyword: 'ebay buy',
    kd: -1,
    cpc: 0,
    vol: -1,
  },
  {
    keyword: 'ebay buy',
    kd: 75.89,
    cpc: 0,
    vol: 21644290,
  },
];

Scroll

Basic scroll

<DataTable/> inherits all Box properties, such as wMax and hMax, which can be used to enable internal scroll.

By default, horizontal scrolling is displayed at the bottom of the table, but it can also be added to the table header. Scroll in the table header is useful for long tables, allowing users to scroll horizontally without having to scroll to the end of the table. For examples, refer to Sticky header.

tsx
import { DataTable } from '@semcore/ui/data-table';
import React from 'react';

const Demo = () => {
  return (
    <DataTable
      data={data}
      aria-label='Scroll inside'
      hMax={200}
      wMax={400}
      columns={[
        { name: 'keyword', children: 'Keyword', gtcWidth: '150px' },
        { name: 'kd', children: 'KD %', gtcWidth: '150px' },
        { name: 'cpc', children: 'CPC', gtcWidth: '150px' },
        { name: 'vol', children: 'Vol.', gtcWidth: '150px' },
      ]}
    />
  );
};

const data = [
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
  {
    keyword: 'ebay buy',
    kd: '-',
    cpc: '$0',
    vol: 'n/a',
  },
  {
    keyword: 'ebay buy',
    kd: '75.89',
    cpc: '$0',
    vol: '21,644,290',
  },
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
  {
    keyword: 'ebay buy',
    kd: '-',
    cpc: '$0',
    vol: 'n/a',
  },
  {
    keyword: 'ebay buy',
    kd: '75.89',
    cpc: '$0',
    vol: '21,644,290',
  },
];

export default Demo;

Virtual scroll with constant row height

Enable scroll virtualization using the virtualScroll property. Passing rowHeight as its subproperty will ensure the best performance.

tsx
import { DataTable } from '@semcore/ui/data-table';
import React from 'react';

const keyword = ['ebay buy', 'www.ebay.com', 'ebay buy'];
const kd = ['77.8', '10', '11.2', '-', '75.89'];
const cpc = ['$3.4', '$0.65', '$1.25', '$0', '$0'];
const vol = ['32,500,000', '65,457,920', '47,354,640', 'n/a', '21,644,290'];

const data = Array(10000)
  .fill(0)
  .map((_, index) => ({
    id: `#${index + 1}`,
    keyword: keyword[Math.floor(keyword.length * Math.random())],
    // [ROW_GROUP]: [
    //   {
    kd: kd[Math.floor(kd.length * Math.random())],
    cpc: cpc[Math.floor(cpc.length * Math.random())],
    vol: vol[Math.floor(vol.length * Math.random())],
    // },
    // ],
  }));

const Demo = () => {
  return (
    <DataTable
      data={data}
      totalRows={10000}
      aria-label='Virtual scroll'
      h={400}
      virtualScroll={{ rowHeight: 45 }}
      headerProps={{ sticky: true }}
      columns={[
        { name: 'id', children: 'ID' },
        { name: 'keyword', children: 'Keyword', gtcWidth: '300px' },
        {
          name: 'group',
          children: 'Organic Sessions',
          columns: [
            { name: 'kd', children: 'KD %' },
            { name: 'cpc', children: 'CPC' },
            { name: 'vol', children: 'Vol.' },
          ],
        },
      ]}
    />
  );
};

export default Demo;

Virtual scroll with variable row height

Omit rowHeight for tables with variable row heights.

tsx
import { DataTable } from '@semcore/ui/data-table';
import React from 'react';

const keyword = [
  'ebay buy',
  'www.ebay.com',
  'ebay buy',
  'some long long long long long long text for test multi rows in table with virtualizarion some long long long long long long text for test multi rows in table with virtualizarion some long long long long long long text for test multi rows in table with virtualizarion some long long long long long long text for test multi rows in table with virtualizarion some long long long long long long text for test multi rows in table with virtualizarion some long long long long long long text for test multi rows in table with virtualizarion some long long long long long long text for test multi rows in table with virtualizarion',
];
const kd = ['77.8', '10', '11.2', '-', '75.89'];
const cpc = ['$3.4', '$0.65', '$1.25', '$0', '$0'];
const vol = ['32,500,000', '65,457,920', '47,354,640', 'n/a', '21,644,290'];

const data = Array(10000)
  .fill(0)
  .map((_, index) => ({
    id: `#${index + 1}`,
    keyword: index < 3 ? keyword[3] : keyword[Math.floor(keyword.length * Math.random())],
    kd: kd[Math.floor(kd.length * Math.random())],
    cpc: cpc[Math.floor(cpc.length * Math.random())],
    vol: vol[Math.floor(vol.length * Math.random())],
  }));

const Demo = () => {
  return (
    <DataTable
      data={data}
      totalRows={10000}
      aria-label='Virtual scroll with variable row height'
      h={400}
      virtualScroll
      headerProps={{ sticky: true }}
      columns={[
        { name: 'id', children: 'ID' },
        { name: 'keyword', children: 'Keyword', gtcWidth: '300px' },
        {
          name: 'group',
          children: 'Organic Sessions',
          columns: [
            { name: 'kd', children: 'KD %' },
            { name: 'cpc', children: 'CPC' },
            { name: 'vol', children: 'Vol.' },
          ],
        },
      ]}
    />
  );
};

export default Demo;

Pagination

Avoid placing Pagination inside the table, as the pagination component has a nav landmark assigned to it.

tsx
import { Flex } from '@semcore/ui/base-components';
import { DataTable } from '@semcore/ui/data-table';
import Pagination from '@semcore/ui/pagination';
import Select from '@semcore/ui/select';
import React from 'react';

const Demo = () => {
  const [limit, setLimit] = React.useState(10);
  const [currentPage, setCurrentPage] = React.useState(0);
  const numberFormat = React.useMemo(() => new Intl.NumberFormat('en-US'), []);
  const currencyFormat = React.useMemo(
    () => new Intl.NumberFormat('en-US', { currency: 'USD', style: 'currency' }),
    [],
  );

  const numLim = Number(limit);
  const tableData: typeof data = [];

  let index = 0;

  for (let i = 0; i < 10; i++) {
    tableData.push(...data.map((item) => {
      index++;

      return {
        ...item,
        keyword: `${index} ${item.keyword}`,
      };
    }));
  }

  return (
    <>
      <DataTable
        data={tableData.slice(currentPage * numLim, currentPage * numLim + numLim)}
        aria-label='Pagination'
        h='auto'
        columns={[
          { name: 'keyword', children: 'Keyword', justifyContent: 'left' },
          {
            name: 'kd',
            children: 'KD %',
            justifyContent: 'right',
            gtcWidth: 'minmax(fit-content, 68px)',
          },
          { name: 'cpc', children: 'CPC', gtcWidth: 'minmax(fit-content, 60px)' },
          {
            name: 'vol',
            children: 'Vol.',
            gtcWidth: 'minmax(fit-content, 120px)',
            justifyContent: 'left',
          },
        ]}
        renderCell={(props) => {
          const { column, row } = props;

          if (!row) return props.defaultRender();

          const value = row[column.name];

          if (column.name === 'keyword') {
            return props.defaultRender();
          }

          if (typeof value !== 'number' || value === -1) {
            return 'n/a';
          }

          if (column.name === 'cpc') {
            return currencyFormat.format(value);
          }

          return numberFormat.format(value);
        }}
      />
      <Flex gap={4} mt={4}>
        <Pagination
          totalPages={Math.ceil(tableData.length / numLim)}
          currentPage={currentPage + 1}
          onCurrentPageChange={(page) => setCurrentPage(page - 1)}
        />
        <Select
          aria-label='Table rows on the page'
          value={numLim}
          onChange={setLimit}
          options={[{ value: 3, children: 3 }, { value: 5, children: 5 }, { value: 8, children: 8 }, { value: 10, children: 10 }]}
        />
      </Flex>
    </>
  );
};

export default Demo;

const data = [
  {
    keyword: 'ebay buy',
    kd: 77.8,
    cpc: 1.25,
    vol: 32500000,
  },
  {
    keyword: 'www.ebay.com',
    kd: 11.2,
    cpc: 3.4,
    vol: 65457920,
  },
  {
    keyword: 'www.ebay.com',
    kd: 10,
    cpc: 0.65,
    vol: 47354640,
  },
  {
    keyword: 'ebay buy',
    kd: -1,
    cpc: 0,
    vol: -1,
  },
  {
    keyword: 'ebay buy last',
    kd: 75.89,
    cpc: 0,
    vol: 21644290,
  },
];

Table states

Initial loading (Skeleton)

Add a skeleton to the table by directly substituting the cell content.

tsx
import { ScreenReaderOnly } from '@semcore/ui/base-components';
import Button from '@semcore/ui/button';
import { DataTable } from '@semcore/ui/data-table';
import Skeleton from '@semcore/ui/skeleton';
import React from 'react';

const Demo = () => {
  const [loading, setLoading] = React.useState(true);
  const [message, setMessage] = React.useState('');

  React.useEffect(() => {
    const timer = setTimeout(() => {
      setMessage('');
    }, 300);
    return () => {
      clearTimeout(timer);
    };
  }, [message]);

  const toggleLoading = () => {
    setLoading(!loading);
    setMessage(loading ? 'Data loaded' : 'Loading started');
  };

  return (
    <>
      <ScreenReaderOnly role='status' aria-live='polite'>
        {message}
      </ScreenReaderOnly>
      <DataTable
        data={data}
        aria-label='Loading using Skeleton'
        h='auto'
        columns={[
          { name: 'keyword', children: 'Keyword' },
          { name: 'kd', children: 'KD %' },
          { name: 'cpc', children: 'CPC' },
          { name: 'vol', children: 'Vol.' },
        ]}
        renderCell={(props) => {
          if (loading) {
            return (
              <Skeleton height={17}>
                <Skeleton.Text y='5' width='60%' />
              </Skeleton>
            );
          }

          return props.defaultRender();
        }}
      />
      <Button onClick={toggleLoading} mt={3}>
        {loading ? 'Stop loading' : 'Start loading'}
      </Button>
    </>
  );
};

const data = [
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
];

export default Demo;

Updating table (SpinContainer)

SpinContainer is the default loading state for the table and can be enabled by the loading prop.

tsx
import { ScreenReaderOnly } from '@semcore/ui/base-components';
import Button from '@semcore/ui/button';
import { DataTable } from '@semcore/ui/data-table';
import React from 'react';

const Demo = (): any => {
  const [loading, setLoading] = React.useState(true);
  const [message, setMessage] = React.useState('');

  React.useEffect(() => {
    const timer = setTimeout(() => {
      setMessage('');
    }, 300);
    return () => {
      clearTimeout(timer);
    };
  }, [message]);

  const toggleLoading = () => {
    setLoading(!loading);
    setMessage(loading ? 'Data loaded' : 'Loading started');
  };

  return (
    <>
      <ScreenReaderOnly role='status' aria-live='polite'>
        {message}
      </ScreenReaderOnly>
      <DataTable
        data={data}
        aria-label='Loading using SpinContainer'
        loading={loading}
        h='auto'
        columns={[
          { name: 'keyword', children: 'Keyword' },
          { name: 'kd', children: 'KD %' },
          { name: 'cpc', children: 'CPC' },
          { name: 'vol', children: 'Vol.' },
        ]}
      />
      <Button onClick={toggleLoading} mt={3}>
        {loading ? 'Stop loading' : 'Start loading'}
      </Button>
    </>
  );
};

const data = [
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
  {
    keyword: 'ebay buy',
    kd: '-',
    cpc: '$0',
    vol: 'n/a',
  },
  {
    keyword: 'ebay buy',
    kd: '75.89',
    cpc: '$0',
    vol: '21,644,290',
  },
];

export default Demo;

Limited data

You can hide the limited data with a blurred overlay by using the limit prop, and add your own message for this table state.

tsx
import { Flex } from '@semcore/ui/base-components';
import Button from '@semcore/ui/button';
import { DataTable } from '@semcore/ui/data-table';
import type { DataTableData } from '@semcore/ui/data-table';
import { Text } from '@semcore/ui/typography';
import React from 'react';

export type LimitedModeExampleProps = {
  rowsLimit?: number;
  columnsLimit?: number;
};

const Demo = (props: LimitedModeExampleProps) => {
  const { rowsLimit, columnsLimit } = props;

  return (
    <DataTable
      data={data}
      aria-label='Limited table example'
      defaultGridTemplateColumnWidth='auto'
      wMax='800px'
      limit={{
        fromRow: rowsLimit,
        fromColumn: columnsLimit,
        renderOverlay() {
          return (
            <Flex alignItems='center' direction='column' gap={3} py={6} wMax={320} style={{ textWrap: 'balance' }}>
              <Text size={300} fontWeight='bold' textAlign='center' id='limited_rows_title'>You've reached your report limit for today</Text>
              <Text size={200} textAlign='center' id='limited_rows_description'>
                To increase your daily report limit,upgrade to a Guru plan.
              </Text>
              <Button
                theme='success'
                use='primary'
                aria-describedby='limited_rows_title limited_rows_description'
              >
                Upgrade to Guru
              </Button>
            </Flex>
          );
        },
      }}
      headerProps={{
        sticky: true,
      }}
      columns={[
        {
          name: 'keyword',
          children: 'Keyword',
        },
        {
          name: 'kd',
          children: 'KD %',
        },
        {
          name: 'cpc',
          children: 'CPC',
        },
        {
          name: 'hiddenColumn',
          children: 'Empty',
        },
        {
          name: 'vol',
          children: 'Vol.',
        },
      ]}
    />
  );
};

const data: DataTableData = [
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
  {
    keyword: 'ebay buy',
    kd: null,
    cpc: '$0',
    vol: 'n/a',
  },
  {
    keyword: 'ebay buy',
    kd: 75.89,
    cpc: '$0',
    vol: '21,644,290',
  },
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
  {
    keyword: 'ebay buy',
    kd: null,
    cpc: '$0',
    vol: 'n/a',
  },
  {
    keyword: 'ebay buy',
    kd: '75.89',
    cpc: '$0',
    vol: '21,644,290',
  },
];

export const limitedModeDefaultProps: LimitedModeExampleProps = {
  rowsLimit: 3,
  columnsLimit: undefined,
};

Demo.defaultProps = limitedModeDefaultProps;

export default Demo;

Empty state

DataTable has a default empty state based on WidgetEmpty which is rendered automatically if the data is empty. You can customize the empty state using the renderEmptyData prop.

tsx
import Button from '@semcore/ui/button';
import { DataTable } from '@semcore/ui/data-table';
import { NoData } from '@semcore/ui/widget-empty';
import React from 'react';

const Demo = () => {
  return (
    <DataTable
      data={[]}
      renderEmptyData={() => (
        <NoData type='nothing-found' my={7} mx='auto'>
          <Button mt={4}>Clear filters</Button>
        </NoData>
      )}
      aria-label='Empty table example'
      defaultGridTemplateColumnWidth='auto'
      wMax='800px'
      headerProps={{
        sticky: true,
      }}
      columns={[
        {
          name: 'keyword',
          children: 'keyword',
        },
        {
          name: 'kd',
          children: 'KD %',
        },
        {
          name: 'cpc',
          children: 'CPC',
        },
        {
          name: 'hiddenColumn',
          children: 'HC',
        },
        {
          name: 'vol',
          children: 'Vol.',
        },
      ]}
    />
  );
};

export default Demo;

Accordion in table

Render expandable rows using the [ACCORDION] key in the data.

tsx
import type { DataTableProps } from '@semcore/ui/data-table';
import { DataTable, ACCORDION } from '@semcore/ui/data-table';
import React from 'react';

export type TableInTableProps = {
  accordionMode: DataTableProps<typeof data, any, any>['accordionMode'];
  onAccordionToggle?: DataTableProps<typeof data, any, any>['onAccordionToggle'];
};

const Demo = (props: TableInTableProps) => {
  return (
    <DataTable
      data={data}
      aria-label='Parent'
      columns={[
        { name: 'keyword', children: 'Keyword' },
        { name: 'kd', children: 'KD %' },
        { name: 'cpc', children: 'CPC' },
        { name: 'vol', children: 'Vol.' },
      ]}
      accordionMode={props.accordionMode}
      onAccordionToggle={props.onAccordionToggle}
    />
  );
};

export const tableInTableDefaultProps: TableInTableProps = {
  accordionMode: 'independent',
};

Demo.defaultProps = tableInTableDefaultProps;

const data = [
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
    [ACCORDION]: [
      {
        keyword: 'www.ebay.com',
        kd: '11.2',
        cpc: '$3.4',
        vol: '65,457,920',
      },
      {
        keyword: 'www.ebay.com',
        kd: '10',
        cpc: '$0.65',
        vol: '47,354,640',
      },
      {
        keyword: 'ebay buy',
        kd: '-',
        cpc: '$0',
        vol: 'n/a',
      },
    ],
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
    [ACCORDION]: [
      {
        keyword: 'www.ebay.com',
        kd: '11.2',
        cpc: '$3.4',
        vol: '65,457,920',
      },
      {
        keyword: 'www.ebay.com',
        kd: '10',
        cpc: '$0.65',
        vol: '47,354,640',
      },
      {
        keyword: 'ebay buy',
        kd: '-',
        cpc: '$0',
        vol: 'n/a',
      },
    ],
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
  {
    keyword: 'ebay buy',
    kd: '-',
    cpc: '$0',
    vol: 'n/a',
  },
  {
    keyword: 'ebay buy',
    kd: '75.89',
    cpc: '$0',
    vol: '21,644,290',
  },
];

export default Demo;

Custom accordion content

You can also set a single cell as the accordion trigger, and customize the accordion content.

tsx
import { Plot, Line, XAxis, YAxis, ResponsiveContainer, minMax } from '@semcore/ui/d3-chart';
import type { DataTableData } from '@semcore/ui/data-table';
import { DataTable, ACCORDION } from '@semcore/ui/data-table';
import { scaleLinear } from 'd3-scale';
import React from 'react';

export type AccordionInTableProps = {
  loading: boolean;
};

const Demo = (props: AccordionInTableProps) => {
  return (
    <DataTable
      loading={props.loading}
      data={data}
      aria-label='Accordion inside table'
      h='100%'
      defaultGridTemplateColumnWidth='1fr'
      columns={[
        { name: 'keyword', children: 'Keyword', gtcWidth: 'minmax(20%, 50%)' },
        {
          name: 'group',
          children: 'Organic Sessions',
          borders: 'both',
          columns: [
            { name: 'kd', children: 'KD %' },
            { name: 'cpc', children: 'CPC' },
            { name: 'vol', children: 'Vol.' },
          ],
        },
      ]}
    />
  );
};

export const accordionInsideTableDefaultProps = {
  loading: false,
};

Demo.defaultProps = accordionInsideTableDefaultProps;

const ChartExample = () => {
  const [[width, height], setSize] = React.useState([600, 300]);
  const MARGIN = 40;
  const [dataChart, setDataChart] = React.useState<any[]>([]);

  React.useEffect(() => {
    const dataChart = Array(20)
      .fill({})
      .map((d, i) => ({
        x: i,
        y: Math.random() * 10,
      }));
    setDataChart(dataChart);
  }, []);
  const xScale = scaleLinear()
    .range([MARGIN, width - MARGIN])
    .domain(minMax(dataChart, 'x'));
  const yScale = scaleLinear()
    .range([height - MARGIN, MARGIN])
    .domain([0, 10]);
  return (
    <ResponsiveContainer onResize={setSize} h={300} w='100%' style={{ background: '#fff' }}>
      <Plot
        data={dataChart}
        scale={[xScale, yScale]}
        width={width}
        height={height}
        style={{ background: '#fff' }}
      >
        <YAxis>
          <YAxis.Ticks />
          <YAxis.Grid />
        </YAxis>
        <XAxis>
          <XAxis.Ticks />
        </XAxis>
        <Line x='x' y='y'>
          <Line.Dots display />
        </Line>
      </Plot>
    </ResponsiveContainer>
  );
};

const data: DataTableData = [
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
    [ACCORDION]: <ChartExample />,
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: {
      toString: () => '65,457,920',
      [ACCORDION]: <ChartExample />,
    },
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
    [ACCORDION]: <ChartExample />,
  },
  {
    keyword: 'ebay buy',
    kd: '-',
    cpc: '$0',
    vol: 'n/a',
    [ACCORDION]: <ChartExample />,
  },
  {
    keyword: 'ebay buy',
    kd: '75.89',
    cpc: '$0',
    vol: '21,644,290',
    [ACCORDION]: <ChartExample />,
  },
];

export default Demo;

Accordion with fixed column

tsx
import { DataTable, ACCORDION } from '@semcore/ui/data-table';
import type { DataTableData, DataTableProps } from '@semcore/ui/data-table';
import React from 'react';
export type AccordionInTableProps = {
  loading: boolean;
};
const Demo = (props: AccordionInTableProps) => {
  return (
    <DataTable
      data={data}
      aria-label='Parent with fixed column'
      h='100%'
      loading={props.loading}
      wMax={600}
      columns={[
        { name: 'keyword', children: 'Keyword', gtcWidth: '400px', fixed: 'left' },
        { name: 'kd', children: 'KD %', gtcWidth: '150px' },
        { name: 'cpc', children: 'CPC', gtcWidth: '150px' },
        { name: 'vol', children: 'Vol.', gtcWidth: '100px' },
      ]}
    />
  );
};
export const accordionTableDefaultProps = {
  loading: false,
};

Demo.defaultProps = accordionTableDefaultProps;
const data = [
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
    [ACCORDION]: [
      {
        keyword: 'www.ebay.com',
        kd: '11.2',
        cpc: '$3.4',
        vol: '65,457,920',
      },
      {
        keyword: 'www.ebay.com',
        kd: '10',
        cpc: '$0.65',
        vol: '47,354,640',
      },
      {
        keyword: 'ebay buy',
        kd: '-',
        cpc: '$0',
        vol: 'n/a',
      },
      {
        keyword: 'ebay buy',
        kd: '75.89',
        cpc: '$0',
        vol: '21,644,290',
      },
    ],
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
  {
    keyword: 'ebay buy',
    kd: '-',
    cpc: '$0',
    vol: 'n/a',
  },
  {
    keyword: 'ebay buy',
    kd: '75.89',
    cpc: '$0',
    vol: '21,644,290',
  },
];

export default Demo;

Export to image

tsx
import { Flex } from '@semcore/ui/base-components';
import Button from '@semcore/ui/button';
import { DataTable } from '@semcore/ui/data-table';
import DropdownMenu from '@semcore/ui/dropdown-menu';
import FileExportM from '@semcore/ui/icon/FileExport/m';
import React from 'react';

const extensions = ['png', 'jpeg', 'webp'];

const Demo = () => {
  const tableRef = React.useRef<HTMLDivElement>(null);
  const ref = React.useRef<SVGForeignObjectElement>(null);
  const width = 500;
  const height = 300;

  const downloadImage = React.useCallback(
    (extention: string) => async () => {
      if (tableRef.current) {
        ref.current?.append(tableRef.current.cloneNode(true));

        const svgElement = ref.current?.parentElement;

        if (svgElement) {
          let svgText = svgElementToSvgText(svgElement);
          svgText = svgText.replace('xmlns="http://www.w3.org/1999/xhtml"', '');
          svgText = svgText.replace(/(\w+)?:?xlink=/g, 'xmlns:xlink='); // Fix root xlink without namespace
          svgText = svgText.replace(/NS\d+:href/g, 'xlink:href'); // Safari NS namespace fix

          const downloadUrl = await svgText2DownloadUrl(svgText, 2 * width, 2 * height, extention);

          const link = document.createElement('a');
          link.href = downloadUrl;
          link.download = `image.${extention}`;

          link.dispatchEvent(
            new MouseEvent('click', {
              bubbles: true,
              cancelable: true,
              view: window,
            }),
          );

          setTimeout(() => {
            link.remove();
          }, 100);
        }
      }
    },
    [],
  );

  return (
    <Flex>
      <div style={{ display: 'none' }}>
        <svg xmlns='http://www.w3.org/2000/svg' width='500' height='300' aria-hidden='true'>
          <foreignObject width='100%' height='100%' ref={ref} />
        </svg>
      </div>

      <DataTable
        data={data}
        aria-label='Export in image'
        ref={tableRef}
        w={500}
        columns={[
          { name: 'keyword', children: 'Keyword' },
          { name: 'kd', children: 'KD %' },
          { name: 'cpc', children: 'CPC' },
          { name: 'vol', children: 'Vol.' },
        ]}
      />

      <DropdownMenu>
        <DropdownMenu.Trigger tag={Button} ml={4}>
          <Button.Addon>
            <FileExportM />
          </Button.Addon>
          <Button.Text>Export</Button.Text>
        </DropdownMenu.Trigger>
        <DropdownMenu.Popper wMax='257px' aria-label='Extensions'>
          <DropdownMenu.List>
            {extensions.map((name) => (
              <DropdownMenu.Item key={name} onClick={downloadImage(name)}>
                {name}
              </DropdownMenu.Item>
            ))}
          </DropdownMenu.List>
        </DropdownMenu.Popper>
      </DropdownMenu>
    </Flex>
  );
};

const data = [
  {
    keyword: 'ebay buy',
    kd: '77.8',
    cpc: '$1.25',
    vol: '32,500,000',
  },
  {
    keyword: 'www.ebay.com',
    kd: '11.2',
    cpc: '$3.4',
    vol: '65,457,920',
  },
  {
    keyword: 'www.ebay.com',
    kd: '10',
    cpc: '$0.65',
    vol: '47,354,640',
  },
  {
    keyword: 'ebay buy',
    kd: '-',
    cpc: '$0',
    vol: 'n/a',
  },
  {
    keyword: 'ebay buy',
    kd: '75.89',
    cpc: '$0',
    vol: '21,644,290',
  },
];

const getCSSStyles = (parentElement: Element) => {
  const selectorTextArr: string[] = [];

  for (let c = 0; c < parentElement.classList.length; c++) {
    if (!selectorTextArr.includes(`.${parentElement.classList[c]}`))
      selectorTextArr.push(`.${parentElement.classList[c]}`);
  }

  // Add Children element Ids and Classes to the list
  const nodes = parentElement.getElementsByTagName('*');
  for (let i = 0; i < nodes.length; i++) {
    const id = nodes[i].id;
    if (!selectorTextArr.includes(`#${id}`)) selectorTextArr.push(`#${id}`);

    const classes = nodes[i].classList;
    for (let c = 0; c < classes.length; c++)
      if (!selectorTextArr.includes(`.${classes[c]}`)) selectorTextArr.push(`.${classes[c]}`);
  }

  // Extract CSS Rules
  let extractedCSSText = '';
  for (let i = 0; i < document.styleSheets.length; i++) {
    const s = document.styleSheets[i];

    try {
      if (!s.cssRules) continue;
    } catch (e: any) {
      if (e.name !== 'SecurityError') throw e; // for Firefox
      continue;
    }

    const cssRules: any = s.cssRules;
    for (let r = 0; r < cssRules.length; r++) {
      if (
        cssRules[r].selectorText &&
        selectorTextArr.some((s) => cssRules[r].selectorText.includes(s))
      )
        extractedCSSText += cssRules[r].cssText;
    }
  }
  return extractedCSSText;
};

const appendCSS = (cssText: string, element: Element) => {
  const styleElement = document.createElement('style');
  styleElement.setAttribute('type', 'text/css');
  styleElement.innerHTML = cssText;
  const refNode = element.hasChildNodes() ? element.children[0] : null;
  element.insertBefore(styleElement, refNode);
};

const svgElementToSvgText = (svgNode: Element) => {
  svgNode.setAttribute('xlink', 'http://www.w3.org/1999/xlink');
  const cssStyleText = getCSSStyles(svgNode);
  appendCSS(cssStyleText, svgNode);

  const serializer = new XMLSerializer();

  const svgString = serializer.serializeToString(svgNode);

  return svgString;
};

const svgText2DownloadUrl = async (svg: string, width: number, height: number, format: string) =>
  new Promise<string>((resolve, reject) => {
    const imgsrc = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svg)))}`;

    const canvas = document.createElement('canvas');
    const context = canvas.getContext('2d');

    canvas.width = width;
    canvas.height = height;

    const image = new Image();
    image.onload = function () {
      context?.clearRect(0, 0, width, height);
      context?.drawImage(image, 0, 0, width, height);

      const img = canvas.toDataURL(`image/${format}`);
      resolve(img);
    };
    image.onerror = reject;

    image.src = imgsrc;
  });

export default Demo;

Released under the MIT License.

Released under the MIT License.