# CustomColumns

**Category:** Features/Display/Custom Columns

## Design

### Description

Allows users to control the display behavior of [`Table`](./?path=/story/base-components-collections-table-table--table) columns. For more information, see [Custom Columns Overview](./?path=/story/features-display-custom-columns--overview).

```tsx
import { CustomColumns } from '@wix/patterns';
```

### Basic

A basic table with the custom columns feature.

```tsx
import { Avatar } from '@wix/design-system';
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  CustomColumns,
  OffsetQuery,
  Table,
  useTableCollection,
} from '@wix/patterns';
import { contacts } from '@wix/crm';

function VisibleColumns() {

  const state = useTableCollection<contacts.Contact>({
    queryName: 'contacts-VisibleColumns',
    itemName: (item) => `${item.info?.name?.first} ${item.info?.name?.last}`,
    fetchData: (query: OffsetQuery) => {
      const { limit, offset, search, filters } = query;

      let queryBuilder = contacts.queryContacts().limit(limit).skip(offset);

      if (search) {
        queryBuilder = queryBuilder.startsWith('info.name.first', search);
      }

      return queryBuilder.find().then(({ items = [], totalCount: total }) => ({
        items,
        total,
      }));
    },
    fetchErrorMessage: () => 'Error fetching contacts',
    filters: {},
  });

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Contacts' }} />
      <CollectionPage.Content>
        <Table
          state={state}
          columns={[
            {
              id: 'avatar',
              name: 'Avatar',
              title: '',
              width: '50px',
              render: (contact) => (
                <Avatar
                  name={`${contact.info?.name?.first} ${contact.info?.name?.last}`}
                  imgProps={{ src: contact.info?.picture?.image }}
                />
              ),
            },
            {
              id: 'name',
              title: 'Name',
              width: '250px',
              render: (contact) =>
                `${contact.info?.name?.first} ${contact.info?.name?.last}`,
            },
            {
              id: 'level',
              title: 'Level',
              render: (contact) => contact.info?.jobTitle,
            },
            {
              id: 'lastActivity',
              title: 'Last Activity',
              render: (contact) =>
                contact.lastActivity?.activityDate?.toLocaleString(),
            },
          ]}
          customColumns={<CustomColumns />}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

---

### Hide specific columns by default

Customize a column to be hidden by default. 
 
 Set the column's `defaultHidden` property to `true`. 

 **Note:** By default, new columns added to an existing table are visible to users. Set `defaultHidden` to `true` to hide them.

```tsx
import { Avatar } from '@wix/design-system';
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  CustomColumns,
  OffsetQuery,
  Table,
  useTableCollection,
} from '@wix/patterns';
import { contacts } from '@wix/crm';

