Create custom cell renderers and editors.
Create custom cell renderers to display data in unique ways. Custom cells can include interactive elements, formatting, icons, images, and complex layouts.
{
accessorKey: 'status',
header: 'Status',
cell: ({ getValue }) => {
const status = getValue() as string;
return (
<Badge variant={status === 'active' ? 'default' : 'secondary'}>
{status}
</Badge>
);
},
}The cell function receives a context object with:
{
getValue: () => any, // Get cell value
row: Row<TData>, // Row object
column: Column<TData>, // Column object
table: Table<TData>, // Table instance
renderValue: () => any, // Formatted value
}{
accessorKey: 'price',
header: 'Price',
cell: ({ getValue }) => {
const price = getValue() as number;
return <span>${price.toFixed(2)}</span>;
},
}import { CheckCircle, XCircle } from 'lucide-react';
{
accessorKey: 'isActive',
header: 'Active',
cell: ({ getValue }) => {
const isActive = getValue() as boolean;
return isActive ? (
<CheckCircle className="h-4 w-4 text-green-500" />
) : (
<XCircle className="h-4 w-4 text-red-500" />
);
},
}{
accessorKey: 'stock',
header: 'Stock',
cell: ({ getValue }) => {
const stock = getValue() as number;
return (
<span className={cn(
'font-mono',
stock > 100 && 'text-green-600',
stock > 0 && stock <= 100 && 'text-yellow-600',
stock === 0 && 'text-red-600 font-bold'
)}>
{stock}
</span>
);
},
}import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
{
accessorKey: 'user',
header: 'User',
cell: ({ row }) => {
const user = row.original;
return (
<div className="flex items-center gap-3">
<Avatar className="h-8 w-8">
<AvatarImage src={user.avatar} />
<AvatarFallback>{user.name[0]}</AvatarFallback>
</Avatar>
<div>
<div className="font-medium">{user.name}</div>
<div className="text-sm text-muted-foreground">{user.email}</div>
</div>
</div>
);
},
}{
accessorKey: 'status',
header: 'Status',
cell: ({ getValue }) => {
const status = getValue() as string;
const variants = {
active: { variant: 'default', icon: CheckCircle, color: 'text-green-500' },
pending: { variant: 'secondary', icon: Clock, color: 'text-yellow-500' },
inactive: { variant: 'outline', icon: XCircle, color: 'text-gray-500' },
};
const config = variants[status] || variants.inactive;
const Icon = config.icon;
return (
<Badge variant={config.variant}>
<Icon className={cn('mr-1 h-3 w-3', config.color)} />
{status}
</Badge>
);
},
}import { Progress } from '@/components/ui/progress';
{
accessorKey: 'completion',
header: 'Progress',
cell: ({ getValue }) => {
const completion = getValue() as number;
return (
<div className="flex items-center gap-2">
<Progress value={completion} className="w-24" />
<span className="text-sm text-muted-foreground w-12">
{completion}%
</span>
</div>
);
},
}{
accessorKey: 'address',
header: 'Address',
cell: ({ row }) => {
const { street, city, country } = row.original.address;
return (
<div className="space-y-0.5">
<div className="font-medium">{street}</div>
<div className="text-sm text-muted-foreground">
{city}, {country}
</div>
</div>
);
},
}{
accessorKey: 'email',
header: 'Email',
cell: ({ getValue }) => {
const email = getValue() as string;
return (
<a
href={`mailto:${email}`}
className="text-primary hover:underline"
onClick={(e) => e.stopPropagation()} // Prevent row click
>
{email}
</a>
);
},
}import { Copy, Check } from 'lucide-react';
{
accessorKey: 'apiKey',
header: 'API Key',
cell: ({ getValue }) => {
const [copied, setCopied] = useState(false);
const apiKey = getValue() as string;
const handleCopy = async () => {
await navigator.clipboard.writeText(apiKey);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="flex items-center gap-2">
<code className="font-mono text-sm">{apiKey}</code>
<Button
size="sm"
variant="ghost"
onClick={(e) => {
e.stopPropagation();
handleCopy();
}}
>
{copied ? (
<Check className="h-3 w-3 text-green-500" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
</div>
);
},
}import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
{
id: 'actions',
header: 'Actions',
cell: ({ row }) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleEdit(row.original)}>
Edit
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleDuplicate(row.original)}>
Duplicate
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleDelete(row.original)}
className="text-destructive"
>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
},
}type PriceCellProps = {
amount: number;
currency?: string;
};
const PriceCell: React.FC<PriceCellProps> = ({
amount,
currency = 'USD'
}) => {
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency,
}).format(amount);
return (
<span className="font-mono text-right">
{formatted}
</span>
);
};
// Use in column
{
accessorKey: 'price',
header: 'Price',
cell: ({ getValue }) => <PriceCell amount={getValue() as number} />,
}import { format, formatDistanceToNow } from 'date-fns';
type DateCellProps = {
date: Date | string;
showRelative?: boolean;
};
const DateCell: React.FC<DateCellProps> = ({ date, showRelative }) => {
const dateObj = typeof date === 'string' ? new Date(date) : date;
return (
<div className="flex flex-col">
<span className="font-medium">
{format(dateObj, 'MMM d, yyyy')}
</span>
{showRelative && (
<span className="text-xs text-muted-foreground">
{formatDistanceToNow(dateObj, { addSuffix: true })}
</span>
)}
</div>
);
};
// Use in column
{
accessorKey: 'createdAt',
header: 'Created',
cell: ({ getValue }) => (
<DateCell date={getValue() as Date} showRelative />
),
}import { Badge } from '@/components/ui/badge';
type TagListCellProps = {
tags: string[];
max?: number;
};
const TagListCell: React.FC<TagListCellProps> = ({ tags, max = 3 }) => {
const visibleTags = tags.slice(0, max);
const remaining = tags.length - max;
return (
<div className="flex flex-wrap gap-1">
{visibleTags.map(tag => (
<Badge key={tag} variant="secondary" className="text-xs">
{tag}
</Badge>
))}
{remaining > 0 && (
<Badge variant="outline" className="text-xs">
+{remaining}
</Badge>
)}
</div>
);
};
// Use in column
{
accessorKey: 'tags',
header: 'Tags',
cell: ({ getValue }) => <TagListCell tags={getValue() as string[]} />,
}{
accessorKey: 'avatar',
header: 'Avatar',
cell: ({ getValue, row }) => (
<Avatar>
<AvatarImage src={getValue() as string} alt={row.original.name} />
<AvatarFallback>{row.original.name[0]}</AvatarFallback>
</Avatar>
),
}{
accessorKey: 'thumbnail',
header: 'Image',
cell: ({ getValue, row }) => (
<img
src={getValue() as string}
alt={row.original.name}
className="h-12 w-12 rounded object-cover"
/>
),
}import { Sparkline } from '@/components/ui/sparkline';
{
accessorKey: 'sales',
header: 'Sales Trend',
cell: ({ getValue }) => {
const data = getValue() as number[];
return <Sparkline data={data} width={100} height={30} />;
},
}import { BarChart, Bar } from 'recharts';
{
accessorKey: 'distribution',
header: 'Distribution',
cell: ({ getValue }) => {
const data = getValue() as Array<{ name: string; value: number }>;
return (
<BarChart width={80} height={30} data={data}>
<Bar dataKey="value" fill="#8884d8" />
</BarChart>
);
},
}Ensure custom cells are accessible:
{
accessorKey: 'discount',
header: 'Discount',
cell: ({ getValue }) => {
const discount = getValue() as number;
if (discount === 0) return null;
return (
<Badge variant="secondary">
{discount}% off
</Badge>
);
},
}{
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => {
const { status, error } = row.original;
switch (status) {
case 'success':
return <CheckCircle className="h-4 w-4 text-green-500" />;
case 'error':
return (
<Tooltip>
<TooltipTrigger>
<XCircle className="h-4 w-4 text-red-500" />
</TooltipTrigger>
<TooltipContent>{error}</TooltipContent>
</Tooltip>
);
case 'pending':
return <Loader2 className="h-4 w-4 animate-spin" />;
default:
return null;
}
},
}import { useState } from 'react';
import {
Avatar,
AvatarImage,
AvatarFallback,
} from '@/components/ui/avatar';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Mail, Phone, MoreHorizontal } from 'lucide-react';
const UserCell: React.FC<{ user: User }> = ({ user }) => {
return (
<div className="flex items-center gap-3">
<Avatar className="h-10 w-10">
<AvatarImage src={user.avatar} />
<AvatarFallback>{user.name[0]}</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium truncate">{user.name}</span>
{user.verified && (
<Badge variant="secondary" className="text-xs">
Verified
</Badge>
)}
</div>
<div className="flex items-center gap-3 text-sm text-muted-foreground">
<Tooltip>
<TooltipTrigger asChild>
<a
href={`mailto:${user.email}`}
className="hover:text-primary flex items-center gap-1"
onClick={(e) => e.stopPropagation()}
>
<Mail className="h-3 w-3" />
<span className="truncate max-w-[150px]">{user.email}</span>
</a>
</TooltipTrigger>
<TooltipContent>{user.email}</TooltipContent>
</Tooltip>
{user.phone && (
<Tooltip>
<TooltipTrigger asChild>
<a
href={`tel:${user.phone}`}
className="hover:text-primary flex items-center gap-1"
onClick={(e) => e.stopPropagation()}
>
<Phone className="h-3 w-3" />
{user.phone}
</a>
</TooltipTrigger>
<TooltipContent>{user.phone}</TooltipContent>
</Tooltip>
)}
</div>
</div>
</div>
);
};
// Use in column
{
accessorKey: 'name',
header: 'User',
size: 300,
cell: ({ row }) => <UserCell user={row.original} />,
}import { memo } from 'react';
const ExpensiveCell = memo<{ data: Data }>(({ data }) => {
// Complex rendering logic
return <div>{/* ... */}</div>;
});
// Use in column
{
cell: ({ row }) => <ExpensiveCell data={row.original} />,
}// ✗ Bad: Creates new function on every render
{
cell: ({ row }) => (
<Button onClick={() => handleClick(row.original)}>
Click
</Button>
),
}
// ✓ Good: Reusable component
const ActionCell: React.FC<{ row: Row<Data> }> = ({ row }) => {
const handleClick = () => handleAction(row.original);
return <Button onClick={handleClick}>Click</Button>;
};
{
cell: ({ row }) => <ActionCell row={row} />,
}{
cell: ({ getValue }) => (
<button
onClick={handleClick}
aria-label={`Edit ${getValue()}`}
className="..."
>
<Pencil className="h-4 w-4" />
</button>
),
}