# Keyboard Navigation for Table Rows

Canonical page: https://infinite-table.com/docs/learn/keyboard-navigation/navigating-rows

To enable keyboard navigation for table rows, specify [keyboardNavigation="row"](https://infinite-table.com/docs/reference/infinite-table-props.md#keyboardNavigation) in your React Infinite Table component.

When row navigation is enabled, clicking a row highlights it and the user can use the arrow keys to navigate the table rows.

**Example**

Click on the table and use the arrow keys to navigate the rows.

```ts
import {
  InfiniteTable,
  DataSource,
  DataSourceData,
} from '@infinite-table/infinite-react';
import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react';
import * as React from 'react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};

const dataSource: DataSourceData<Developer> = () => {
  return fetch(
    'https://infinite-table.com/.netlify/functions/json-server' +
      `/developers1k-sql?`,
  )
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const columns: InfiniteTablePropColumns<Developer> = {
  preferredLanguage: { field: 'preferredLanguage' },
  country: { field: 'country' },
  salary: {
    field: 'salary',
    type: 'number',
  },
  age: { field: 'age' },
  canDesign: { field: 'canDesign' },
  firstName: { field: 'firstName' },
  stack: { field: 'stack' },
  id: { field: 'id' },
  hobby: { field: 'hobby' },
  city: { field: 'city' },
  currency: { field: 'currency' },
};

export default function KeyboardNavigationForCells() {
  return (
    <>
      <DataSource<Developer> primaryKey="id" data={dataSource}>
        <InfiniteTable<Developer>
          debugId="navigating-rows-initial-example"
          keyboardNavigation="row"
          columns={columns}
        />
      </DataSource>
    </>
  );
}
```

- Use `ArrowUp` and `ArrowDown` to navigate to the previous and next row.
- Use `PageUp` and `PageDown` to navigate the rows vertically by pages (a page is considered equal to the visible row count).
- Use `Home` and `End` to navigate vertically to the first and last row respectively

Other possible values for the [`keyboardNavigation`](https://infinite-table.com/docs/reference/infinite-table-props.md#keyboardNavigation) prop, besides `"row"`, are `"cell"` and `false`.

## Using a default active row

You can also specify an initial active row, by using [defaultActiveRowIndex=2](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultActiveRowIndex). This tells the table that there should be a default active row, namely the one at index 2 (so the third row).

**Example**

This example starts with row at index `2` already active.

```ts
import {
  InfiniteTable,
  DataSource,
  DataSourceData,
} from '@infinite-table/infinite-react';
import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react';
import * as React from 'react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};

const dataSource: DataSourceData<Developer> = () => {
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`)
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const columns: InfiniteTablePropColumns<Developer> = {
  preferredLanguage: { field: 'preferredLanguage' },
  country: { field: 'country' },
  salary: {
    field: 'salary',
    type: 'number',
  },
  age: { field: 'age' },
  canDesign: { field: 'canDesign' },
  firstName: { field: 'firstName' },
  stack: { field: 'stack' },
  id: { field: 'id' },
  hobby: { field: 'hobby' },
  city: { field: 'city' },
  currency: { field: 'currency' },
};

const domProps = { style: { height: '90vh' } };

export default function KeyboardNavigationForRows() {
  return (
    <>
      <DataSource<Developer> primaryKey="id" data={dataSource}>
        <InfiniteTable<Developer>
          debugId="navigating-rows-uncontrolled-example"
          domProps={domProps}
          columns={columns}
          keyboardNavigation="row"
          defaultActiveRowIndex={2}
        />
      </DataSource>
    </>
  );
}
```

## Listening to active row changes

You can easily listen to changes in the row navigation by using the [`onActiveRowIndexChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onActiveRowIndexChange) callback.