function DefaultColumnsSelection() {

  const state = useTableCollection<contacts.Contact>({
    queryName: 'contacts-DefaultColumnsSelection',
    itemName: (item) => `${item.info?.name?.first} ${item.info?.name?.last}`,
    fetchData: (query: OffsetQuery) => {
      const { limit, offset, search, filters } = query;

      let queryBuilder = contacts.queryContacts().limit(limit).skip(offset);

      if (search) {
        queryBuilder = queryBuilder.startsWith('info.name.first', search);
      }

      return queryBuilder.find().then(({ items = [], totalCount: total }) => ({
        items,
        total,
      }));
    },
    fetchErrorMessage: () => 'Error fetching contacts',
    filters: {},
  });

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Contacts' }} />
      <CollectionPage.Content>
        <Table
          state={state}
          columns={[
            {
              id: 'avatar',
              name: 'Avatar',
              title: '',
              width: '50px',
              render: (contact) => (
                <Avatar
                  name={`${contact.info?.name?.first} ${contact.info?.name?.last}`}
                  imgProps={{ src: contact.info?.picture?.image }}
                />
              ),
            },
            {
              id: 'name',
              title: 'Name',
              width: '250px',
              render: (contact) =>
                `${contact.info?.name?.first} ${contact.info?.name?.last}`,
            },
            {
              id: 'level',
              title: 'Level',
              render: (contact) => contact.info?.jobTitle,
            },
            {
              id: 'lastActivity',
              title: 'Last Activity',
              render: (contact) =>
                contact.lastActivity?.activityDate?.toLocaleString(),
              defaultHidden: true,
            },
          ]}
          customColumns={<CustomColumns />}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

---

### Lock columns

Locking columns restricts users from editing the specified columns and disables the hide and show functionality (checkbox).

```tsx
import { Avatar } from '@wix/design-system';
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  CustomColumns,
  OffsetQuery,
  Table,
  useTableCollection,
} from '@wix/patterns';
import { contacts } from '@wix/crm';

function DisabledCustomColumns() {

  const state = useTableCollection<contacts.Contact>({
    queryName: 'contacts-DisabledCustomColumns',
    itemName: (item) => `${item.info?.name?.first} ${item.info?.name?.last}`,
    fetchData: (query: OffsetQuery) => {
      const { limit, offset, search, filters } = query;

      let queryBuilder = contacts.queryContacts().limit(limit).skip(offset);

      if (search) {
        queryBuilder = queryBuilder.startsWith('info.name.first', search);
      }

      return queryBuilder.find().then(({ items = [], totalCount: total }) => ({
        items,
        total,
      }));
    },
    fetchErrorMessage: () => 'Error fetching contacts',
    filters: {},
  });

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Contacts' }} />
      <CollectionPage.Content>
        <Table
          state={state}
          columns={[
            {
              id: 'avatar',
              name: 'Avatar',
              title: '',
              width: '50px',
              hideable: false,
              render: (contact) => (
                <Avatar
                  name={`${contact.info?.name?.first} ${contact.info?.name?.last}`}
                  imgProps={{ src: contact.info?.picture?.image }}
                />
              ),
            },
            {
              id: 'name',
              title: 'Name',
              width: '250px',
              render: (contact) =>
                `${contact.info?.name?.first} ${contact.info?.name?.last}`,
            },
            {
              id: 'level',
              title: 'Level',
              render: (contact) => contact.info?.jobTitle,
            },
            {
              id: 'lastActivity',
              title: 'Last Activity',
              hideable: false,
              render: (contact) =>
                contact.lastActivity?.activityDate?.toLocaleString(),
            },
          ]}
          customColumns={<CustomColumns />}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

---

### Exclude columns

Excluding columns restricts users from editing the specified columns and hides them from the custom column selection panel.

```tsx
import { Avatar } from '@wix/design-system';
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  CustomColumns,
  OffsetQuery,
  Table,
  useTableCollection,
} from '@wix/patterns';
import { contacts } from '@wix/crm';

function HiddenColumns() {

  const state = useTableCollection<contacts.Contact>({
    queryName: 'contacts-HiddenColumns',
    itemName: (item) => `${item.info?.name?.first} ${item.info?.name?.last}`,
    fetchData: (query: OffsetQuery) => {
      const { limit, offset, search, filters } = query;

      let queryBuilder = contacts.queryContacts().limit(limit).skip(offset);

      if (search) {
        queryBuilder = queryBuilder.startsWith('info.name.first', search);
      }

      return queryBuilder.find().then(({ items = [], totalCount: total }) => ({
        items,
        total,
      }));
    },
    fetchErrorMessage: () => 'Error fetching contacts',
    filters: {},
  });

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Contacts' }} />
      <CollectionPage.Content>
        <Table
          state={state}
          columns={[
            {
              id: 'avatar',
              name: 'Avatar',
              hiddenFromCustomColumnsSelection: true,
              title: '',
              width: '50px',
              render: (contact) => (
                <Avatar
                  name={`${contact.info?.name?.first} ${contact.info?.name?.last}`}
                  imgProps={{ src: contact.info?.picture?.image }}
                />
              ),
            },
            {
              id: 'name',
              title: 'Name',
              width: '250px',
              render: (contact) =>
                `${contact.info?.name?.first} ${contact.info?.name?.last}`,
            },
            {
              id: 'level',
              title: 'Level',
              render: (contact) => contact.info?.jobTitle,
            },
            {
              id: 'lastActivity',
              title: 'Last Activity',
              render: (contact) =>
                contact.lastActivity?.activityDate?.toLocaleString(),
            },
          ]}
          customColumns={<CustomColumns />}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

---

### Add a tooltip

Add a tooltip next to the column's name. The tooltip allows you to add more information about the column.

```tsx
import { Avatar } from '@wix/design-system';
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  Table,
  useTableCollection,
  CustomColumns,
  OffsetQuery,
} from '@wix/patterns';
import { contacts } from '@wix/crm';

function InfoTooltip() {

  const state = useTableCollection<contacts.Contact>({
    queryName: 'contacts-InfoTooltip',
    itemName: (item) => `${item.info?.name?.first} ${item.info?.name?.last}`,
    fetchData: (query: OffsetQuery) => {
      const { limit, offset, search, filters } = query;

      let queryBuilder = contacts.queryContacts().limit(limit).skip(offset);

      if (search) {
        queryBuilder = queryBuilder.startsWith('info.name.first', search);
      }

      return queryBuilder.find().then(({ items = [], totalCount: total }) => ({
        items,
        total,
      }));
    },
    fetchErrorMessage: ({ err }) => String(err),
    filters: {},
  });

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Contacts' }} />
      <CollectionPage.Content>
        <Table
          state={state}
          columns={[
            {
              id: 'avatar',
              name: 'Avatar',
              title: '',
              width: '50px',
              hideable: false,
              infoTooltipProps: {
                panelContent: 'Avatar info',
              },
              render: (contact) => (
                <Avatar
                  name={`${contact.info?.name?.first} ${contact.info?.name?.last}`}
                  imgProps={{ src: contact.info?.picture?.image }}
                />
              ),
            },
            {
              id: 'name',
              title: 'Name',
              width: '250px',
              render: (contact) =>
                `${contact.info?.name?.first} ${contact.info?.name?.last}`,
            },
            {
              id: 'level',
              title: 'Level',
              render: (contact) => contact.info?.jobTitle,
              infoTooltipProps: {
                content: 'Level info',
              },
            },
            {
              id: 'lastActivity',
              title: 'Last Activity',
              render: (contact) =>
                contact.lastActivity?.activityDate?.toLocaleString(),
            },
          ]}
          customColumns={<CustomColumns />}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

---

### Reorder columns

Drag and drop columns in the side panel to reorder the columns. 

 When adding new columns to an existing table component, the custom column order is preserved, and new columns are appended to the right of the existing columns. However, if the columns are still in their original order, the updated table overrides it.

```tsx
import { Avatar } from '@wix/design-system';
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  Table,
  useTableCollection,
  CustomColumns,
  Views,
  OffsetQuery,
} from '@wix/patterns';
import { contacts } from '@wix/crm';

