React AriaExamples

Stock Table

A stock Table featuring sticky headers, sorting, multiple selection, and column resizing, styled with Tailwind CSS.

Example#


import {Cell, Column, ColumnResizer, Group, ResizableTableContainer, Row, Table, TableBody, TableHeader} from 'react-aria-components';
import type {CellProps, ColumnProps, RowProps, SortDescriptor} from 'react-aria-components';
import ArrowUpIcon from '@spectrum-icons/ui/ArrowUpSmall';
import {useMemo, useState} from 'react';

function StockTableExample() {
  let [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
    column: 'symbol',
    direction: 'ascending'
  });
  let sortedItems = useMemo(() => {
    return stocks.sort((a, b) => {
      let first = a[sortDescriptor.column];
      let second = b[sortDescriptor.column];
      let cmp = first.localeCompare(second);
      if (sortDescriptor.direction === 'descending') {
        cmp *= -1;
      }
      return cmp;
    });
  }, [sortDescriptor]);

  return (
    <div className="bg-gradient-to-r from-indigo-500 to-violet-500 p-8 rounded-lg flex items-center justify-center md:col-span-2">
      <ResizableTableContainer className="max-h-[280px] w-full overflow-auto scroll-pt-[2.321rem] relative bg-white rounded-lg shadow text-gray-600">
        <Table
          aria-label="Stocks"
          selectionMode="multiple"
          selectionBehavior="replace"
          sortDescriptor={sortDescriptor}
          onSortChange={setSortDescriptor}
          className="border-separate border-spacing-0"
        >
          <TableHeader>
            <StockColumn id="symbol" allowsSorting>Symbol</StockColumn>
            <StockColumn id="name" isRowHeader allowsSorting defaultWidth="3fr">
              Name
            </StockColumn>
            <StockColumn id="marketCap" allowsSorting>Market Cap</StockColumn>
            <StockColumn id="sector" allowsSorting>Sector</StockColumn>
            <StockColumn id="industry" allowsSorting defaultWidth="2fr">
              Industry
            </StockColumn>
          </TableHeader>
          <TableBody items={sortedItems}>
            {(item) => (
              <StockRow>
                <StockCell>
                  <span className="font-mono bg-slate-100 border border-slate-200 rounded px-1 group-selected:bg-slate-700 group-selected:border-slate-800">
                    ${item.symbol}
                  </span>
                </StockCell>
                <StockCell className="font-semibold">{item.name}</StockCell>
                <StockCell>{item.marketCap}</StockCell>
                <StockCell>{item.sector}</StockCell>
                <StockCell>{item.industry}</StockCell>
              </StockRow>
            )}
          </TableBody>
        </Table>
      </ResizableTableContainer>
    </div>
  );
}

function StockColumn(props: ColumnProps & { children: React.ReactNode }) {
  return (
    <Column
      {...props}
      className="sticky top-0 p-0 border-0 border-b border-solid border-slate-300 bg-slate-200 font-bold text-left cursor-default first:rounded-tl-lg last:rounded-tr-lg whitespace-nowrap outline-none"
    >
      {({ allowsSorting, sortDirection }) => (
        <div className="flex items-center pl-4 py-1">
          <Group
            role="presentation"
            tabIndex={-1}
            className="flex flex-1 items-center overflow-hidden outline-none rounded focus-visible:ring-2 ring-slate-600"
          >
            <span className="flex-1 truncate">{props.children}</span>
            {allowsSorting && (
              <span
                className={`ml-1 w-4 h-4 flex items-center justify-center transition ${
                  sortDirection === 'descending' ? 'rotate-180' : ''
                }`}
              >
                {sortDirection && <ArrowUpIcon width={8} height={10} />}
              </span>
            )}
          </Group>
          <ColumnResizer className="w-px px-[8px] py-1 h-5 bg-clip-content bg-slate-400 cursor-col-resize rounded resizing:bg-slate-800 resizing:w-[2px] resizing:pl-[7px] focus-visible:ring-2 ring-slate-600 ring-inset" />
        </div>
      )}
    </Column>
  );
}

function StockRow<T extends object>(props: RowProps<T>) {
  return (
    <Row
      {...props}
      className="even:bg-slate-100 selected:bg-slate-600 selected:text-white cursor-default group outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-slate-600 focus-visible:-outline-offset-4 selected:focus-visible:outline-white"
    />
  );
}

function StockCell(props: CellProps) {
  return (
    <Cell
      {...props}
      className={`px-4 py-2 truncate ${props.className} focus-visible:outline focus-visible:outline-2 focus-visible:outline-slate-600 focus-visible:-outline-offset-4 group-selected:focus-visible:outline-white`}
    />
  );
}
import {
  Cell,
  Column,
  ColumnResizer,
  Group,
  ResizableTableContainer,
  Row,
  Table,
  TableBody,
  TableHeader
} from 'react-aria-components';
import type {
  CellProps,
  ColumnProps,
  RowProps,
  SortDescriptor
} from 'react-aria-components';
import ArrowUpIcon from '@spectrum-icons/ui/ArrowUpSmall';
import {useMemo, useState} from 'react';

