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 && <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 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 && (
<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 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 &&
(
<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 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.