function ReorderColumns() {

  const state = useTableCollection<contacts.Contact>({
    queryName: 'contacts-ReorderColumns',
    itemName: (item) => `${item.info?.name?.first} ${item.info?.name?.last}`,
    fetchData: (query: OffsetQuery) => {
      const { limit, offset, search, filters } = query;

      let queryBuilder = contacts.queryContacts().limit(limit).skip(offset);

      if (search) {
        queryBuilder = queryBuilder.startsWith('info.name.first', search);
      }

      return queryBuilder.find().then(({ items = [], totalCount: total }) => ({
        items,
        total,
      }));
    },
    fetchErrorMessage: ({ err }) => String(err),
    filters: {},
  });

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Contacts' }} />
      <CollectionPage.Content>
        <Table
          state={state}
          columns={[
            {
              id: 'avatar',
              name: 'Avatar',
              title: '',
              width: '50px',
              hideable: false,
              infoTooltipProps: {
                panelContent: 'Avatar info',
              },
              render: (contact) => (
                <Avatar
                  name={`${contact.info?.name?.first} ${contact.info?.name?.last}`}
                  imgProps={{ src: contact.info?.picture?.image }}
                />
              ),
            },
            {
              id: 'name',
              title: 'Name',
              width: '250px',
              render: (contact) =>
                `${contact.info?.name?.first} ${contact.info?.name?.last}`,
            },
            {
              id: 'level',
              title: 'Level',
              render: (contact) => contact.info?.jobTitle,
            },
            {
              id: 'lastActivity',
              title: 'Last Activity',
              render: (contact) =>
                contact.lastActivity?.activityDate?.toLocaleString(),
            },
          ]}
          views={<Views />}
          customColumns={<CustomColumns />}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

---

### Freeze column reorder

To freeze columns and prevent users from reordering them, use the `reorderDisabled` prop. This disables the drag and drop functionality for the specified columns. If a table has a horizontal scroll feature, the frozen columns are sticky. 
 
>**Note**: You can only freeze columns in a table starting from the first column, and in consecutive order.

```tsx
import { Avatar } from '@wix/design-system';
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  CustomColumns,
  OffsetQuery,
  Table,
  useTableCollection,
  Views,
} from '@wix/patterns';
import { contacts } from '@wix/crm';

function ReorderDisabled() {

  const state = useTableCollection<contacts.Contact>({
    queryName: 'contacts-ReorderDisabled',
    itemName: (item) => `${item.info?.name?.first} ${item.info?.name?.last}`,
    fetchData: (query: OffsetQuery) => {
      const { limit, offset, search, filters } = query;

      let queryBuilder = contacts.queryContacts().limit(limit).skip(offset);

      if (search) {
        queryBuilder = queryBuilder.startsWith('info.name.first', search);
      }

      return queryBuilder.find().then(({ items = [], totalCount: total }) => ({
        items,
        total,
      }));
    },
    fetchErrorMessage: ({ err }) => String(err),
    filters: {},
  });

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Contacts' }} />
      <CollectionPage.Content>
        <Table
          state={state}
          horizontalScroll
          columns={[
            {
              id: 'avatar',
              name: 'Avatar',
              title: '',
              width: '50px',
              hideable: false,
              reorderDisabled: true,
              infoTooltipProps: {
                panelContent: 'Avatar info',
              },
              render: (contact) => (
                <Avatar
                  name={`${contact.info?.name?.first} ${contact.info?.name?.last}`}
                  imgProps={{ src: contact.info?.picture?.image }}
                />
              ),
            },
            {
              id: 'name',
              title: 'Name',
              width: '150px',
              reorderDisabled: true,
              render: (contact) =>
                `${contact.info?.name?.first} ${contact.info?.name?.last}`,
            },
            {
              id: 'level',
              title: 'Level',
              width: '80px',
              render: (contact) => contact.info?.jobTitle,
            },
            {
              id: 'lastActivity',
              title: 'Last Activity',
              width: '80px',
              render: (contact) =>
                contact.lastActivity?.activityDate?.toLocaleString(),
            },
            ...new Array(3).fill(null).flatMap((_, i) => [
              {
                id: `name-${i}`,
                title: `Name ${i}`,
                width: '150px',
                render: (contact: contacts.Contact) =>
                  `${contact.info?.name?.first} ${contact.info?.name?.last}`,
              },
              {
                id: `level-${i}`,
                title: `Level ${i}`,
                render: (contact: contacts.Contact) => contact.info?.jobTitle,
                width: '80px',
              },
              {
                id: `lastActivity-${i}`,
                title: 'Last Activity',
                render: (contact: contacts.Contact) =>
                  contact.lastActivity?.activityDate?.toLocaleString(),
                width: '80px',
              },
            ]),
          ]}
          views={<Views />}
          customColumns={<CustomColumns />}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

---

### Load only selected columns

To achieve greater optimization, consider utilizing the `refreshOnColumnsChange` flag with `true` value and retrieving the `columns` array from the query object. This approach enables you to retrieve the necessary fields from your backend.

```tsx
import { Avatar } from '@wix/design-system';
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  Table,
  useTableCollection,
  Views,
  CustomColumns,
  OffsetQuery,
} from '@wix/patterns';
import { contacts } from '@wix/crm';