function StockTableExample() {
  let [sortDescriptor, setSortDescriptor] = useState<
    SortDescriptor
  >({ column: 'symbol', direction: 'ascending' });
  let sortedItems = useMemo(() => {
    return stocks.sort((a, b) => {
      let first = a[sortDescriptor.column];
      let second = b[sortDescriptor.column];
      let cmp = first.localeCompare(second);
      if (sortDescriptor.direction === 'descending') {
        cmp *= -1;
      }
      return cmp;
    });
  }, [sortDescriptor]);

  return (
    <div className="bg-gradient-to-r from-indigo-500 to-violet-500 p-8 rounded-lg flex items-center justify-center md:col-span-2">
      <ResizableTableContainer className="max-h-[280px] w-full overflow-auto scroll-pt-[2.321rem] relative bg-white rounded-lg shadow text-gray-600">
        <Table
          aria-label="Stocks"
          selectionMode="multiple"
          selectionBehavior="replace"
          sortDescriptor={sortDescriptor}
          onSortChange={setSortDescriptor}
          className="border-separate border-spacing-0"
        >
          <TableHeader>
            <StockColumn id="symbol" allowsSorting>
              Symbol
            </StockColumn>
            <StockColumn
              id="name"
              isRowHeader
              allowsSorting
              defaultWidth="3fr"
            >
              Name
            </StockColumn>
            <StockColumn id="marketCap" allowsSorting>
              Market Cap
            </StockColumn>
            <StockColumn id="sector" allowsSorting>
              Sector
            </StockColumn>
            <StockColumn
              id="industry"
              allowsSorting
              defaultWidth="2fr"
            >
              Industry
            </StockColumn>
          </TableHeader>
          <TableBody items={sortedItems}>
            {(item) => (
              <StockRow>
                <StockCell>
                  <span className="font-mono bg-slate-100 border border-slate-200 rounded px-1 group-selected:bg-slate-700 group-selected:border-slate-800">
                    ${item.symbol}
                  </span>
                </StockCell>
                <StockCell className="font-semibold">
                  {item.name}
                </StockCell>
                <StockCell>{item.marketCap}</StockCell>
                <StockCell>{item.sector}</StockCell>
                <StockCell>{item.industry}</StockCell>
              </StockRow>
            )}
          </TableBody>
        </Table>
      </ResizableTableContainer>
    </div>
  );
}

function StockColumn(
  props: ColumnProps & { children: React.ReactNode }
) {
  return (
    <Column
      {...props}
      className="sticky top-0 p-0 border-0 border-b border-solid border-slate-300 bg-slate-200 font-bold text-left cursor-default first:rounded-tl-lg last:rounded-tr-lg whitespace-nowrap outline-none"
    >
      {({ allowsSorting, sortDirection }) => (
        <div className="flex items-center pl-4 py-1">
          <Group
            role="presentation"
            tabIndex={-1}
            className="flex flex-1 items-center overflow-hidden outline-none rounded focus-visible:ring-2 ring-slate-600"
          >
            <span className="flex-1 truncate">
              {props.children}
            </span>
            {allowsSorting && (
              <span
                className={`ml-1 w-4 h-4 flex items-center justify-center transition ${
                  sortDirection === 'descending'
                    ? 'rotate-180'
                    : ''
                }`}
              >
                {sortDirection && (
                  <ArrowUpIcon width={8} height={10} />
                )}
              </span>
            )}
          </Group>
          <ColumnResizer className="w-px px-[8px] py-1 h-5 bg-clip-content bg-slate-400 cursor-col-resize rounded resizing:bg-slate-800 resizing:w-[2px] resizing:pl-[7px] focus-visible:ring-2 ring-slate-600 ring-inset" />
        </div>
      )}
    </Column>
  );
}

function StockRow<T extends object>(props: RowProps<T>) {
  return (
    <Row
      {...props}
      className="even:bg-slate-100 selected:bg-slate-600 selected:text-white cursor-default group outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-slate-600 focus-visible:-outline-offset-4 selected:focus-visible:outline-white"
    />
  );
}

function StockCell(props: CellProps) {
  return (
    <Cell
      {...props}
      className={`px-4 py-2 truncate ${props.className} focus-visible:outline focus-visible:outline-2 focus-visible:outline-slate-600 focus-visible:-outline-offset-4 group-selected:focus-visible:outline-white`}
    />
  );
}
import {
  Cell,
  Column,
  ColumnResizer,
  Group,
  ResizableTableContainer,
  Row,
  Table,
  TableBody,
  TableHeader
} from 'react-aria-components';
import type {
  CellProps,
  ColumnProps,
  RowProps,
  SortDescriptor
} from 'react-aria-components';
import ArrowUpIcon from '@spectrum-icons/ui/ArrowUpSmall';
import {
  useMemo,
  useState
} from 'react';

