---
name: Table V2
menu: Components
route: /components/table-v2
---

import { Playground } from 'docz'
import classnames from 'classnames'

import { TableV2 as Table, Icon } from '@personio/ui-components'

import './fullscreen-example.scss' 
import './custom-styling.scss'

# Table

This component renders structured data as an HTML `<table>` element.

The overall API is data-based (as opposed to component-based). This means, the component processes column definitions and data as input. The actual rendering is done internally. For customization purposes, render props for rows, head cells and cells can be passed in.

The component responsibilities are: render tabular data, apply default styling, provide fixed header and columns, provide row selection, provide sorting interaction elements. Other specific customization with regards to individual table cells and/or rows is not scope of this component. Rather, such things should be solved with individual render functions.

## Columns and data

The component expects two separate object arrays as input. 

`columns` must be a collection of definitions that describe the contained columns. Each item in this array is an object with the following structure: `{ key?, label?, renderTd?, renderTh? }`. The string `key` serves multiple purposes. For one, it is the name of the property to access for each row. Secondly, it can be used to uniquely identify a column. And third, the value is also used as default React key for `<th>` and `<td>` elements. The string `label` is solely used as content of the `<th>` element. Both `renderTd()` and `renderTh()` are optional column-specific customer render functions (see next section).

`data` is a set of data items to be rendered as table rows. Each item in this array can have an arbitrary structure. The properties to show must match the names of the respective column `key` values.

**Note:** Currently, the `key` attribute only supports basic property names. Consequently, the values to render can only be contained in direct properties of a data row. This means, nested object structures are currently not supported.

## Custom render functions

For customization of rows, head cells and body cells, the component exposes the three optional render props `renderTr()`, `renderTh()`, `renderTd()`.
By default, the component uses the internally defined standard render functions.
Both `renderTd()` and `renderTh()` can also be defined a column level (see section above).
For the interface/signature of the individual render functions, look at the advanced examples or the source code. 

## Basic example

<Playground>
  <Table 
    className="myFancyTable"
    columns={[
      { key: 'firstName', label: 'First Name' },
      { key: 'lastName', label: 'Last Name' },
      { key: 'position', label: 'Position' },
      { key: 'email', label: 'E-Mail Address' },
    ]}
    data={[
      { firstName: 'John', lastName: 'Doe', position: 'Engineer', email: 'john.doe@example.com' },
      { firstName: 'Jane', lastName: 'Doe', position: 'Designer', email: 'jane.doe@example.com' },
    ]}
  />
</Playground>

## Fixed header

<Playground>
  <Table 
    fixedHeader
    columns={[
      { key: 'firstName', label: 'First Name' },
      { key: 'lastName', label: 'Last Name' },
      { key: 'position', label: 'Position' },
      { key: 'email', label: 'E-Mail Address' },
    ]}
    data={[
      { firstName: 'John', lastName: 'Doe', position: 'Engineer', email: 'john.doe@example.com' },
      { firstName: 'Jane', lastName: 'Doe', position: 'Designer', email: 'jane.doe@example.com' },
      { firstName: 'Jim', lastName: 'Doe', position: 'Product Manager', email: 'jim.doe@example.com' },
      { firstName: 'Han', lastName: 'Solo', position: 'QA Engineer', email: 'han.solo@example.com' },
    ]}
  />
</Playground>

## Fixed columns