function LoadOnlySelectedColumns() {

  const state = useTableCollection<contacts.Contact>({
    queryName: 'contacts-LoadOnlySelectedColumns',
    refreshOnColumnsChange: true,
    itemName: (item) => `${item.info?.name?.first} ${item.info?.name?.last}`,
    fetchData: (query: OffsetQuery) => {
      const { limit, offset, search, filters } = query;

      let queryBuilder = contacts.queryContacts().limit(limit).skip(offset);

      if (search) {
        queryBuilder = queryBuilder.startsWith('info.name.first', search);
      }

      return queryBuilder.find().then(({ items = [], totalCount: total }) => ({
        items,
        total,
      }));
    },
    fetchErrorMessage: ({ err }) => String(err),
    filters: {},
  });

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Contacts' }} />
      <CollectionPage.Content>
        <Table
          state={state}
          columns={[
            {
              id: 'avatar',
              name: 'Avatar',
              title: '',
              width: '50px',
              hideable: false,
              infoTooltipProps: {
                panelContent: 'Avatar info',
              },
              render: (contact) => (
                <Avatar
                  name={`${contact.info?.name?.first} ${contact.info?.name?.last}`}
                  imgProps={{ src: contact.info?.picture?.image }}
                />
              ),
            },
            {
              id: 'name',
              title: 'Name',
              width: '250px',
              render: (contact) =>
                `${contact.info?.name?.first} ${contact.info?.name?.last}`,
            },
            {
              id: 'level',
              title: 'Level',
              render: (contact) => contact.info?.jobTitle,
            },
            {
              id: 'lastActivity',
              title: 'Last Activity',
              render: (contact) =>
                contact.lastActivity?.activityDate?.toLocaleString(),
            },
          ]}
          views={<Views />}
          customColumns={<CustomColumns />}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

---

### Get visible columns

Retrieve the visible columns from the custom columns feature.

```tsx
import { Avatar } from '@wix/design-system';
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  CustomColumns,
  OffsetQuery,
  Table,
  useTableCollection,
  runInAction,
} from '@wix/patterns';
import { contacts } from '@wix/crm';
import { PrimaryActions } from '../../../src';

function VisibleColumns() {

  const state = useTableCollection<contacts.Contact>({
    queryName: 'contacts-GetVisibleColumns',
    itemName: (item) => `${item.info?.name?.first} ${item.info?.name?.last}`,
    fetchData: (query: OffsetQuery) => {
      const { limit, offset, search, filters } = query;

      let queryBuilder = contacts.queryContacts().limit(limit).skip(offset);

      if (search) {
        queryBuilder = queryBuilder.startsWith('info.name.first', search);
      }

      return queryBuilder.find().then(({ items = [], totalCount: total }) => ({
        items,
        total,
      }));
    },
    fetchErrorMessage: () => 'Error fetching contacts',
    filters: {},
  });

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header
        title={{ text: 'Contacts' }}
        primaryAction={
          <PrimaryActions
            label="Settings"
            onClick={() => {
              runInAction(() => {
                console.log(state.visibleColumns);
              });
            }}
          />
        }
      />
      <CollectionPage.Content>
        <Table
          state={state}
          columns={[
            {
              id: 'avatar',
              name: 'Avatar',
              title: '',
              width: '50px',
              render: (contact) => (
                <Avatar
                  name={`${contact.info?.name?.first} ${contact.info?.name?.last}`}
                  imgProps={{ src: contact.info?.picture?.image }}
                />
              ),
            },
            {
              id: 'name',
              title: 'Name',
              width: '250px',
              render: (contact) =>
                `${contact.info?.name?.first} ${contact.info?.name?.last}`,
            },
            {
              id: 'level',
              title: 'Level',
              render: (contact) => contact.info?.jobTitle,
            },
            {
              id: 'lastActivity',
              title: 'Last Activity',
              render: (contact) =>
                contact.lastActivity?.activityDate?.toLocaleString(),
            },
          ]}
          customColumns={<CustomColumns />}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

## API

### API

Columns are defined on the `Table` component, and their properties affect the behavior of the custom columns feature. For more information, see the [column properties](./?path=/story/features-display-custom-columns--overview#column-properties) section of the Custom Columns Overview.

### Props

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `onColumnClick` | `((column: Column, checked: boolean) => unknown)` | No | - | Adds a callback that is called when a column is clicked. @param column @param checked |

## BI

### Custom Columns Events

| Event                                                                                                     | Description                                                        |
| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| [144:119](https://bo.wix.com/data-tools/bi-catalog-app/event/144:119) | Sent when a feature sidebar/menu in a Wix Patterns component is opened or closed |
| [144:177](https://bo.wix.com/data-tools/bi-catalog-app/event/144:177) | Sent when a user is making his first change in visible columns- add / remove / reorder them- for every time he opened the side panel |



### Component load events
Event   |   Description   |
--------- | --------------- |
[144:110](https://bo.wix.com/data-tools/bi-catalog-app/event/144:110) | Sent when a Wix Patterns component starts loading
[144:111](https://bo.wix.com/data-tools/bi-catalog-app/event/144:111) | Sent when a Wix Patterns component is done loading


#### 🐪 Couldn't find what you need?
* Check our [BI Catalog](https://bo.wix.com/data-tools/bi-catalog-app?viewId=all-items-view&selectedColumns=src%2Cevid%2Cname%2Cowner%2Cproduct%2CuserType+false%2CdateUpdated%2CdateCreated+false%2CcreatedBy+false%2Cstatus&source=%5B%7B%22id%22%3A%22144%22%2C%22name%22%3A%22144+-+Cairo%22%7D%5D)
* Contact us on [#cairo-bi](https://wix.slack.com/archives/C03N53KURH9)


## Testkit

### Usage

All `` testkit APIs are exposed directly from the `` [testkit](./?path=/story/base-components-collections-table-table--table&tab=Testkit).


