Pool Table
A flexible data table with token icons, user-defined columns, and optional row actions
| $245.8M | $18.2M | 12.4% | ||
| $12.4M | $3.1M | 24.8% | ||
| $89.2M | $5.6M | 8.1% | ||
| $156.3M | $8.4M | 9.6% |
import { PoolTable } from "@/components/sol/pool-table";
import { Button } from "@/components/ui/button";
const SOL_ICON =
"https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/So11111111111111111111111111111111111111112/logo.png";
const USDC_ICON =
"https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v/logo.png";
export function PoolTableDemo() {
return (
<PoolTable
columns={[
{ key: "tvl", label: "TVL" },
{ key: "volume", label: "Volume (24h)" },
{ key: "apy", label: "APY", className: "text-emerald-500" },
]}
rows={[
{
icons: [
{ src: SOL_ICON, alt: "SOL" },
{ src: USDC_ICON, alt: "USDC" },
],
data: { tvl: "$245.8M", volume: "$18.2M", apy: "12.4%" },
},
]}
actions={[
<Button key="sol-usdc" variant="outline" size="sm">Deposit</Button>,
]}
/>
);
}Installation
pnpm dlx shadcn@latest add @solanaui/pool-table
npx shadcn@latest add @solanaui/pool-table
yarn dlx shadcn@latest add @solanaui/pool-table
Usage
Define your columns and rows. Each row has icons (one or more token icons), an optional name, and a data object mapping column keys to display values.
<PoolTable
columns={[
{ key: "tvl", label: "TVL" },
{ key: "apy", label: "APY", className: "text-emerald-500" },
]}
rows={[
{
icons: [
{ src: SOL_ICON, alt: "SOL" },
{ src: USDC_ICON, alt: "USDC" },
],
data: { tvl: "$245.8M", apy: "12.4%" },
},
]}
/>Single Icon Per Row
Works with a single icon for token listings, validator tables, or any data table with icons.
<PoolTable
columns={[
{ key: "price", label: "Price" },
{ key: "apy", label: "APY", className: "text-emerald-500" },
]}
rows={[
{
icons: [{ src: SOL_ICON, alt: "SOL" }],
name: "Solana",
data: { price: "$162.56", apy: "6.82%" },
},
{
icons: [{ src: USDC_ICON, alt: "USDC" }],
name: "USD Coin",
data: { price: "$1.00", apy: "8.45%" },
},
]}
/>With Actions
Use the actions array to add per-row action buttons. Combine with Dialog and ActionBox for interactive deposit flows.
import { Button } from "@/components/ui/button";
<PoolTable
columns={[
{ key: "tvl", label: "TVL" },
{ key: "apy", label: "APY", className: "text-emerald-500" },
]}
rows={[
{
icons: [{ src: SOL_ICON, alt: "SOL" }, { src: USDC_ICON, alt: "USDC" }],
data: { tvl: "$245.8M", apy: "12.4%" },
},
]}
actions={[
<Button key="sol-usdc" variant="outline" size="sm">Deposit</Button>,
]}
/>Source Code
"use client";import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";import React from "react";import { TokenIconGroup } from "@/registry/sol/token-icon-group";import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from "@/components/ui/table";import type { SortDirection } from "@/registry/lib/sort-utils";import { compareValues } from "@/registry/lib/sort-utils";import { cn } from "@/lib/utils";interface PoolTableColumn { key: string; label: string; className?: string;}interface PoolTableRow { icons: { src: string; alt?: string }[]; name?: string; data: Record<string, string>;}interface PoolTableProps { columns: PoolTableColumn[]; rows: PoolTableRow[]; actions?: React.ReactNode[]; className?: string;}const PoolTable = ({ columns, rows, actions, className }: PoolTableProps) => { const showActions = actions && actions.length > 0; const [sortKey, setSortKey] = React.useState<string | null>(null); const [sortDirection, setSortDirection] = React.useState<SortDirection>(null); const handleSort = (key: string) => { if (sortKey !== key) { setSortKey(key); setSortDirection("asc"); } else if (sortDirection === "asc") { setSortDirection("desc"); } else { setSortKey(null); setSortDirection(null); } }; const sortedIndices = React.useMemo(() => { const indices = rows.map((_, i) => i); if (!sortKey || !sortDirection) return indices; return indices.sort((a, b) => { let valA: string; let valB: string; if (sortKey === "__name__") { valA = rows[a].name ?? rows[a].icons .map((t) => t.alt) .filter(Boolean) .join("/"); valB = rows[b].name ?? rows[b].icons .map((t) => t.alt) .filter(Boolean) .join("/"); } else { valA = rows[a].data[sortKey] ?? ""; valB = rows[b].data[sortKey] ?? ""; } const result = compareValues(valA, valB); return sortDirection === "desc" ? -result : result; }); }, [rows, sortKey, sortDirection]); return ( <Table className={className}> <TableHeader> <TableRow> <TableHead> <button type="button" onClick={() => handleSort("__name__")} className={cn( "inline-flex items-center gap-1 cursor-pointer hover:text-foreground transition-colors", sortKey === "__name__" ? "text-foreground" : "text-muted-foreground", )} > Name {sortKey === "__name__" && sortDirection === "asc" && ( <ChevronUpIcon className="size-3.5" /> )} {sortKey === "__name__" && sortDirection === "desc" && ( <ChevronDownIcon className="size-3.5" /> )} </button> </TableHead> {columns.map((col) => { const isActive = sortKey === col.key; return ( <TableHead key={col.key} className={col.className}> <button type="button" onClick={() => handleSort(col.key)} className={cn( "inline-flex items-center gap-1 cursor-pointer hover:text-foreground transition-colors", isActive ? "text-foreground" : "text-muted-foreground", )} > {col.label} {isActive && sortDirection === "asc" && ( <ChevronUpIcon className="size-3.5" /> )} {isActive && sortDirection === "desc" && ( <ChevronDownIcon className="size-3.5" /> )} </button> </TableHead> ); })} {showActions && <TableHead className="text-right" />} </TableRow> </TableHeader> <TableBody> {sortedIndices.map((originalIndex) => { const row = rows[originalIndex]; const rowName = row.name ?? row.icons .map((t) => t.alt) .filter(Boolean) .join("/"); return ( <TableRow key={`${rowName}-${row.data[columns[0]?.key] ?? originalIndex}`} > <TableCell> <div className="flex items-center gap-2"> <TokenIconGroup tokens={row.icons} size={20} overlap={row.icons.length > 1 ? -6 : 0} /> <span className="font-medium">{rowName}</span> </div> </TableCell> {columns.map((col) => ( <TableCell key={col.key} className={cn(col.className)}> {row.data[col.key] ?? "-"} </TableCell> ))} {showActions && actions[originalIndex] && ( <TableCell className="text-right"> {actions[originalIndex]} </TableCell> )} </TableRow> ); })} </TableBody> </Table> );};export type { PoolTableProps, PoolTableRow, PoolTableColumn };export { PoolTable };