Skip to main content
react-md

Table

Tables display sets of data across rows and columns.

Simple Example

A table can be created using the following components:

The TableRow will default to rendering a border on the bottom and a different background color while hovered when in the TableBody.

A caption can be added by using the Typography component.

This is a caption.
Header 1Header 2
Cell 1Cell 2
Cell 1Cell 2
Footer 1Footer 2
import { Table } from "@react-md/core/table/Table";
import { TableBody } from "@react-md/core/table/TableBody";
import { TableCell } from "@react-md/core/table/TableCell";
import { TableFooter } from "@react-md/core/table/TableFooter";
import { TableHeader } from "@react-md/core/table/TableHeader";
import { TableRow } from "@react-md/core/table/TableRow";
import { Typography } from "@react-md/core/typography/Typography";
import { type ReactElement } from "react";

export default function SimpleTableExample(): ReactElement {
  return (
    <Table>
      <Typography type="caption">This is a caption.</Typography>
      <TableHeader>
        <TableRow>
          <TableCell>Header 1</TableCell>
          <TableCell>Header 2</TableCell>
        </TableRow>
      </TableHeader>
      <TableBody>
        <TableRow>
          <TableCell>Cell 1</TableCell>
          <TableCell>Cell 2</TableCell>
        </TableRow>
        <TableRow>
          <TableCell>Cell 1</TableCell>
          <TableCell>Cell 2</TableCell>
        </TableRow>
      </TableBody>
      <TableFooter>
        <TableRow>
          <TableCell>Footer 1</TableCell>
          <TableCell>Footer 2</TableCell>
        </TableRow>
      </TableFooter>
    </Table>
  );
}

Press Enter to start editing.

Disable Hover Styles

The row hover styles that appear on all the TableRow in the TableBody can be configured on the Table, TableBody, or TableRowcomponents with thedisableHoverprop. The TableRow will inherit this value or override the inherited value if defined.

Hover is disabled
Hover is disabled
Hover is enabled
import { Table } from "@react-md/core/table/Table";
import { TableBody } from "@react-md/core/table/TableBody";
import { TableCell } from "@react-md/core/table/TableCell";
import { TableRow } from "@react-md/core/table/TableRow";
import { type ReactElement } from "react";

export default function DisableHoverExample(): ReactElement {
  // try moving the `disableHover` from the `Table` to the `TableBody`
  return (
    <Table disableHover>
      <TableBody>
        <TableRow>
          <TableCell>Hover is disabled</TableCell>
        </TableRow>
        <TableRow>
          <TableCell>Hover is disabled</TableCell>
        </TableRow>
        <TableRow disableHover={false}>
          <TableCell>Hover is enabled</TableCell>
        </TableRow>
      </TableBody>
    </Table>
  );
}

Press Enter to start editing.

Disable Border Styles

The bottom border that appears on all TableRow in the TableBody can be removed by enabling the disableBorders prop on the Table, TableBody, or TableRow components. The TableRow will inherit this value or override the inherited value if defined.

Borders are disabled
Borders are disabled
Borders are enabled
Borders don't apply
import { Table } from "@react-md/core/table/Table";
import { TableBody } from "@react-md/core/table/TableBody";
import { TableCell } from "@react-md/core/table/TableCell";
import { TableRow } from "@react-md/core/table/TableRow";
import { type ReactElement } from "react";

export default function DisableBordersExample(): ReactElement {
  // try moving the `disableBorders` from the `Table` to the `TableBody`
  return (
    <Table disableBorders>
      <TableBody>
        <TableRow>
          <TableCell>Borders are disabled</TableCell>
        </TableRow>
        <TableRow>
          <TableCell>Borders are disabled</TableCell>
        </TableRow>
        <TableRow disableBorders={false}>
          <TableCell>Borders are enabled</TableCell>
        </TableRow>
        <TableRow>
          <TableCell>{"Borders don't apply"}</TableCell>
        </TableRow>
      </TableBody>
    </Table>
  );
}

Press Enter to start editing.

Scrollable Tables

If a table should be scrollable, wrap it in a TableContainer. The TableContainer allows scrolling horizontally and vertically by adding:

.rmd-table-container {
  max-width: 100%;
  overflow: auto;
}
Header 1Header 2Header 3Header 4Header 5Header 6Header 7Header 8Header 9Header 10Header 11Header 12Header 13Header 14Header 15Header 16Header 17Header 18Header 19Header 20
Cell 1Cell 2Cell 3Cell 4Cell 5Cell 6Cell 7Cell 8Cell 9Cell 10Cell 11Cell 12Cell 13Cell 14Cell 15Cell 16Cell 17Cell 18Cell 19Cell 20
Cell 1Cell 2Cell 3Cell 4Cell 5Cell 6Cell 7Cell 8Cell 9Cell 10Cell 11Cell 12Cell 13Cell 14Cell 15Cell 16Cell 17Cell 18Cell 19Cell 20
Cell 1Cell 2Cell 3Cell 4Cell 5Cell 6Cell 7Cell 8Cell 9Cell 10Cell 11Cell 12Cell 13Cell 14Cell 15Cell 16Cell 17Cell 18Cell 19Cell 20
Cell 1Cell 2Cell 3Cell 4Cell 5Cell 6Cell 7Cell 8Cell 9Cell 10Cell 11Cell 12Cell 13Cell 14Cell 15Cell 16Cell 17Cell 18Cell 19Cell 20
Cell 1Cell 2Cell 3Cell 4Cell 5Cell 6Cell 7Cell 8Cell 9Cell 10Cell 11Cell 12Cell 13Cell 14Cell 15Cell 16Cell 17Cell 18Cell 19Cell 20
Cell 1Cell 2Cell 3Cell 4Cell 5Cell 6Cell 7Cell 8Cell 9Cell 10Cell 11Cell 12Cell 13Cell 14Cell 15Cell 16Cell 17Cell 18Cell 19Cell 20
Cell 1Cell 2Cell 3Cell 4Cell 5Cell 6Cell 7Cell 8Cell 9Cell 10Cell 11Cell 12Cell 13Cell 14Cell 15Cell 16Cell 17Cell 18Cell 19Cell 20
Cell 1Cell 2Cell 3Cell 4Cell 5Cell 6Cell 7Cell 8Cell 9Cell 10Cell 11Cell 12Cell 13Cell 14Cell 15Cell 16Cell 17Cell 18Cell 19Cell 20
Cell 1Cell 2Cell 3Cell 4Cell 5Cell 6Cell 7Cell 8Cell 9Cell 10Cell 11Cell 12Cell 13Cell 14Cell 15Cell 16Cell 17Cell 18Cell 19Cell 20
Cell 1Cell 2Cell 3Cell 4Cell 5Cell 6Cell 7Cell 8Cell 9Cell 10Cell 11Cell 12Cell 13Cell 14Cell 15Cell 16Cell 17Cell 18Cell 19Cell 20
Cell 1Cell 2Cell 3Cell 4Cell 5Cell 6Cell 7Cell 8Cell 9Cell 10Cell 11Cell 12Cell 13Cell 14Cell 15Cell 16Cell 17Cell 18Cell 19Cell 20
Cell 1Cell 2Cell 3Cell 4Cell 5Cell 6Cell 7Cell 8Cell 9Cell 10Cell 11Cell 12Cell 13Cell 14Cell 15Cell 16Cell 17Cell 18Cell 19Cell 20
Cell 1Cell 2Cell 3Cell 4Cell 5Cell 6Cell 7Cell 8Cell 9Cell 10Cell 11Cell 12Cell 13Cell 14Cell 15Cell 16Cell 17Cell 18Cell 19Cell 20
Cell 1Cell 2Cell 3Cell 4Cell 5Cell 6Cell 7Cell 8Cell 9Cell 10Cell 11Cell 12Cell 13Cell 14Cell 15Cell 16Cell 17Cell 18Cell 19Cell 20
Cell 1Cell 2Cell 3Cell 4Cell 5Cell 6Cell 7Cell 8Cell 9Cell 10Cell 11Cell 12Cell 13Cell 14Cell 15Cell 16Cell 17Cell 18Cell 19Cell 20
Cell 1Cell 2Cell 3Cell 4Cell 5Cell 6Cell 7Cell 8Cell 9Cell 10Cell 11Cell 12Cell 13Cell 14Cell 15Cell 16Cell 17Cell 18Cell 19Cell 20
Cell 1Cell 2Cell 3Cell 4Cell 5Cell 6Cell 7Cell 8Cell 9Cell 10Cell 11Cell 12Cell 13Cell 14Cell 15Cell 16Cell 17Cell 18Cell 19Cell 20
Cell 1Cell 2Cell 3Cell 4Cell 5Cell 6Cell 7Cell 8Cell 9Cell 10Cell 11Cell 12Cell 13Cell 14Cell 15Cell 16Cell 17Cell 18Cell 19Cell 20
Cell 1Cell 2Cell 3Cell 4Cell 5Cell 6Cell 7Cell 8Cell 9Cell 10Cell 11Cell 12Cell 13Cell 14Cell 15Cell 16Cell 17Cell 18Cell 19Cell 20
Cell 1Cell 2Cell 3Cell 4Cell 5Cell 6Cell 7Cell 8Cell 9Cell 10Cell 11Cell 12Cell 13Cell 14Cell 15Cell 16Cell 17Cell 18Cell 19Cell 20
import { Table } from "@react-md/core/table/Table";
import { TableBody } from "@react-md/core/table/TableBody";
import { TableCell } from "@react-md/core/table/TableCell";
import { TableContainer } from "@react-md/core/table/TableContainer";
import { TableHeader } from "@react-md/core/table/TableHeader";
import { TableRow } from "@react-md/core/table/TableRow";
import { type ReactElement } from "react";