<Playground>
  <div style={{ width: '100%', overflow: 'auto' }}>
    <Table 
      fixedColumnCount={2}
      columns={[
        { key: 'firstName', label: 'First Name' },
        { key: 'lastName', label: 'Last Name' },
        { key: 'position', label: 'Position' },
        { key: 'personalNotes', label: 'Personal Notes' },
      ]}
      data={[
        { firstName: 'John', lastName: 'Doe', position: 'Engineer', personalNotes: 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.' },
        { firstName: 'Jane', lastName: 'Doe', position: 'Designer', personalNotes: 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.' },
        { firstName: 'Jim', lastName: 'Doe', position: 'Product Manager', personalNotes: 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.' },
        { firstName: 'Han', lastName: 'Solo', position: 'QA Engineer', personalNotes: 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.' },
      ]}
    />
  </div>
</Playground>

## Sortable columns

The table component provides styles for `<th>` elements of sortable columns.
However, it **does not care of the actual sorting of data**.

Setting the class "sortable" on a `<th>` element indicates that it can be clicked to trigger a sorting.
Sorted columns can be marked with the status classes "sorted-ascending" and "sorted-descending".

<Playground>
  <div>
    <Table 
      renderTh={({ column, columnIndex }) => (
        <th 
          key={column.key}
          onClick={() => alert(`sorting by ${column.key}`)}
          className={classnames(
            'sortable',
            column.key == 'lastName' ? 'sorted-ascending' : null
          )}
        >
          {column.label}
        </th>
      )}
      columns={[
        { key: 'firstName', label: 'First Name' },
        { key: 'lastName', label: 'Last Name' },
        { key: 'position', label: 'Position' },
        { key: 'email', label: 'E-Mail Address' },
      ]}
      data={[
        { firstName: 'John', lastName: 'Doe', position: 'Engineer', email: 'john.doe@example.com' },
        { firstName: 'Jane', lastName: 'Doe', position: 'Designer', email: 'jane.doe@example.com' },
        { firstName: 'Jim', lastName: 'Doe', position: 'Product Manager', email: 'jim.doe@example.com' },
        { firstName: 'Han', lastName: 'Solo', position: 'QA Engineer', email: 'han.solo@example.com' },
      ]}
    />
  </div>
</Playground>

## Selectable rows

Sometimes, tables need to provide the ability to select rows, both individual ones and all of them.
This should be done by precedding the actual table content with a column that containes checkboxes.

Note that **the actual selection behavior must be implemented customly**.

<Playground>
  <div>
    <Table 
      columns={[
        { 
          renderTh: () => (<th key="select"><input type="checkbox" /></th>),
          renderTd: () => (<td key="select"><input type="checkbox" /></td>),
        },
        { key: 'firstName', label: 'First Name' },
        { key: 'lastName', label: 'Last Name' },
        { key: 'position', label: 'Position' },
      ]}
      data={[
        { firstName: 'John', lastName: 'Doe', position: 'Engineer' },
        { firstName: 'Jane', lastName: 'Doe', position: 'Designer' },
        { firstName: 'Jim', lastName: 'Doe', position: 'Product Manager' },
        { firstName: 'Han', lastName: 'Solo', position: 'QA Engineer' },
      ]}
    />
  </div>
</Playground>

## Highlighted rows

The table components provides styles for row hover, selection and highlighting.
For selected rows, use the "selected" class.
For background highlights, use the according class (currently only "warning" available).

<Playground>
  <div>
    <Table 
      columns={[
        { key: 'firstName', label: 'First Name' },
        { key: 'lastName', label: 'Last Name' },
        { key: 'position', label: 'Position' },
      ]}
      data={[
        { firstName: 'John', lastName: 'Doe', position: 'Engineer' },
        { firstName: 'Jane', lastName: 'Doe', position: 'Designer' },
        { firstName: 'Jim', lastName: 'Doe', position: 'Product Manager' },
        { firstName: 'Han', lastName: 'Solo', position: 'QA Engineer' },
      ]}
      renderTr={({ children, rowIndex }) => (
        <tr 
          className={classnames(
            rowIndex > 1 ? 'selected' : null, 
            rowIndex % 2 == 1 ? 'warning' : null,
          )}
        >
          {children}
        </tr>
      )}
    />
  </div>
</Playground>


## Custom row React Keys

By default, the table component uses the `key` value as React Key for both `<th>` and `<td>` elements.
The property should provide a stable value to uniquely identify the individual elements.
Still, the behavior can be customized by providing custom render functions.

For table rows, the component uses the row index value as standard React Key.
However, this value is not a stable key, which **can affect performance when sorting/filtering data**.
Again, setting a custom table row key can be done by defining a render function for row elements.

<Playground>
  <div>
    <Table 
      renderTr={({ children, dataRow }) => (
        <tr key={dataRow.id}>{children}</tr>
      )}
      columns={[
        { key: 'firstName', label: 'First Name' },
        { key: 'lastName', label: 'Last Name' },
        { key: 'position', label: 'Position' },
        { key: 'email', label: 'E-Mail Address' },
      ]}
      data={[
        { id: 1, firstName: 'John', lastName: 'Doe', position: 'Engineer', email: 'john.doe@example.com' },
        { id: 2, firstName: 'Jane', lastName: 'Doe', position: 'Designer', email: 'jane.doe@example.com' },
        { id: 3, firstName: 'Jim', lastName: 'Doe', position: 'Product Manager', email: 'jim.doe@example.com' },
        { id: 4, firstName: 'Han', lastName: 'Solo', position: 'QA Engineer', email: 'han.solo@example.com' },
      ]}
    />
  </div>
</Playground>

## Custom styling

The selectors for the styling are mostly a combination of a root CSS Module class and element selectors.
For example, the selector for the head row looks like this: `.table thead tr`.
The main reason for this is that the styles should also work when using custom render function.
Otherwise, every consumer would need to set the correct classes manually.

The downside of this approach is that it can cause specificity issues when overriding styles.

#### Setting the className prop

The component provides the `className` property to pass in custom class names to the `<table>` element.
This makes it possible to style the table itself and also its elements (when used in combination with element selectors). 

```css
.myCustomTable {
  border: 1px solid #000;

  td:nth-child(even) {
    background:#F8F8F8;
  }

  td:nth-child(odd) {
    background:#EEE;
  }
}
```

<Playground>
  <Table 
    className="myCustomTable"
    columns={[
      { key: 'firstName', label: 'First Name' },
      { key: 'lastName', label: 'Last Name' },
      { key: 'position', label: 'Position' },
      { key: 'email', label: 'E-Mail Address' },
    ]}
    data={[
      { firstName: 'John', lastName: 'Doe', position: 'Engineer', email: 'john.doe@example.com' },
      { firstName: 'Jane', lastName: 'Doe', position: 'Designer', email: 'jane.doe@example.com' },
      { firstName: 'Jim', lastName: 'Doe', position: 'Product Manager', email: 'jim.doe@example.com' },
      { firstName: 'Han', lastName: 'Solo', position: 'QA Engineer', email: 'han.solo@example.com' },
    ]}
  />
</Playground>

#### Using custom render function

When using a custom render function, there is the possibility to set a custom `className` prop on the respective elements.

```css
.email {
  font-weight: bold;
}
```

<Playground>
  <Table 
    columns={[
      { key: 'firstName', label: 'First Name' },
      { key: 'lastName', label: 'Last Name' },
      { key: 'position', label: 'Position' },
      { 
        key: 'email', 
        label: 'E-Mail Address', 
        renderTd: ({ content }) => (<td className="email">{content}</td>),
      },
    ]}
    data={[
      { firstName: 'John', lastName: 'Doe', position: 'Engineer', email: 'john.doe@example.com' },
      { firstName: 'Jane', lastName: 'Doe', position: 'Designer', email: 'jane.doe@example.com' },
      { firstName: 'Jim', lastName: 'Doe', position: 'Product Manager', email: 'jim.doe@example.com' },
      { firstName: 'Han', lastName: 'Solo', position: 'QA Engineer', email: 'han.solo@example.com' },
    ]}
  />
</Playground>

## Fullscreen table

Many of the tables displayed in Personio should behave similar to fullscreen spreadsheets. 
However, in some situations there needs to be static content above the table. 
This makes it hard to simply display the table at its full dimensions and scroll the browser window. 
One possible solution approach for this is to make the static content sticky in a horizontal way.

Consider the next example where the first `<div>` represents the `root element element of a separate page.
Note that all the elements (sidebar, top bar, preceeding content and table) are **direct** children of the root.

<Playground
style={{ border: '1px dotted #000', padding: 0, transform: 'translateX(0)', height: 500 }}>

  <div id="root" style={{ overflow: 'auto', padding: '50px 0 0 50px' }}>
    <div id="sidebar"></div>
    <div id="top-bar">Top Bar</div>
    <div id="table-preceeding-content" style={{ position: 'sticky', left: 0, paddingLeft: 10 }}>
      <h2>This is some page content above a table.</h2>
      <p>
        Dolore commodo sint officia fugiat magna esse. Irure commodo exercitation commodo proident. Excepteur aute id irure culpa mollit sint exercitation pariatur aute. Tempor do culpa labore dolor non et esse reprehenderit eiusmod. Eiusmod est ipsum sint amet id irure ad occaecat amet exercitation adipisicing. Consectetur deserunt esse est cupidatat sint veniam dolore adipisicing adipisicing sunt laboris. Sit minim reprehenderit consectetur aliqua dolore excepteur consectetur est magna minim dolore adipisicing nisi.
      </p>
    </div>
    <Table
      fixedHeader={true} 
      fixedColumnCount={3}
      columns={ [ 
        {
          renderTh: () => (<th><input type="checkbox" /></th>),
          renderTd: () => (<td><input type="checkbox" /></td>)
        },
        { label: 'First Name', key: 'firstName' },
        { label: 'Last Name', key: 'lastName' },
        { label: 'Status', key: 'status' },
        { label: 'Employment type', key: 'employmentType' },
        { label: 'Email', key: 'email' },
        { label: 'Office', key: 'office' },
        { label: 'Department', key: 'department' },
        { label: 'Position', key: 'position' },
      ] } 
      data={ [ {
        "firstName": "Alan",
        "lastName": "Foster",
        "status": "Active",
        "employmentType": "Internal",
        "email": "Alan.Foster@demo-sample.com",
        "office": "London",
        "department": "Accounting",
        "position": "office Manager"
      },
      {
        "firstName": "Kai",
        "lastName": "Schumacher",
        "status": "Active",
        "employmentType": "Internal",
        "email": "Kai.Schumacher@demo-sample.com",
        "office": "Munich",
        "department": "IT",
        "position": "UX Designer"
      },
      {
        "firstName": "Sebastian",
        "lastName": "Meyer",
        "status": "",
        "employmentType": "Internal",
        "email": "Sebastian.Meyer@demo-sample.com",
        "office": "Munich",
        "department": "Accounting",
        "position": "Budget Analyst"
      },
      {
        "firstName": "Patrick",
        "lastName": "Wagner",
        "status": "Active",
        "employmentType": "Internal",
        "email": "Patrick.Wagner@demo-sample.com",
        "office": "Munich",
        "department": "Accounting",
        "position": "Capital Accountant"
      },
      {
        "firstName": "Norman",
        "lastName": "Scott",
        "status": "Active",
        "employmentType": "Internal",
        "email": "norman.scott@demo-sample.com",
        "office": "London",
        "department": "IT",
        "position": "IT Specialist"
      },
      {
        "firstName": "Marc",
        "lastName": "Porter",
        "status": "Active",
        "employmentType": "Internal",
        "email": "Marc.Porter@demo-sample.com",
        "office": "London",
        "department": "Sales",
        "position": "Head of Sales"
      },
      {
        "firstName": "Manuel",
        "lastName": "Dietrich",
        "status": "Active",
        "employmentType": "Internal",
        "email": "Manuel.Dietrich@demo-sample.com",
        "office": "Munich",
        "department": "Finance & Legal",
        "position": "Working Student"
      },
      {
        "firstName": "Larissa",
        "lastName": "Morgan",
        "status": "Active",
        "employmentType": "Internal",
        "email": "larissa.morgan@demo-sample.com",
        "office": "London",
        "department": "Sales",
        "position": "Senior Sales Manager"
      },
      {
        "firstName": "Karsten",
        "lastName": "Becker",
        "status": "Active",
        "employmentType": "Internal",
        "email": "Karsten.Becker@demo-sample.com",
        "office": "Munich",
        "department": "Customer Support",
        "position": "Account Manager"
      },
      {
        "firstName": "Julian",
        "lastName": "Schneider",
        "status": "Active",
        "employmentType": "Internal",
        "email": "julian.schneider@demo-sample.com",
        "office": "London",
        "department": "Management",
        "position": "CEO"
      },
      {
        "firstName": "Alexander",
        "lastName": "Lawrence",
        "status": "Active",
        "employmentType": "Internal",
        "email": "alex.lawrence.ext@personio.de",
        "office": "Munich",
        "department": "Management",
        "position": "CTO"
      },
      {
        "firstName": "Josephine",
        "lastName": "Friedrich",
        "status": "Active",
        "employmentType": "Internal",
        "email": "Josephine.Friedrich@demo-sample.com",
        "office": "Munich",
        "department": "HR",
        "position": "HR Manager"
      },
      {
        "firstName": "Jörn",
        "lastName": "Peters",
        "status": "Active",
        "employmentType": "Internal",
        "email": "Jörn.Peters@demo-sample.com",
        "office": "Munich",
        "department": "HR",
        "position": "Recruiting Manager"
      },
      {
        "firstName": "Johannes",
        "lastName": "Graf",
        "status": "Active",
        "employmentType": "Internal",
        "email": "Johannes.Graf@demo-sample.com",
        "office": "Munich",
        "department": "Finance & Legal",
        "position": "Finanzberater"
      },
      {
        "firstName": "Jasmin",
        "lastName": "Hofman",
        "status": "Active",
        "employmentType": "Internal",
        "email": "Jasmin.Hofman@demo-sample.com",
        "office": "London",
        "department": "Finance & Legal",
        "position": "Head of Finance"
      },
      {
        "firstName": "Emma",
        "lastName": "Weber",
        "status": "Active",
        "employmentType": "Internal",
        "email": "Emma.Weber@demo-sample.com",
        "office": "Munich",
        "department": "Marketing",
        "position": "Marketing Manager"
      },
      {
        "firstName": "Charlotte",
        "lastName": "Haas",
        "status": "Active",
        "employmentType": "Internal",
        "email": "charlotte.haas@demo-sample.com",
        "office": "Munich",
        "department": "HR",
        "position": "Head of HR"
      },
      {
        "firstName": "Alison",
        "lastName": "Bell",
        "status": "Active",
        "employmentType": "Internal",
        "email": "alison.bell@demo-sample.com",
        "office": "London",
        "department": "Sales",
        "position": "Sales Manager"
      },
      {
        "firstName": "Stephan",
        "lastName": "Klein",
        "status": "Active",
        "employmentType": "Internal",
        "email": "Stephan.Klein@demo-sample.com",
        "office": "Munich",
        "department": "Marketing",
        "position": "Head of Marketing"
      } ] } 
    />
  </div>
</Playground>