function StockTableExample() {
  let [
    sortDescriptor,
    setSortDescriptor
  ] = useState<
    SortDescriptor
  >({
    column: 'symbol',
    direction:
      'ascending'
  });
  let sortedItems =
    useMemo(() => {
      return stocks.sort(
        (a, b) => {
          let first =
            a[
              sortDescriptor
                .column
            ];
          let second =
            b[
              sortDescriptor
                .column
            ];
          let cmp = first
            .localeCompare(
              second
            );
          if (
            sortDescriptor
              .direction ===
              'descending'
          ) {
            cmp *= -1;
          }
          return cmp;
        }
      );
    }, [sortDescriptor]);

  return (
    <div className="bg-gradient-to-r from-indigo-500 to-violet-500 p-8 rounded-lg flex items-center justify-center md:col-span-2">
      <ResizableTableContainer className="max-h-[280px] w-full overflow-auto scroll-pt-[2.321rem] relative bg-white rounded-lg shadow text-gray-600">
        <Table
          aria-label="Stocks"
          selectionMode="multiple"
          selectionBehavior="replace"
          sortDescriptor={sortDescriptor}
          onSortChange={setSortDescriptor}
          className="border-separate border-spacing-0"
        >
          <TableHeader>
            <StockColumn
              id="symbol"
              allowsSorting
            >
              Symbol
            </StockColumn>
            <StockColumn
              id="name"
              isRowHeader
              allowsSorting
              defaultWidth="3fr"
            >
              Name
            </StockColumn>
            <StockColumn
              id="marketCap"
              allowsSorting
            >
              Market Cap
            </StockColumn>
            <StockColumn
              id="sector"
              allowsSorting
            >
              Sector
            </StockColumn>
            <StockColumn
              id="industry"
              allowsSorting
              defaultWidth="2fr"
            >
              Industry
            </StockColumn>
          </TableHeader>
          <TableBody
            items={sortedItems}
          >
            {(item) => (
              <StockRow>
                <StockCell>
                  <span className="font-mono bg-slate-100 border border-slate-200 rounded px-1 group-selected:bg-slate-700 group-selected:border-slate-800">
                    ${item
                      .symbol}
                  </span>
                </StockCell>
                <StockCell className="font-semibold">
                  {item
                    .name}
                </StockCell>
                <StockCell>
                  {item
                    .marketCap}
                </StockCell>
                <StockCell>
                  {item
                    .sector}
                </StockCell>
                <StockCell>
                  {item
                    .industry}
                </StockCell>
              </StockRow>
            )}
          </TableBody>
        </Table>
      </ResizableTableContainer>
    </div>
  );
}

function StockColumn(
  props: ColumnProps & {
    children:
      React.ReactNode;
  }
) {
  return (
    <Column
      {...props}
      className="sticky top-0 p-0 border-0 border-b border-solid border-slate-300 bg-slate-200 font-bold text-left cursor-default first:rounded-tl-lg last:rounded-tr-lg whitespace-nowrap outline-none"
    >
      {(
        {
          allowsSorting,
          sortDirection
        }
      ) => (
        <div className="flex items-center pl-4 py-1">
          <Group
            role="presentation"
            tabIndex={-1}
            className="flex flex-1 items-center overflow-hidden outline-none rounded focus-visible:ring-2 ring-slate-600"
          >
            <span className="flex-1 truncate">
              {props
                .children}
            </span>
            {allowsSorting &&
              (
                <span
                  className={`ml-1 w-4 h-4 flex items-center justify-center transition ${
                    sortDirection ===
                        'descending'
                      ? 'rotate-180'
                      : ''
                  }`}
                >
                  {sortDirection &&
                    (
                      <ArrowUpIcon
                        width={8}
                        height={10}
                      />
                    )}
                </span>
              )}
          </Group>
          <ColumnResizer className="w-px px-[8px] py-1 h-5 bg-clip-content bg-slate-400 cursor-col-resize rounded resizing:bg-slate-800 resizing:w-[2px] resizing:pl-[7px] focus-visible:ring-2 ring-slate-600 ring-inset" />
        </div>
      )}
    </Column>
  );
}

function StockRow<
  T extends object
>(props: RowProps<T>) {
  return (
    <Row
      {...props}
      className="even:bg-slate-100 selected:bg-slate-600 selected:text-white cursor-default group outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-slate-600 focus-visible:-outline-offset-4 selected:focus-visible:outline-white"
    />
  );
}

function StockCell(
  props: CellProps
) {
  return (
    <Cell
      {...props}
      className={`px-4 py-2 truncate ${props.className} focus-visible:outline focus-visible:outline-2 focus-visible:outline-slate-600 focus-visible:-outline-offset-4 group-selected:focus-visible:outline-white`}
    />
  );
}

Tailwind config#

This example uses the tailwindcss-react-aria-components plugin. Add it to your tailwind.config.js:

module.exports = {
  // ...
  plugins: [
    require('tailwindcss-react-aria-components')
  ]
};
module.exports = {
  // ...
  plugins: [
    require('tailwindcss-react-aria-components')
  ]
};
module.exports = {
  // ...
  plugins: [
    require(
      'tailwindcss-react-aria-components'
    )
  ]
};

Components#


Table
A table displays data in rows and columns, with row selection and sorting.