import styles from "./ScrollableTableExample.module.scss";

const rows = 20;
const columns = 20;

export default function ScrollableTableExample(): ReactElement {
  return (
    <TableContainer className={styles.container}>
      <Table>
        <TableHeader>
          <TableRow>
            {Array.from({ length: columns }, (_, column) => (
              <TableCell key={column}>{`Header ${column + 1}`}</TableCell>
            ))}
          </TableRow>
        </TableHeader>
        <TableBody>
          {Array.from({ length: rows }, (_, i) => (
            <TableRow key={i}>
              {Array.from({ length: columns }, (_, column) => (
                <TableCell key={column}>{`Cell ${column + 1}`}</TableCell>
              ))}
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </TableContainer>
  );
}

Press Enter to start editing.

.container {
  max-height: 20rem;
}

Press Enter to start editing.

Selectable Rows

A common pattern with tables are to make the rows selectable so different actions can be applied. A TableRow can be updated to have a selected state which will add a different background color and a clickable state that updates the cursor to be a pointer.

Header 1Header 2Header 3
Cell 1Cell 2Cell 3
Cell 1Cell 2Cell 3
Cell 1Cell 2Cell 3
"use client";

import { Table } from "@react-md/core/table/Table";
import { TableBody } from "@react-md/core/table/TableBody";
import { TableCell } from "@react-md/core/table/TableCell";
import { TableContainer } from "@react-md/core/table/TableContainer";
import { TableHeader } from "@react-md/core/table/TableHeader";
import { TableRow } from "@react-md/core/table/TableRow";
import { type ReactElement, useState } from "react";

export default function SelectableRowsExample(): ReactElement {
  const [selectedRows, setSelectedRows] = useState<readonly number[]>([]);
  const toggleRow = (index: number): void => {
    setSelectedRows((prevSelectedRows) => {
      if (prevSelectedRows.includes(index)) {
        return prevSelectedRows.filter((i) => i !== index);
      }

      return [...prevSelectedRows, index];
    });
  };

  return (
    <TableContainer>
      <Table fullWidth>
        <TableHeader>
          <TableRow>
            <TableCell>Header 1</TableCell>
            <TableCell>Header 2</TableCell>
            <TableCell>Header 3</TableCell>
          </TableRow>
        </TableHeader>
        <TableBody>
          <TableRow
            clickable
            onClick={() => {
              toggleRow(0);
            }}
            selected={selectedRows.includes(0)}
          >
            <TableCell>Cell 1</TableCell>
            <TableCell>Cell 2</TableCell>
            <TableCell>Cell 3</TableCell>
          </TableRow>
          <TableRow
            clickable
            onClick={() => {
              toggleRow(1);
            }}
            selected={selectedRows.includes(1)}
          >
            <TableCell>Cell 1</TableCell>
            <TableCell>Cell 2</TableCell>
            <TableCell>Cell 3</TableCell>
          </TableRow>
          <TableRow
            clickable
            onClick={() => {
              toggleRow(2);
            }}
            selected={selectedRows.includes(2)}
          >
            <TableCell>Cell 1</TableCell>
            <TableCell>Cell 2</TableCell>
            <TableCell>Cell 3</TableCell>
          </TableRow>
        </TableBody>
      </Table>
    </TableContainer>
  );
}

Press Enter to start editing.

Selectable Rows with Checkbox

It is recommended to use the TableCheckbox component as the first cell in each row to help show the row can be selected and the useCheckboxGroup hook to control the selected state.

Dessert (100g serving)TypeCaloriesFat (g)Carbs (g)Protein (g)Sodium (mg)Calcium (mg)Icon (mg)
Frozen yogurtIce cream159624487141
Ice cream sandwichIce cream2379374.312981
EclairPastry2621637633767
CupcakePastry3053.7674.341338
GingerbreadPastry35616493.9327716
Jelly beanOther37509405000
LollipopOther3920.29803802
HoneycombOther4083.2876.5562045
DonutPastry5225514.9326222
KitKatOther16665754126
"use client";

import { Form } from "@react-md/core/form/Form";
import { useCheckboxGroup } from "@react-md/core/form/useCheckboxGroup";
import { Table } from "@react-md/core/table/Table";
import { TableBody } from "@react-md/core/table/TableBody";
import { TableCell } from "@react-md/core/table/TableCell";
import { TableCheckbox } from "@react-md/core/table/TableCheckbox";
import { TableContainer } from "@react-md/core/table/TableContainer";
import { TableHeader } from "@react-md/core/table/TableHeader";
import { TableRow } from "@react-md/core/table/TableRow";
import { type ReactElement } from "react";

export default function SelectableRowsWithCheckboxExample(): ReactElement {
  const { getCheckboxProps, getIndeterminateProps } = useCheckboxGroup({
    name: "desserts",
    values: desserts.map(({ name }) => name),
  });

  return (
    <TableContainer>
      <Form>
        <Table fullWidth>
          <TableHeader>
            <TableRow>
              <TableCheckbox {...getIndeterminateProps()} />
              {columns.map((name, i) => (
                <TableCell key={name} grow={i === 0}>
                  {name}
                </TableCell>
              ))}
            </TableRow>
          </TableHeader>
          <TableBody hAlign="right">
            {desserts.map((dessert) => {
              const {
                name,
                type,
                calories,
                fat,
                carbs,
                protein,
                sodium,
                calcium,
                iron,
              } = dessert;
              const checkboxProps = getCheckboxProps(name);
              const { checked, onChange } = checkboxProps;

              return (
                <TableRow
                  key={name}
                  selected={checked}
                  onClick={onChange}
                  clickable
                >
                  <TableCheckbox {...checkboxProps} />
                  <TableCell hAlign="left">{name}</TableCell>
                  <TableCell>{type}</TableCell>
                  <TableCell>{calories}</TableCell>
                  <TableCell>{fat}</TableCell>
                  <TableCell>{carbs}</TableCell>
                  <TableCell>{protein}</TableCell>
                  <TableCell>{sodium}</TableCell>
                  <TableCell>{calcium}</TableCell>
                  <TableCell>{iron}</TableCell>
                </TableRow>
              );
            })}
          </TableBody>
        </Table>
      </Form>
    </TableContainer>
  );
}

const columns = [
  "Dessert (100g serving)",
  "Type",
  "Calories",
  "Fat (g)",
  "Carbs (g)",
  "Protein (g)",
  "Sodium (mg)",
  "Calcium (mg)",
  "Icon (mg)",
] as const;

export interface Dessert {
  name: string;
  calories: number;
  fat: number;
  carbs: number;
  protein: number;
  sodium: number;
  calcium: number;
  iron: number;
  type: "Ice cream" | "Pastry" | "Other";
}

export type DessertKey = keyof Dessert;

export const desserts: readonly Dessert[] = [
  {
    name: "Frozen yogurt",
    type: "Ice cream",
    calories: 159,
    fat: 6.0,
    carbs: 24,
    protein: 4.0,
    sodium: 87,
    calcium: 14,
    iron: 1,
  },
  {
    name: "Ice cream sandwich",
    type: "Ice cream",
    calories: 237,
    fat: 9.0,
    carbs: 37,
    protein: 4.3,
    sodium: 129,
    calcium: 8,
    iron: 1,
  },
  {
    name: "Eclair",
    type: "Pastry",
    calories: 262,
    fat: 16.0,
    carbs: 37,
    protein: 6.0,
    sodium: 337,
    calcium: 6,
    iron: 7,
  },
  {
    name: "Cupcake",
    type: "Pastry",
    calories: 305,
    fat: 3.7,
    carbs: 67,
    protein: 4.3,
    sodium: 413,
    calcium: 3,
    iron: 8,
  },
  {
    name: "Gingerbread",
    type: "Pastry",
    calories: 356,
    fat: 16.0,
    carbs: 49,
    protein: 3.9,
    sodium: 327,
    calcium: 7,
    iron: 16,
  },
  {
    name: "Jelly bean",
    type: "Other",
    calories: 375,
    fat: 0.0,
    carbs: 94,
    protein: 0.0,
    sodium: 50,
    calcium: 0,
    iron: 0,
  },
  {
    name: "Lollipop",
    type: "Other",
    calories: 392,
    fat: 0.2,
    carbs: 98,
    protein: 0.0,
    sodium: 38,
    calcium: 0,
    iron: 2,
  },
  {
    name: "Honeycomb",
    type: "Other",
    calories: 408,
    fat: 3.2,
    carbs: 87,
    protein: 6.5,
    sodium: 562,
    calcium: 0,
    iron: 45,
  },
  {
    name: "Donut",
    type: "Pastry",
    calories: 52,
    fat: 25.0,
    carbs: 51,
    protein: 4.9,
    sodium: 326,
    calcium: 2,
    iron: 22,
  },
  {
    name: "KitKat",
    type: "Other",
    calories: 16,
    fat: 6.0,
    carbs: 65,
    protein: 7.0,
    sodium: 54,
    calcium: 12,
    iron: 6,
  },
];

export const dessertColumns = Object.keys(desserts[0]) as readonly DessertKey[];

Press Enter to start editing.

Selectable Rows with Radio

If only one row can be selected at a time, the TableRadio component can be used instead.

Dessert (100g serving)TypeCaloriesFat (g)Carbs (g)Protein (g)Sodium (mg)Calcium (mg)Icon (mg)
Frozen yogurtIce cream159624487141
Ice cream sandwichIce cream2379374.312981
EclairPastry2621637633767
CupcakePastry3053.7674.341338
GingerbreadPastry35616493.9327716
Jelly beanOther37509405000
LollipopOther3920.29803802
HoneycombOther4083.2876.5562045
DonutPastry5225514.9326222
KitKatOther16665754126
"use client";

import { Form } from "@react-md/core/form/Form";
import { Table } from "@react-md/core/table/Table";
import { TableBody } from "@react-md/core/table/TableBody";
import { TableCell } from "@react-md/core/table/TableCell";
import { TableContainer } from "@react-md/core/table/TableContainer";
import { TableHeader } from "@react-md/core/table/TableHeader";
import { TableRadio } from "@react-md/core/table/TableRadio";
import { TableRow } from "@react-md/core/table/TableRow";
import { type ReactElement, useState } from "react";

export default function TableRadioExample(): ReactElement {
  const [value, setValue] = useState<string | null>(null);

  return (
    <TableContainer>
      <Form>
        <Table fullWidth>
          <TableHeader>
            <TableRow>
              <TableCell header={false} />
              {columns.map((name, i) => (
                <TableCell key={name} grow={i === 0}>
                  {name}
                </TableCell>
              ))}
            </TableRow>
          </TableHeader>
          <TableBody hAlign="right">
            {desserts.map((dessert) => {
              const {
                name,
                type,
                calories,
                fat,
                carbs,
                protein,
                sodium,
                calcium,
                iron,
              } = dessert;
              const selected = value === name;
              const onChange = (): void => {
                setValue((prevValue) => {
                  if (name === prevValue) {
                    return null;
                  }

                  return name;
                });
              };

              return (
                <TableRow
                  key={name}
                  onClick={onChange}
                  selected={selected}
                  clickable
                >
                  <TableRadio
                    name="selections"
                    checked={selected}
                    onChange={onChange}
                  />
                  <TableCell hAlign="left">{name}</TableCell>
                  <TableCell>{type}</TableCell>
                  <TableCell>{calories}</TableCell>
                  <TableCell>{fat}</TableCell>
                  <TableCell>{carbs}</TableCell>
                  <TableCell>{protein}</TableCell>
                  <TableCell>{sodium}</TableCell>
                  <TableCell>{calcium}</TableCell>
                  <TableCell>{iron}</TableCell>
                </TableRow>
              );
            })}
          </TableBody>
        </Table>
      </Form>
    </TableContainer>
  );
}

const columns = [
  "Dessert (100g serving)",
  "Type",
  "Calories",
  "Fat (g)",
  "Carbs (g)",
  "Protein (g)",
  "Sodium (mg)",
  "Calcium (mg)",
  "Icon (mg)",
] as const;

export interface Dessert {
  name: string;
  calories: number;
  fat: number;
  carbs: number;
  protein: number;
  sodium: number;
  calcium: number;
  iron: number;
  type: "Ice cream" | "Pastry" | "Other";
}

export type DessertKey = keyof Dessert;

export const desserts: readonly Dessert[] = [
  {
    name: "Frozen yogurt",
    type: "Ice cream",
    calories: 159,
    fat: 6.0,
    carbs: 24,
    protein: 4.0,
    sodium: 87,
    calcium: 14,
    iron: 1,
  },
  {
    name: "Ice cream sandwich",
    type: "Ice cream",
    calories: 237,
    fat: 9.0,
    carbs: 37,
    protein: 4.3,
    sodium: 129,
    calcium: 8,
    iron: 1,
  },
  {
    name: "Eclair",
    type: "Pastry",
    calories: 262,
    fat: 16.0,
    carbs: 37,
    protein: 6.0,
    sodium: 337,
    calcium: 6,
    iron: 7,
  },
  {
    name: "Cupcake",
    type: "Pastry",
    calories: 305,
    fat: 3.7,
    carbs: 67,
    protein: 4.3,
    sodium: 413,
    calcium: 3,
    iron: 8,
  },
  {
    name: "Gingerbread",
    type: "Pastry",
    calories: 356,
    fat: 16.0,
    carbs: 49,
    protein: 3.9,
    sodium: 327,
    calcium: 7,
    iron: 16,
  },
  {
    name: "Jelly bean",
    type: "Other",
    calories: 375,
    fat: 0.0,
    carbs: 94,
    protein: 0.0,
    sodium: 50,
    calcium: 0,
    iron: 0,
  },
  {
    name: "Lollipop",
    type: "Other",
    calories: 392,
    fat: 0.2,
    carbs: 98,
    protein: 0.0,
    sodium: 38,
    calcium: 0,
    iron: 2,
  },
  {
    name: "Honeycomb",
    type: "Other",
    calories: 408,
    fat: 3.2,
    carbs: 87,
    protein: 6.5,
    sodium: 562,
    calcium: 0,
    iron: 45,
  },
  {
    name: "Donut",
    type: "Pastry",
    calories: 52,
    fat: 25.0,
    carbs: 51,
    protein: 4.9,
    sodium: 326,
    calcium: 2,
    iron: 22,
  },
  {
    name: "KitKat",
    type: "Other",
    calories: 16,
    fat: 6.0,
    carbs: 65,
    protein: 7.0,
    sodium: 54,
    calcium: 12,
    iron: 6,
  },
];

export const dessertColumns = Object.keys(desserts[0]) as readonly DessertKey[];

Press Enter to start editing.

Updating Selected Row Color

The selected background color defaults to core.interaction-get-var(selected-background-color). This can be configured by the core.$table-row-selected-color Sass variable or the --rmd-table-selected-background-color custom property.

If the text color and hover colors also need to change while selected, it is recommended to just add a custom className while selected instead. Here's a quick example of using the primary color while selected.

Cell 1Cell 2
Cell 1Cell 2
Cell 1Cell 2
import { cssUtils } from "@react-md/core/cssUtils";
import { Table } from "@react-md/core/table/Table";
import { TableBody } from "@react-md/core/table/TableBody";
import { TableCell } from "@react-md/core/table/TableCell";
import { TableRow } from "@react-md/core/table/TableRow";
import { type UseStateSetter } from "@react-md/core/types";
import { cnb } from "cnbuilder";
import { type ReactElement, useState } from "react";

import styles from "./UpdatingSelectedRowColorExample.module.scss";

export default function UpdatingSelectedRowColorExample(): ReactElement {
  const [selectedRow, setSelectedRow] = useState(0);
  return (
    <Table className={styles.table}>
      <TableBody>
        <CustomSelectedRow
          rowIndex={0}
          selectedRow={selectedRow}
          setSelectedRow={setSelectedRow}
        />
        <CustomSelectedRow
          rowIndex={1}
          selectedRow={selectedRow}
          setSelectedRow={setSelectedRow}
        />
        <CustomSelectedRow
          rowIndex={2}
          selectedRow={selectedRow}
          setSelectedRow={setSelectedRow}
        />
      </TableBody>
    </Table>
  );
}

interface CustomSelectedRowProps {
  rowIndex: number;
  selectedRow: number;
  setSelectedRow: UseStateSetter<number>;
}

function CustomSelectedRow(props: CustomSelectedRowProps): ReactElement {
  const { rowIndex, selectedRow, setSelectedRow } = props;

  const selected = rowIndex === selectedRow;
  const onChange = (): void => {
    setSelectedRow(rowIndex);
  };
  return (
    <TableRow
      onClick={onChange}
      selected={selected}
      className={cssUtils({
        className: cnb(selected && styles.selected),
        backgroundColor: selected ? "primary" : undefined,
      })}
      clickable
    >
      <TableCell>Cell 1</TableCell>
      <TableCell>Cell 2</TableCell>
    </TableRow>
  );
}

Press Enter to start editing.

@use "everything";

.selected {
  @include everything.table-set-var(cell-color, currentcolor);

  @include everything.mouse-hover {
    background-color: color-mix(
      in srgb,
      everything.theme-get-var(primary-color) 80%,
      everything.theme-get-var(on-primary-color)
    );
  }
}

Press Enter to start editing.

Sortable Columns

To create a sortable header cell, provide the aria-sort prop to a TableCell as one of the following values:

When the aria-sort prop has been set to one of these values, the cell will automatically update the children to be rendered within a button element so that it can be tab-focused and clickable for keyboard users. However when the sort behavior has been set to none, only the button element will be rendered without the current sort icon to show that it is not currently sorted, but can be.

CupcakePastry3053.7674.341338
DonutPastry5225514.9326222
EclairPastry2621637633767
Frozen yogurtIce cream159624487141
GingerbreadPastry35616493.9327716
HoneycombOther4083.2876.5562045
Ice cream sandwichIce cream2379374.312981
Jelly beanOther37509405000
KitKatOther16665754126
LollipopOther3920.29803802
"use client";

import { cssUtils } from "@react-md/core/cssUtils";
import { Table } from "@react-md/core/table/Table";
import { TableBody } from "@react-md/core/table/TableBody";
import { TableCell } from "@react-md/core/table/TableCell";
import { TableContainer } from "@react-md/core/table/TableContainer";
import { TableHeader } from "@react-md/core/table/TableHeader";
import { TableRow } from "@react-md/core/table/TableRow";
import { type SortOrder } from "@react-md/core/table/types";
import { type ReactElement, useState } from "react";

export default function SortableColumnsExample(): ReactElement {
  const { data, sortKey, sortOrder, update } = useSortedColumns();
  return (
    <TableContainer>
      <Table fullWidth>
        <TableHeader>
          <TableRow>
            {dessertColumns.map((name, i) => (
              <TableCell
                key={name}
                aria-sort={name === sortKey ? sortOrder : "none"}
                onClick={() => {
                  update(name);
                }}
                grow={i === 0}
                contentProps={{
                  className: cssUtils({ textTransform: "capitalize" }),
                }}
              >
                {name}
              </TableCell>
            ))}
          </TableRow>
        </TableHeader>
        <TableBody>
          {data.map((dessert) => (
            <TableRow key={dessert.name}>
              {dessertColumns.map((key) => {
                const value = dessert[key];

                return (
                  <TableCell
                    key={key}
                    grow={key === "name"}
                    hAlign={value === "number" ? "right" : undefined}
                  >
                    {value}
                  </TableCell>
                );
              })}
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </TableContainer>
  );
}

/**
 * A custom sort function for the list of desserts.
 */
const sort = (key: DessertKey, ascending: boolean): readonly Dessert[] => {
  const sorted = desserts.slice();
  sorted.sort((a, b) => {
    const aValue = a[key];
    const bValue = b[key];

    const value =
      typeof aValue === "number"
        ? aValue - (bValue as number)
        : aValue.localeCompare(bValue as string);

    return value * (ascending ? 1 : -1);
  });

  return sorted;
};

interface SortState {
  data: readonly Dessert[];
  sortKey: DessertKey;
  sortOrder: SortOrder;
}

interface SortedColumnsHookResult extends SortState {
  update: (sortKey: DessertKey) => void;
}

function useSortedColumns(): SortedColumnsHookResult {
  const [state, setState] = useState<SortState>(() => ({
    data: sort("name", true),
    sortKey: "name",
    sortOrder: "ascending",
  }));

  const update = (sortKey: DessertKey): void => {
    setState((prevState) => {
      const prevSortKey = prevState.sortKey;
      const prevSortOrder = prevState.sortOrder;

      let sortOrder: SortOrder;
      if (sortKey === prevSortKey) {
        // it's the same column, so toggle the sort order
        sortOrder = prevSortOrder === "ascending" ? "descending" : "ascending";
      } else {
        // it's a new column to sort by, so default to ascending for the name column
        // but descending for all the rest.
        sortOrder = sortKey === "name" ? "ascending" : "descending";
      }

      return {
        data: sort(sortKey, sortOrder === "ascending"),
        sortKey,
        sortOrder,
      };
    });
  };

  return {
    ...state,
    update,
  };
}

export interface Dessert {
  name: string;
  calories: number;
  fat: number;
  carbs: number;
  protein: number;
  sodium: number;
  calcium: number;
  iron: number;
  type: "Ice cream" | "Pastry" | "Other";
}

export type DessertKey = keyof Dessert;

export const desserts: readonly Dessert[] = [
  {
    name: "Frozen yogurt",
    type: "Ice cream",
    calories: 159,
    fat: 6.0,
    carbs: 24,
    protein: 4.0,
    sodium: 87,
    calcium: 14,
    iron: 1,
  },
  {
    name: "Ice cream sandwich",
    type: "Ice cream",
    calories: 237,
    fat: 9.0,
    carbs: 37,
    protein: 4.3,
    sodium: 129,
    calcium: 8,
    iron: 1,
  },
  {
    name: "Eclair",
    type: "Pastry",
    calories: 262,
    fat: 16.0,
    carbs: 37,
    protein: 6.0,
    sodium: 337,
    calcium: 6,
    iron: 7,
  },
  {
    name: "Cupcake",
    type: "Pastry",
    calories: 305,
    fat: 3.7,
    carbs: 67,
    protein: 4.3,
    sodium: 413,
    calcium: 3,
    iron: 8,
  },
  {
    name: "Gingerbread",
    type: "Pastry",
    calories: 356,
    fat: 16.0,
    carbs: 49,
    protein: 3.9,
    sodium: 327,
    calcium: 7,
    iron: 16,
  },
  {
    name: "Jelly bean",
    type: "Other",
    calories: 375,
    fat: 0.0,
    carbs: 94,
    protein: 0.0,
    sodium: 50,
    calcium: 0,
    iron: 0,
  },
  {
    name: "Lollipop",
    type: "Other",
    calories: 392,
    fat: 0.2,
    carbs: 98,
    protein: 0.0,
    sodium: 38,
    calcium: 0,
    iron: 2,
  },
  {
    name: "Honeycomb",
    type: "Other",
    calories: 408,
    fat: 3.2,
    carbs: 87,
    protein: 6.5,
    sodium: 562,
    calcium: 0,
    iron: 45,
  },
  {
    name: "Donut",
    type: "Pastry",
    calories: 52,
    fat: 25.0,
    carbs: 51,
    protein: 4.9,
    sodium: 326,
    calcium: 2,
    iron: 22,
  },
  {
    name: "KitKat",
    type: "Other",
    calories: 16,
    fat: 6.0,
    carbs: 65,
    protein: 7.0,
    sodium: 54,
    calcium: 12,
    iron: 6,
  },
];

export const dessertColumns = Object.keys(desserts[0]) as readonly DessertKey[];

Press Enter to start editing.

Customizing Sort Icon

The sortable TableCell component will use the sort icon from the ICON_CONFIG by default but can also be configured by providing the sortIcon prop.

The sort icon can also be placed after the children by enabling the sortIconAfter prop and disable the rotation transition by enabling disableTransition to the iconRotatorProps.

import { Table } from "@react-md/core/table/Table";
import { TableCell } from "@react-md/core/table/TableCell";
import { TableContainer } from "@react-md/core/table/TableContainer";
import { TableHeader } from "@react-md/core/table/TableHeader";
import { TableRow } from "@react-md/core/table/TableRow";
import { type SortOrder } from "@react-md/core/table/types";
import { useToggle } from "@react-md/core/useToggle";
import ChevronLeftIcon from "@react-md/material-icons/ChevronLeftIcon";
import { type ReactElement } from "react";

export default function CustomizingSortIconExample(): ReactElement {
  const { toggled, toggle } = useToggle();
  const sort: SortOrder = toggled ? "ascending" : "descending";
  return (
    <TableContainer>
      <Table>
        <TableHeader>
          <TableRow>
            <TableCell aria-sort={sort} sortIconAfter onClick={toggle}>
              Sort Icon After
            </TableCell>
            <TableCell
              aria-sort={sort}
              sortIcon={<ChevronLeftIcon />}
              onClick={toggle}
            >
              Custom Sort Icon
            </TableCell>
            <TableCell
              aria-sort={sort}
              onClick={toggle}
              iconRotatorProps={{
                disableTransition: true,
              }}
            >
              Disable Rotate Transition
            </TableCell>
          </TableRow>
        </TableHeader>
      </Table>
    </TableContainer>
  );
}

Press Enter to start editing.

Sticky Tables

Tables within react-md can be updated to have sticky headers, footers, and columns by using sticky positioning. When an element has position: sticky set, it will be fixed within the closest scroll container based on the top, right, bottom, and left properties. If there are no parent elements that have overflow: auto or overflow: scroll, the sticky elements can be positioned relative to the entire document.

Container Based Sticky Table

To create a sticky TableHeader or TableFooter, use the StickyTableSection component with type="header" or type="footer" which will add top: 0 and bottom: 0 as the sticky positioning. This will make it so the header and footer are stuck to the top and bottom of the TableContainer component.

Header 1Header 2
Row 1 Cell 1Row 1 Cell 2
Row 2 Cell 1Row 2 Cell 2
Row 3 Cell 1Row 3 Cell 2
Row 4 Cell 1Row 4 Cell 2
Row 5 Cell 1Row 5 Cell 2
Row 6 Cell 1Row 6 Cell 2
Row 7 Cell 1Row 7 Cell 2
Row 8 Cell 1Row 8 Cell 2
Row 9 Cell 1Row 9 Cell 2
Row 10 Cell 1Row 10 Cell 2
Row 11 Cell 1Row 11 Cell 2
Row 12 Cell 1Row 12 Cell 2
Row 13 Cell 1Row 13 Cell 2
Row 14 Cell 1Row 14 Cell 2
Row 15 Cell 1Row 15 Cell 2
Row 16 Cell 1Row 16 Cell 2
Row 17 Cell 1Row 17 Cell 2
Row 18 Cell 1Row 18 Cell 2
Row 19 Cell 1Row 19 Cell 2
Row 20 Cell 1Row 20 Cell 2
Content
import { StickyTableSection } from "@react-md/core/table/StickyTableSection";
import { Table } from "@react-md/core/table/Table";
import { TableBody } from "@react-md/core/table/TableBody";
import { TableCell } from "@react-md/core/table/TableCell";
import { TableContainer } from "@react-md/core/table/TableContainer";
import { TableRow } from "@react-md/core/table/TableRow";
import { type ReactElement } from "react";

import styles from "./ContainerBasedStickyTableExample.module.scss";

export default function ContainerBasedStickyTableExample(): ReactElement {
  return (
    <TableContainer className={styles.container}>
      <Table fullWidth>
        <StickyTableSection type="header">
          <TableRow>
            <TableCell>Header 1</TableCell>
            <TableCell>Header 2</TableCell>
          </TableRow>
        </StickyTableSection>
        <TableBody>
          {Array.from({ length: 20 }, (_, i) => (
            <TableRow key={i}>
              <TableCell>{`Row ${i + 1} Cell 1`}</TableCell>
              <TableCell>{`Row ${i + 1} Cell 2`}</TableCell>
            </TableRow>
          ))}
        </TableBody>
        <StickyTableSection type="footer">
          <TableRow>
            <TableCell colSpan={2}>Content</TableCell>
          </TableRow>
        </StickyTableSection>
      </Table>
    </TableContainer>
  );
}

Press Enter to start editing.

.container {
  max-height: 20rem;
  width: 100%;
}

Press Enter to start editing.

Viewport Based Sticky Table

To create a sticky TableHeader and TableFooter relative to the viewport, do not wrap the Table with the TableContainer and ensure no parent elements have overflow: auto set. If there is a fixed header in the app, the header will also need to update it's position so it is stuck below that header. This can be done by updating the --rmd-table-sticky-header custom property or setting the top style.

Header 1Header 2
Row 1 Cell 1Row 1 Cell 2
Row 2 Cell 1Row 2 Cell 2
Row 3 Cell 1Row 3 Cell 2
Row 4 Cell 1Row 4 Cell 2
Row 5 Cell 1Row 5 Cell 2
Row 6 Cell 1Row 6 Cell 2
Row 7 Cell 1Row 7 Cell 2
Row 8 Cell 1Row 8 Cell 2
Row 9 Cell 1Row 9 Cell 2
Row 10 Cell 1Row 10 Cell 2
Row 11 Cell 1Row 11 Cell 2
Row 12 Cell 1Row 12 Cell 2
Row 13 Cell 1Row 13 Cell 2
Row 14 Cell 1Row 14 Cell 2
Row 15 Cell 1Row 15 Cell 2
Row 16 Cell 1Row 16 Cell 2
Row 17 Cell 1Row 17 Cell 2
Row 18 Cell 1Row 18 Cell 2
Row 19 Cell 1Row 19 Cell 2
Row 20 Cell 1Row 20 Cell 2
Row 21 Cell 1Row 21 Cell 2
Row 22 Cell 1Row 22 Cell 2
Row 23 Cell 1Row 23 Cell 2
Row 24 Cell 1Row 24 Cell 2
Row 25 Cell 1Row 25 Cell 2
Row 26 Cell 1Row 26 Cell 2
Row 27 Cell 1Row 27 Cell 2
Row 28 Cell 1Row 28 Cell 2
Row 29 Cell 1Row 29 Cell 2
Row 30 Cell 1Row 30 Cell 2
Row 31 Cell 1Row 31 Cell 2
Row 32 Cell 1Row 32 Cell 2
Row 33 Cell 1Row 33 Cell 2
Row 34 Cell 1Row 34 Cell 2
Row 35 Cell 1Row 35 Cell 2
Row 36 Cell 1Row 36 Cell 2
Row 37 Cell 1Row 37 Cell 2
Row 38 Cell 1Row 38 Cell 2
Row 39 Cell 1Row 39 Cell 2
Row 40 Cell 1Row 40 Cell 2
Footer
import { StickyTableSection } from "@react-md/core/table/StickyTableSection";
import { Table } from "@react-md/core/table/Table";
import { TableBody } from "@react-md/core/table/TableBody";
import { TableCell } from "@react-md/core/table/TableCell";
import { TableRow } from "@react-md/core/table/TableRow";
import { type ReactElement } from "react";

import styles from "./ViewportBasedStickyTableExample.module.scss";

export default function ViewportBasedStickyTableExample(): ReactElement {
  return (
    <Table fullWidth className={styles.container}>
      <StickyTableSection type="header">
        <TableRow>
          <TableCell>Header 1</TableCell>
          <TableCell>Header 2</TableCell>
        </TableRow>
      </StickyTableSection>
      <TableBody>
        {Array.from({ length: 40 }, (_, i) => (
          <TableRow key={i}>
            <TableCell>{`Row ${i + 1} Cell 1`}</TableCell>
            <TableCell>{`Row ${i + 1} Cell 2`}</TableCell>
          </TableRow>
        ))}
      </TableBody>
      <StickyTableSection type="footer">
        <TableRow>
          <TableCell colSpan={40}>Footer</TableCell>
        </TableRow>
      </StickyTableSection>
    </Table>
  );
}

Press Enter to start editing.

@use "everything";

.container {
  @include everything.table-set-var(
    sticky-header,
    everything.layout-get-var(header-height)
  );
}

Press Enter to start editing.

Sticky Columns Example

A column of TableCell can also become sticky horizontally by enabling the sticky prop on each TableCell in that column. It defaults to using left: 0 (right: 0 when RTL) but can be configured by the --rmd-table-sticky-cell custom property.

This example will showcase a sticky checkbox cell followed by a sticky row header cell with sticky headers and footers.

Header 1Header 2Header 3Header 4Header 5Header 6Header 7Header 8Header 9Header 10Header 11Header 12Header 13Header 14Header 15Header 16Header 17Header 18Header 19Header 20
Row HeaderCell 1 - 1Cell 1 - 2Cell 1 - 3Cell 1 - 4Cell 1 - 5Cell 1 - 6Cell 1 - 7Cell 1 - 8Cell 1 - 9Cell 1 - 10Cell 1 - 11Cell 1 - 12Cell 1 - 13Cell 1 - 14Cell 1 - 15Cell 1 - 16Cell 1 - 17Cell 1 - 18Cell 1 - 19Cell 1 - 20
Row HeaderCell 2 - 1Cell 2 - 2Cell 2 - 3Cell 2 - 4Cell 2 - 5Cell 2 - 6Cell 2 - 7Cell 2 - 8Cell 2 - 9Cell 2 - 10Cell 2 - 11Cell 2 - 12Cell 2 - 13Cell 2 - 14Cell 2 - 15Cell 2 - 16Cell 2 - 17Cell 2 - 18Cell 2 - 19Cell 2 - 20
Row HeaderCell 3 - 1Cell 3 - 2Cell 3 - 3Cell 3 - 4Cell 3 - 5Cell 3 - 6Cell 3 - 7Cell 3 - 8Cell 3 - 9Cell 3 - 10Cell 3 - 11Cell 3 - 12Cell 3 - 13Cell 3 - 14Cell 3 - 15Cell 3 - 16Cell 3 - 17Cell 3 - 18Cell 3 - 19Cell 3 - 20
Row HeaderCell 4 - 1Cell 4 - 2Cell 4 - 3Cell 4 - 4Cell 4 - 5Cell 4 - 6Cell 4 - 7Cell 4 - 8Cell 4 - 9Cell 4 - 10Cell 4 - 11Cell 4 - 12Cell 4 - 13Cell 4 - 14Cell 4 - 15Cell 4 - 16Cell 4 - 17Cell 4 - 18Cell 4 - 19Cell 4 - 20
Row HeaderCell 5 - 1Cell 5 - 2Cell 5 - 3Cell 5 - 4Cell 5 - 5Cell 5 - 6Cell 5 - 7Cell 5 - 8Cell 5 - 9Cell 5 - 10Cell 5 - 11Cell 5 - 12Cell 5 - 13Cell 5 - 14Cell 5 - 15Cell 5 - 16Cell 5 - 17Cell 5 - 18Cell 5 - 19Cell 5 - 20
Row HeaderCell 6 - 1Cell 6 - 2Cell 6 - 3Cell 6 - 4Cell 6 - 5Cell 6 - 6Cell 6 - 7Cell 6 - 8Cell 6 - 9Cell 6 - 10Cell 6 - 11Cell 6 - 12Cell 6 - 13Cell 6 - 14Cell 6 - 15Cell 6 - 16Cell 6 - 17Cell 6 - 18Cell 6 - 19Cell 6 - 20
Row HeaderCell 7 - 1Cell 7 - 2Cell 7 - 3Cell 7 - 4Cell 7 - 5Cell 7 - 6Cell 7 - 7Cell 7 - 8Cell 7 - 9Cell 7 - 10Cell 7 - 11Cell 7 - 12Cell 7 - 13Cell 7 - 14Cell 7 - 15Cell 7 - 16Cell 7 - 17Cell 7 - 18Cell 7 - 19Cell 7 - 20
Row HeaderCell 8 - 1Cell 8 - 2Cell 8 - 3Cell 8 - 4Cell 8 - 5Cell 8 - 6Cell 8 - 7Cell 8 - 8Cell 8 - 9Cell 8 - 10Cell 8 - 11Cell 8 - 12Cell 8 - 13Cell 8 - 14Cell 8 - 15Cell 8 - 16Cell 8 - 17Cell 8 - 18Cell 8 - 19Cell 8 - 20
Row HeaderCell 9 - 1Cell 9 - 2Cell 9 - 3Cell 9 - 4Cell 9 - 5Cell 9 - 6Cell 9 - 7Cell 9 - 8Cell 9 - 9Cell 9 - 10Cell 9 - 11Cell 9 - 12Cell 9 - 13Cell 9 - 14Cell 9 - 15Cell 9 - 16Cell 9 - 17Cell 9 - 18Cell 9 - 19Cell 9 - 20
Row HeaderCell 10 - 1Cell 10 - 2Cell 10 - 3Cell 10 - 4Cell 10 - 5Cell 10 - 6Cell 10 - 7Cell 10 - 8Cell 10 - 9Cell 10 - 10Cell 10 - 11Cell 10 - 12Cell 10 - 13Cell 10 - 14Cell 10 - 15Cell 10 - 16Cell 10 - 17Cell 10 - 18Cell 10 - 19Cell 10 - 20
Row HeaderCell 11 - 1Cell 11 - 2Cell 11 - 3Cell 11 - 4Cell 11 - 5Cell 11 - 6Cell 11 - 7Cell 11 - 8Cell 11 - 9Cell 11 - 10Cell 11 - 11Cell 11 - 12Cell 11 - 13Cell 11 - 14Cell 11 - 15Cell 11 - 16Cell 11 - 17Cell 11 - 18Cell 11 - 19Cell 11 - 20
Row HeaderCell 12 - 1Cell 12 - 2Cell 12 - 3Cell 12 - 4Cell 12 - 5Cell 12 - 6Cell 12 - 7Cell 12 - 8Cell 12 - 9Cell 12 - 10Cell 12 - 11Cell 12 - 12Cell 12 - 13Cell 12 - 14Cell 12 - 15Cell 12 - 16Cell 12 - 17Cell 12 - 18Cell 12 - 19Cell 12 - 20
Row HeaderCell 13 - 1Cell 13 - 2Cell 13 - 3Cell 13 - 4Cell 13 - 5Cell 13 - 6Cell 13 - 7Cell 13 - 8Cell 13 - 9Cell 13 - 10Cell 13 - 11Cell 13 - 12Cell 13 - 13Cell 13 - 14Cell 13 - 15Cell 13 - 16Cell 13 - 17Cell 13 - 18Cell 13 - 19Cell 13 - 20
Row HeaderCell 14 - 1Cell 14 - 2Cell 14 - 3Cell 14 - 4Cell 14 - 5Cell 14 - 6Cell 14 - 7Cell 14 - 8Cell 14 - 9Cell 14 - 10Cell 14 - 11Cell 14 - 12Cell 14 - 13Cell 14 - 14Cell 14 - 15Cell 14 - 16Cell 14 - 17Cell 14 - 18Cell 14 - 19Cell 14 - 20
Row HeaderCell 15 - 1Cell 15 - 2Cell 15 - 3Cell 15 - 4Cell 15 - 5Cell 15 - 6Cell 15 - 7Cell 15 - 8Cell 15 - 9Cell 15 - 10Cell 15 - 11Cell 15 - 12Cell 15 - 13Cell 15 - 14Cell 15 - 15Cell 15 - 16Cell 15 - 17Cell 15 - 18Cell 15 - 19Cell 15 - 20
Row HeaderCell 16 - 1Cell 16 - 2Cell 16 - 3Cell 16 - 4Cell 16 - 5Cell 16 - 6Cell 16 - 7Cell 16 - 8Cell 16 - 9Cell 16 - 10Cell 16 - 11Cell 16 - 12Cell 16 - 13Cell 16 - 14Cell 16 - 15Cell 16 - 16Cell 16 - 17Cell 16 - 18Cell 16 - 19Cell 16 - 20
Row HeaderCell 17 - 1Cell 17 - 2Cell 17 - 3Cell 17 - 4Cell 17 - 5Cell 17 - 6Cell 17 - 7Cell 17 - 8Cell 17 - 9Cell 17 - 10Cell 17 - 11Cell 17 - 12Cell 17 - 13Cell 17 - 14Cell 17 - 15Cell 17 - 16Cell 17 - 17Cell 17 - 18Cell 17 - 19Cell 17 - 20
Row HeaderCell 18 - 1Cell 18 - 2Cell 18 - 3Cell 18 - 4Cell 18 - 5Cell 18 - 6Cell 18 - 7Cell 18 - 8Cell 18 - 9Cell 18 - 10Cell 18 - 11Cell 18 - 12Cell 18 - 13Cell 18 - 14Cell 18 - 15Cell 18 - 16Cell 18 - 17Cell 18 - 18Cell 18 - 19Cell 18 - 20
Row HeaderCell 19 - 1Cell 19 - 2Cell 19 - 3Cell 19 - 4Cell 19 - 5Cell 19 - 6Cell 19 - 7Cell 19 - 8Cell 19 - 9Cell 19 - 10Cell 19 - 11Cell 19 - 12Cell 19 - 13Cell 19 - 14Cell 19 - 15Cell 19 - 16Cell 19 - 17Cell 19 - 18Cell 19 - 19Cell 19 - 20
Row HeaderCell 20 - 1Cell 20 - 2Cell 20 - 3Cell 20 - 4Cell 20 - 5Cell 20 - 6Cell 20 - 7Cell 20 - 8Cell 20 - 9Cell 20 - 10Cell 20 - 11Cell 20 - 12Cell 20 - 13Cell 20 - 14Cell 20 - 15Cell 20 - 16Cell 20 - 17Cell 20 - 18Cell 20 - 19Cell 20 - 20
Row HeaderCell 21 - 1Cell 21 - 2Cell 21 - 3Cell 21 - 4Cell 21 - 5Cell 21 - 6Cell 21 - 7Cell 21 - 8Cell 21 - 9Cell 21 - 10Cell 21 - 11Cell 21 - 12Cell 21 - 13Cell 21 - 14Cell 21 - 15Cell 21 - 16Cell 21 - 17Cell 21 - 18Cell 21 - 19Cell 21 - 20
Row HeaderCell 22 - 1Cell 22 - 2Cell 22 - 3Cell 22 - 4Cell 22 - 5Cell 22 - 6Cell 22 - 7Cell 22 - 8Cell 22 - 9Cell 22 - 10Cell 22 - 11Cell 22 - 12Cell 22 - 13Cell 22 - 14Cell 22 - 15Cell 22 - 16Cell 22 - 17Cell 22 - 18Cell 22 - 19Cell 22 - 20
Row HeaderCell 23 - 1Cell 23 - 2Cell 23 - 3Cell 23 - 4Cell 23 - 5Cell 23 - 6Cell 23 - 7Cell 23 - 8Cell 23 - 9Cell 23 - 10Cell 23 - 11Cell 23 - 12Cell 23 - 13Cell 23 - 14Cell 23 - 15Cell 23 - 16Cell 23 - 17Cell 23 - 18Cell 23 - 19Cell 23 - 20
Row HeaderCell 24 - 1Cell 24 - 2Cell 24 - 3Cell 24 - 4Cell 24 - 5Cell 24 - 6Cell 24 - 7Cell 24 - 8Cell 24 - 9Cell 24 - 10Cell 24 - 11Cell 24 - 12Cell 24 - 13Cell 24 - 14Cell 24 - 15Cell 24 - 16Cell 24 - 17Cell 24 - 18Cell 24 - 19Cell 24 - 20
Row HeaderCell 25 - 1Cell 25 - 2Cell 25 - 3Cell 25 - 4Cell 25 - 5Cell 25 - 6Cell 25 - 7Cell 25 - 8Cell 25 - 9Cell 25 - 10Cell 25 - 11Cell 25 - 12Cell 25 - 13Cell 25 - 14Cell 25 - 15Cell 25 - 16Cell 25 - 17Cell 25 - 18Cell 25 - 19Cell 25 - 20
Row HeaderCell 26 - 1Cell 26 - 2Cell 26 - 3Cell 26 - 4Cell 26 - 5Cell 26 - 6Cell 26 - 7Cell 26 - 8Cell 26 - 9Cell 26 - 10Cell 26 - 11Cell 26 - 12Cell 26 - 13Cell 26 - 14Cell 26 - 15Cell 26 - 16Cell 26 - 17Cell 26 - 18Cell 26 - 19Cell 26 - 20
Row HeaderCell 27 - 1Cell 27 - 2Cell 27 - 3Cell 27 - 4Cell 27 - 5Cell 27 - 6Cell 27 - 7Cell 27 - 8Cell 27 - 9Cell 27 - 10Cell 27 - 11Cell 27 - 12Cell 27 - 13Cell 27 - 14Cell 27 - 15Cell 27 - 16Cell 27 - 17Cell 27 - 18Cell 27 - 19Cell 27 - 20
Row HeaderCell 28 - 1Cell 28 - 2Cell 28 - 3Cell 28 - 4Cell 28 - 5Cell 28 - 6Cell 28 - 7Cell 28 - 8Cell 28 - 9Cell 28 - 10Cell 28 - 11Cell 28 - 12Cell 28 - 13Cell 28 - 14Cell 28 - 15Cell 28 - 16Cell 28 - 17Cell 28 - 18Cell 28 - 19Cell 28 - 20
Row HeaderCell 29 - 1Cell 29 - 2Cell 29 - 3Cell 29 - 4Cell 29 - 5Cell 29 - 6Cell 29 - 7Cell 29 - 8Cell 29 - 9Cell 29 - 10Cell 29 - 11Cell 29 - 12Cell 29 - 13Cell 29 - 14Cell 29 - 15Cell 29 - 16Cell 29 - 17Cell 29 - 18Cell 29 - 19Cell 29 - 20
Row HeaderCell 30 - 1Cell 30 - 2Cell 30 - 3Cell 30 - 4Cell 30 - 5Cell 30 - 6Cell 30 - 7Cell 30 - 8Cell 30 - 9Cell 30 - 10Cell 30 - 11Cell 30 - 12Cell 30 - 13Cell 30 - 14Cell 30 - 15Cell 30 - 16Cell 30 - 17Cell 30 - 18Cell 30 - 19Cell 30 - 20
Sticky Footer
import { useCheckboxGroup } from "@react-md/core/form/useCheckboxGroup";
import { StickyTableSection } from "@react-md/core/table/StickyTableSection";
import { Table } from "@react-md/core/table/Table";
import { TableBody } from "@react-md/core/table/TableBody";
import { TableCell } from "@react-md/core/table/TableCell";
import { TableCheckbox } from "@react-md/core/table/TableCheckbox";
import { TableContainer } from "@react-md/core/table/TableContainer";
import { TableRow } from "@react-md/core/table/TableRow";
import type { ReactElement } from "react";

import styles from "./StickyColumnsExample.module.scss";

const rows = Array.from({ length: 30 }, (_, i) => `row-${i + 1}`);
const headers = Array.from({ length: 20 }, (_, i) => `Header ${i + 1}`);

export default function StickyColumnsExample(): ReactElement {
  const { getCheckboxProps, getIndeterminateProps } = useCheckboxGroup({
    name: "selection",
    values: rows,
  });

  return (
    <TableContainer className={styles.container}>
      <Table fullWidth>
        <StickyTableSection type="header">
          <TableRow>
            <TableCheckbox
              id="sticky-header-checkbox"
              {...getIndeterminateProps()}
              colSpan={2}
              sticky
            />
            {headers.map((header) => (
              <TableCell key={header}>{header}</TableCell>
            ))}
          </TableRow>
        </StickyTableSection>
        <TableBody>
          {rows.map((row, rowIndex) => {
            const checkboxProps = getCheckboxProps(row);
            const { checked, onChange } = checkboxProps;

            return (
              <TableRow
                key={row}
                clickable
                selected={checked}
                onClick={onChange}
              >
                <TableCheckbox
                  id={`${row}-checkbox`}
                  {...checkboxProps}
                  sticky
                />
                <TableCell header sticky className={styles.sticky}>
                  Row Header
                </TableCell>
                {headers.map((header, cellIndex) => (
                  <TableCell key={header}>
                    {`Cell ${rowIndex + 1} - ${cellIndex + 1}`}
                  </TableCell>
                ))}
              </TableRow>
            );
          })}
        </TableBody>
        <StickyTableSection type="footer">
          <TableRow>
            <TableCell sticky colSpan={2}>
              Sticky Footer
            </TableCell>
            <TableCell colSpan={20} />
          </TableRow>
        </StickyTableSection>
      </Table>
    </TableContainer>
  );
}

Press Enter to start editing.

@use "everything";

.container {
  max-height: 25rem;
}

.sticky {
  // if you don't need auto-RTL support, you could just set the `left` value
  // instead of using the mixin and updating the css variable
  // @include everything.table-set-var(sticky-cell, 4rem);
  @include everything.table-set-var(
    sticky-cell,
    calc(
      everything.$table-cell-input-toggle-horizontal-padding * 2 +
        everything.$input-toggle-normal-size * 2
    )
  );
}

Press Enter to start editing.

Sticky Active Styles

When the TableHeader/TableFooter have the sticky prop enabled, some magic happens behind the scenes to automatically raise the elevation for the TableHeader/TableFooter when covering rows of content by scroll position. These styles can be configured globally by the core.$table-sticky-header-inactive-styles, core.$table-sticky-header-active-styles, core.$table-sticky-footer-inactive-styles, and core.$table-sticky-footer-active-styles Sass variables.

If the styles should not be configured globally, provide a custom stickyActiveClassName and optionally className to override the styling.

Header 1Header 2
Row 1 Cell 1Row 1 Cell 2
Row 2 Cell 1Row 2 Cell 2
Row 3 Cell 1Row 3 Cell 2
Row 4 Cell 1Row 4 Cell 2
Row 5 Cell 1Row 5 Cell 2
Row 6 Cell 1Row 6 Cell 2
Row 7 Cell 1Row 7 Cell 2
Row 8 Cell 1Row 8 Cell 2
Row 9 Cell 1Row 9 Cell 2
Row 10 Cell 1Row 10 Cell 2
Row 11 Cell 1Row 11 Cell 2
Row 12 Cell 1Row 12 Cell 2
Row 13 Cell 1Row 13 Cell 2
Row 14 Cell 1Row 14 Cell 2
Row 15 Cell 1Row 15 Cell 2
Row 16 Cell 1Row 16 Cell 2
Row 17 Cell 1Row 17 Cell 2
Row 18 Cell 1Row 18 Cell 2
Row 19 Cell 1Row 19 Cell 2
Row 20 Cell 1Row 20 Cell 2
Content
import { cssUtils } from "@react-md/core/cssUtils";
import { StickyTableSection } from "@react-md/core/table/StickyTableSection";
import { Table } from "@react-md/core/table/Table";
import { TableBody } from "@react-md/core/table/TableBody";
import { TableCell } from "@react-md/core/table/TableCell";
import { TableContainer } from "@react-md/core/table/TableContainer";
import { TableRow } from "@react-md/core/table/TableRow";
import {
  isTableFooterStickyActive,
  isTableHeaderStickyActive,
} from "@react-md/core/table/useStickyTableSection";
import { type ReactElement } from "react";

import styles from "./StickyActiveStylesExample.module.scss";

export default function StickyActiveStylesExample(): ReactElement {
  return (
    <TableContainer className={styles.container}>
      <Table fullWidth>
        <StickyTableSection
          type="header"
          stickyActiveClassName={cssUtils({
            backgroundColor: "secondary",
            className: styles.active,
          })}
          isStickyActive={(entry, isInTableContainer) => {
            // this can be used to manually override if the header should be
            // considered sticky active

            // this is the default implementation and can be used as a fallback
            return isTableHeaderStickyActive(entry, isInTableContainer);
          }}
        >
          <TableRow>
            <TableCell>Header 1</TableCell>
            <TableCell>Header 2</TableCell>
          </TableRow>
        </StickyTableSection>
        <TableBody>
          {Array.from({ length: 20 }, (_, i) => (
            <TableRow key={i}>
              <TableCell>{`Row ${i + 1} Cell 1`}</TableCell>
              <TableCell>{`Row ${i + 1} Cell 2`}</TableCell>
            </TableRow>
          ))}
        </TableBody>
        <StickyTableSection
          type="footer"
          stickyActiveClassName={cssUtils({
            backgroundColor: "primary",
            className: styles.active,
          })}
          isStickyActive={(entry, isInTableContainer) => {
            // this can be used to manually override if the header should be
            // considered sticky active

            // this is the default implementation and can be used as a fallback
            return isTableFooterStickyActive(entry, isInTableContainer);
          }}
        >
          <TableRow>
            <TableCell colSpan={2}>Content</TableCell>
          </TableRow>
        </StickyTableSection>
      </Table>
    </TableContainer>
  );
}

Press Enter to start editing.

@use "everything";

.container {
  max-height: 20rem;
  width: 100%;
}

.active {
  @include everything.table-set-var(cell-color, currentcolor);
}

Press Enter to start editing.

Disable Sticky Active Styles

The sticky active styles can also be disabled by enabling the disableStickyStyles or using the TableHeader/TableFooter components instead with the sticky prop enabled.

Header 1Header 2
Row 1 Cell 1Row 1 Cell 2
Row 2 Cell 1Row 2 Cell 2
Row 3 Cell 1Row 3 Cell 2
Row 4 Cell 1Row 4 Cell 2
Row 5 Cell 1Row 5 Cell 2
Row 6 Cell 1Row 6 Cell 2
Row 7 Cell 1Row 7 Cell 2
Row 8 Cell 1Row 8 Cell 2
Row 9 Cell 1Row 9 Cell 2
Row 10 Cell 1Row 10 Cell 2
Row 11 Cell 1Row 11 Cell 2
Row 12 Cell 1Row 12 Cell 2
Row 13 Cell 1Row 13 Cell 2
Row 14 Cell 1Row 14 Cell 2
Row 15 Cell 1Row 15 Cell 2
Row 16 Cell 1Row 16 Cell 2
Row 17 Cell 1Row 17 Cell 2
Row 18 Cell 1Row 18 Cell 2
Row 19 Cell 1Row 19 Cell 2
Row 20 Cell 1Row 20 Cell 2
Content
import { StickyTableSection } from "@react-md/core/table/StickyTableSection";
import { Table } from "@react-md/core/table/Table";
import { TableBody } from "@react-md/core/table/TableBody";
import { TableCell } from "@react-md/core/table/TableCell";
import { TableContainer } from "@react-md/core/table/TableContainer";
import { TableHeader } from "@react-md/core/table/TableHeader";
import { TableRow } from "@react-md/core/table/TableRow";
import { type ReactElement } from "react";

import styles from "./DisableStickyActiveStylesExample.module.scss";

export default function DisableStickyActiveStylesExample(): ReactElement {
  return (
    <TableContainer className={styles.container}>
      <Table fullWidth>
        <TableHeader sticky>
          <TableRow>
            <TableCell>Header 1</TableCell>
            <TableCell>Header 2</TableCell>
          </TableRow>
        </TableHeader>
        <TableBody>
          {Array.from({ length: 20 }, (_, i) => (
            <TableRow key={i}>
              <TableCell>{`Row ${i + 1} Cell 1`}</TableCell>
              <TableCell>{`Row ${i + 1} Cell 2`}</TableCell>
            </TableRow>
          ))}
        </TableBody>
        <StickyTableSection type="footer" disableStickyStyles>
          <TableRow>
            <TableCell colSpan={2}>Content</TableCell>
          </TableRow>
        </StickyTableSection>
      </Table>
    </TableContainer>
  );
}

Press Enter to start editing.

.container {
  max-height: 20rem;
  width: 100%;
}

Press Enter to start editing.