When you use controlled [`activeRowIndex`](https://infinite-table.com/docs/reference/infinite-table-props.md#activeRowIndex), make sure to use [onActiveRowIndexChange](https://infinite-table.com/docs/reference/infinite-table-props.md#onActiveRowIndexChange) to update the prop value, as otherwise the component will not update on navigation

**Example**

This example starts with row at index `2` already active and uses [onActiveRowIndexChange](https://infinite-table.com/docs/reference/infinite-table-props.md#onActiveRowIndexChange) to update [`activeRowIndex`](https://infinite-table.com/docs/reference/infinite-table-props.md#activeRowIndex).

```ts
import {
  InfiniteTable,
  DataSource,
  DataSourceData,
} from '@infinite-table/infinite-react';
import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react';
import * as React from 'react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};

const dataSource: DataSourceData<Developer> = () => {
  return fetch(
    'https://infinite-table.com/.netlify/functions/json-server' +
      `/developers1k-sql?`,
  )
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const columns: InfiniteTablePropColumns<Developer> = {
  preferredLanguage: { field: 'preferredLanguage' },
  country: { field: 'country' },
  salary: {
    field: 'salary',
    type: 'number',
  },
  age: { field: 'age' },
  canDesign: { field: 'canDesign' },
  firstName: { field: 'firstName' },
  stack: { field: 'stack' },
  id: { field: 'id' },
  hobby: { field: 'hobby' },
  city: { field: 'city' },
  currency: { field: 'currency' },
};

export default function KeyboardNavigationForRows() {
  const [activeRowIndex, setActiveRowIndex] = React.useState(2);
  return (
    <>
      <div
        style={{
          color: 'var(--infinite-cell-color)',
        }}
      >
        Current active row: {activeRowIndex}.
      </div>
      <DataSource<Developer> primaryKey="id" data={dataSource}>
        <InfiniteTable<Developer>
          debugId="navigating-rows-controlled-example"
          keyboardNavigation="row"
          activeRowIndex={activeRowIndex}
          onActiveRowIndexChange={setActiveRowIndex}
          columns={columns}
        />
      </DataSource>
    </>
  );
}
```

## Toggling group rows

When the DataSource is [grouped](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy), you can use the keyboard to collapse/expand group rows, by pressing the `Enter` key on the active row.

Since you're in row navigation mode, you can also use

- `←` to collapse a group row
- `→` to expand a group row

**Example**

Press the `Enter` key on the active group row to toggle it. `ArrowLeft` will collapse a group row and `ArrowRight` will expand a group row.

```ts
import { InfiniteTable, DataSource } from '@infinite-table/infinite-react';
import type {
  DataSourceProps,
  InfiniteTableProps,
  InfiniteTablePropColumns,
} from '@infinite-table/infinite-react';
import * as React from 'react';

const columns: InfiniteTablePropColumns<Developer> = {
  country: {
    field: 'country',
  },
  firstName: {
    field: 'firstName',
    defaultHiddenWhenGroupedBy: '*',
  },
  stack: {
    field: 'stack',
  },
  age: { field: 'age' },
  id: { field: 'id' },
  preferredLanguage: {
    field: 'preferredLanguage',
  },
  canDesign: {
    field: 'canDesign',
  },
};

const defaultGroupBy: DataSourceProps<Developer>['groupBy'] = [
  {
    field: 'canDesign',
  },
  {
    field: 'stack',
  },
  {
    field: 'preferredLanguage',
  },
];

const groupColumn: InfiniteTableProps<Developer>['groupColumn'] = {
  field: 'firstName',

  defaultWidth: 300,
};

const domProps = {
  style: {
    flex: 1,
    minHeight: 500,
  },
};

export default function App() {
  return (
    <DataSource<Developer>
      data={dataSource}
      groupBy={defaultGroupBy}
      primaryKey="id"
    >
      <InfiniteTable<Developer>
        debugId="keyboard-toggle-group-rows"
        columns={columns}
        domProps={domProps}
        keyboardNavigation="row"
        hideColumnWhenGrouped
        groupColumn={groupColumn}
        columnDefaultWidth={150}
      />
    </DataSource>
  );
}

const dataSource = () => {
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100')
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  email: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};
```

## Selecting Rows with the Keyboard

When [`rowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowSelection) is enabled (read more about it in the [row selection page](../selection/row-selection)), you can use the spacebar key to select a group row (or `shift` + spacebar to do multiple selection).

By default [`keyboardSelection`](https://infinite-table.com/docs/reference/infinite-table-props.md#keyboardSelection) is enabled, so you can use the **spacebar** key to select multiple rows, when [selectionMode="multi-row"](https://infinite-table.com/docs/reference/datasource-props/index.md#selectionMode). Using the spacebar key is equivalent to doing a mouse click, so expect the combination of **spacebar** + `cmd`/`ctrl`/`shift` modifier keys to behave just like clicking + the same modifier keys.

**Example: Multi row selection with keyboard support**

Use spacebar + optional `cmd`/`ctrl`/`shift` modifier keys just like you would do clicking + the same modifier keys.

```ts
import { InfiniteTable, DataSource } from '@infinite-table/infinite-react';
import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react';
import * as React from 'react';
import { useState } from 'react';

const columns: InfiniteTablePropColumns<Developer> = {
  country: {
    field: 'country',
  },
  firstName: {
    field: 'firstName',
  },
  stack: {
    field: 'stack',
  },
  age: { field: 'age' },
  id: { field: 'id' },
  salary: {
    field: 'salary',
    type: 'number',
  },
  canDesign: { field: 'canDesign' },
};

export default function App() {
  const [keyboardSelection, setKeyboardSelection] = useState(true);
  return (
    <>
      <div
        style={{
          color: 'var(--infinite-cell-color)',
          padding: 10,
        }}
      >
        Keyboard selection is now{' '}
        <b>{keyboardSelection ? 'enabled' : 'disabled'}</b>.
        <button
          style={{
            border: '1px solid tomato',
            display: 'block',
            padding: 5,
            marginTop: 10,
          }}
          onClick={() => setKeyboardSelection((x) => !x)}
        >
          Click to toggle keyboard selection
        </button>
      </div>
      <DataSource<Developer>
        data={dataSource}
        selectionMode="multi-row"
        primaryKey="id"
      >
        <InfiniteTable<Developer>
          debugId="default-selection-mode-multi-row-keyboard-toggle-example-row-navigation"
          keyboardNavigation="row"
          keyboardSelection={keyboardSelection}
          columns={columns}
          columnDefaultWidth={150}
        />
      </DataSource>
    </>
  );
}

const dataSource = () => {
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100')
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  email: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};
```

For selection all the rows in the table, you can use `cmd`/`ctrl` + `A` keyboard shortcut.

Keyboard selection is also possible when there's a column configured with checkbox selection - [make sure you read more about it](../selection/row-selection#using-a-selection-checkbox).

## Theming

By default, the style of the element that highlights the active row is the same style as that of the element that highlights the active cell.

The easiest is to override the style is via those three CSS variables:

- `--infinite-active-cell-border-color--r` - the `red` component of the border color
- `--infinite-active-cell-border-color--g` - the `green` component of the border color
- `--infinite-active-cell-border-color--b` - the `blue` component of the border color

The initial values for those are `77`, `149` and`215` respectively, so the border color is `rgb(77, 149, 215)`.

In addition, the background color of the element that highlights the active row is set to the same color as the border color (computed based on the above `r`, `g` and `b` variables), but with an opacity of `0.25`, configured via the `--infinite-active-row-background-alpha` CSS variable.

When the table is not focused, the opacity for the background color is set to `0.1`, which is the default value of the `--infinite-active-row-background-alpha--table-unfocused` CSS variable.

 
To summarize, use

- `--infinite-active-cell-border-color--r`
- `--infinite-active-cell-border-color--g`
- `--infinite-active-cell-border-color--b`

to control border and background color of the active row highlight element.

No, it's not a mistake that the element that highlights the active row is configured via the same CSS variables as the element that highlights the active cell. This is deliberate - so override CSS variables for cell, and those are propagated to the row highlight element.

There are other CSS variables as well, that give you fined-tuned control over both the border and background color for the active row, if you don't want to use the above three variables to propagate the same color across both border and background.

- `--infinite-active-cell-background` - the background color. If you use this, you need to set opacity yourself. Applied for both cell and row.
- `--infinite-active-row-background` - the background color. If you use this, you need to set opacity yourself. If this is specified, it takes precendence over `--infinite-active-cell-background`
- `--infinite-active-cell-background` - the background color. If you use this, you need to set opacity yourself. Applied for both cell and row.
- `--infinite-active-row-background` - the background color. If this is specified, it takes precedence over `--infinite-active-cell-background`
- `--infinite-active-row-border` - border configuration (eg:`2px solid magenta`). If you use this, it will not be propagated to the background color.

For more details on the CSS variables, see the [CSS Variables documentation](../theming/css-variables##active-row-background).

**Example: Theming active row highlight**

Use the color picker to configured the desired color for the active row highlight

```ts
import {
  InfiniteTable,
  DataSource,
  DataSourceData,
  debounce,
} from '@infinite-table/infinite-react';
import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react';
import * as React from 'react';
import { useState } from 'react';
import { useMemo } from 'react';
import { HTMLProps } from 'react';
import { ChangeEvent } from 'react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};

const dataSource: DataSourceData<Developer> = () => {
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`)
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const columns: InfiniteTablePropColumns<Developer> = {
  preferredLanguage: { field: 'preferredLanguage' },
  country: { field: 'country' },
  salary: {
    field: 'salary',
    type: 'number',
  },
  age: { field: 'age' },
  canDesign: { field: 'canDesign' },
  firstName: { field: 'firstName' },
  stack: { field: 'stack' },
  id: { field: 'id' },
  hobby: { field: 'hobby' },
  city: { field: 'city' },
  currency: { field: 'currency' },
};

const rgb = {
  r: 77,
  g: 149,
  b: 215,
};
const defaultColor = `#${rgb.r.toString(16)}${rgb.g.toString(
  16,
)}${rgb.b.toString(16)}`;

export default function KeyboardNavigationForRows() {
  const [color, setColor] = useState({
    ...rgb,
  });

  const domProps = useMemo(() => {
    return {
      style: {
        '--infinite-active-cell-border-color--r': color.r,
        '--infinite-active-cell-border-color--g': color.g,
        '--infinite-active-cell-border-color--b': color.b,
        // for the same of the example being more obvious,
        // make the opacity of the unfocused table same as the one used on focus
        '--infinite-active-cell-background-alpha--table-unfocused': '0.25', // but this defaults to 0.1
      },
    } as HTMLProps<HTMLDivElement>;
  }, [color]);

  const onChange = useMemo(() => {
    const onColorChange = (event: ChangeEvent<HTMLInputElement>) => {
      const color = event.target.value;

      const r = parseInt(color.substr(1, 2), 16);
      const g = parseInt(color.substr(3, 2), 16);
      const b = parseInt(color.substr(5, 2), 16);

      setColor({
        r,
        g,
        b,
      });
    };
    return debounce(onColorChange, { wait: 200 });
  }, []);

  return (
    <>
      <div
        style={{
          color: 'var(--infinite-cell-color)',
        }}
      >
        Select color{' '}
        <input type="color" onChange={onChange} defaultValue={defaultColor} />
      </div>
      <DataSource<Developer> primaryKey="id" data={dataSource}>
        <InfiniteTable<Developer>
          debugId="navigating-rows-theming-example"
          keyboardNavigation="row"
          defaultActiveRowIndex={7}
          domProps={domProps}
          columns={columns}
        />
      </DataSource>
    </>
  );
}
```
