Data Table Filters
blockdata-table-cellsource: ok · refreshed
Data Table Cell Renderers
Cell renderer system with 12 types: text, code, badge, boolean, star, number, status-code, level-indicator, timestamp, heatmap, bar, and gauge.
npx shadcn add "https://logs.run/r/data-table-cell.json"free
Where it goes
statsdata-table
Visual style
terminal-mono
Interaction
animated-counter
Use cases
dashboard
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightlucide-react@^0.469.0date-fns@^4.1.0@date-fns/utc@^2.1.0sonner@^1.7.2
Registry dependencies
https://data-table.openstatus.dev/r/data-table.jsontooltiphover-card
Files18 · source cached 10/2/2026, 2:56:49 AM
src/components/data-table/data-table-cell/index.tsx
export { DataTableCellText } from "./data-table-cell-text";
export { DataTableCellCode } from "./data-table-cell-code";
export { DataTableCellNumber } from "./data-table-cell-number";
export { DataTableCellBar } from "./data-table-cell-bar";
export { DataTableCellHeatmap } from "./data-table-cell-heatmap";
export { DataTableCellGauge } from "./data-table-cell-gauge";
export { DataTableCellTimestamp } from "./data-table-cell-timestamp";
export { DataTableCellBadge } from "./data-table-cell-badge";
export { DataTableCellBoolean } from "./data-table-cell-boolean";
export { DataTableCellStar } from "./data-table-cell-star";
export { DataTableCellStatusCode } from "./data-table-cell-status-code";
export { DataTableCellLevelIndicator } from "./data-table-cell-level-indicator";
src/components/data-table/data-table-cell/data-table-cell-text.tsx
import { TextWithTooltip } from "@/components/custom/text-with-tooltip";
export function DataTableCellText({
value,
color,
}: {
value: string | number;
color?: string;
}) {
return <TextWithTooltip text={value} style={color ? { color } : undefined} />;
}
src/components/data-table/data-table-cell/data-table-cell-code.tsx
export function DataTableCellCode({
value,
color,
}: {
value: string | number;
color?: string;
}) {
return (
<span className="font-mono" style={color ? { color } : undefined}>
{value}
</span>
);
}
src/components/data-table/data-table-cell/data-table-cell-badge.tsx
import { boxRadiusClassName } from "@/lib/style";
import { cn } from "@/lib/utils";
export function DataTableCellBadge({
value,
color,
}: {
value: string | number;
color?: string;
}) {
return (
<span
className={cn(
"border px-1.5 py-0.5 font-mono text-xs",
boxRadiusClassName,
)}
style={
color
? {
color,
backgroundColor: `${color}1a`,
borderColor: `${color}33`,
}
: undefined
}
>
{value}
</span>
);
}
src/components/data-table/data-table-cell/data-table-cell-boolean.tsx
import { Check, Minus } from "lucide-react";
export function DataTableCellBoolean({
value,
color,
}: {
value: boolean;
color?: string;
}) {
if (value) {
return (
<Check
className={`ml-auto h-4 w-4 ${color ? "" : "text-foreground"}`}
style={color ? { color } : undefined}
/>
);
}
return (
<Minus
className={`ml-auto h-4 w-4 ${color ? "" : "text-muted-foreground/50"}`}
style={color ? { color } : undefined}
/>
);
}
src/components/data-table/data-table-cell/data-table-cell-star.tsx
import { Star } from "lucide-react";
export function DataTableCellStar({ value }: { value: boolean }) {
if (value) {
return (
<span className="ml-auto inline-flex items-center">
<Star
className="h-4 w-4 fill-yellow-400 text-yellow-400"
aria-hidden="true"
/>
<span className="sr-only">Favorited</span>
</span>
);
}
return (
<span className="ml-auto inline-flex items-center">
<Star className="text-muted-foreground/40 h-4 w-4" aria-hidden="true" />
<span className="sr-only">Not favorited</span>
</span>
);
}
src/components/data-table/data-table-cell/data-table-cell-number.tsx
export function DataTableCellNumber({
value,
unit,
color,
}: {
value: number;
unit?: string;
color?: string;
}) {
const formatted = new Intl.NumberFormat("en-US", {
maximumFractionDigits: 3,
}).format(value);
return (
<span className="font-mono" style={color ? { color } : undefined}>
{formatted}
{unit && <span className="text-muted-foreground">{unit}</span>}
</span>
);
}
src/components/data-table/data-table-cell/data-table-cell-status-code.tsx
import { getStatusColor } from "@/lib/request/status-code";
export function DataTableCellStatusCode({
value,
color,
}: {
value: number;
color?: string;
}) {
const colors = getStatusColor(value);
return (
<span
className={`font-mono ${color ? "" : colors.text}`}
style={color ? { color } : undefined}
>
{value}
</span>
);
}
src/components/data-table/data-table-cell/data-table-cell-level-indicator.tsx
import { cn } from "@/lib/utils";
/**
* Severity → the core block's semantic colour tokens (`--error`, `--warning`,
* `--info`, `--success` in globals.css). The timeline chart reads the same
* variables for its series, so a level's dot and its bar always match.
*/
const LEVEL_COLORS: Record<string, string> = {
error: "bg-error",
warn: "bg-warning",
warning: "bg-warning",
info: "bg-info",
debug: "bg-muted-foreground",
success: "bg-success",
};
/**
* A colored square keyed by severity.
*
* The label is off by default: in a narrow table column the dot is the whole
* message. The row sheet turns it on, where "info" next to the dot reads as
* a value rather than noise.
*/
export function DataTableCellLevelIndicator({
value,
color: colorOverride,
label = value,
showLabel = false,
dotPosition = "start",
alignLabels,
}: {
value: string;
color?: string;
/** Text shown next to the dot. Defaults to the raw value. */
label?: string;
showLabel?: boolean;
/**
* Which side of the label the dot sits on. The filter sidebar puts it at the
* end: a mark right after the checkbox reads as a second checkbox.
*/
dotPosition?: "start" | "end";
/**
* The labels of the sibling options. With the dot at the end, the label
* takes the width of the widest of them so the dots line up. They are laid
* out invisibly rather than counted in `ch`: only the browser knows how wide
* "WARNING" or a CJK label is in the current font.
*/
alignLabels?: string[];
}) {
const builtinColor = LEVEL_COLORS[value.toLowerCase()] ?? "bg-muted";
const dot = (
<span
className={cn(
"inline-block size-3 shrink-0 rounded-sm",
!colorOverride && builtinColor,
)}
style={colorOverride ? { backgroundColor: colorOverride } : undefined}
/>
);
// Dot only: centred in the (narrow) cell, named for screen readers via
// `aria-label` rather than an `sr-only` span — that span is absolutely
// positioned and would escape the table's scroll container, stretching the
// page by one row height per row.
if (!showLabel) {
return (
<span className="flex items-center justify-center">
<span role="img" aria-label={label}>
{dot}
</span>
</span>
);
}
return (
<span
className="inline-flex items-center gap-1.5"
style={colorOverride ? { color: colorOverride } : undefined}
>
{dotPosition === "start" ? dot : null}
{alignLabels?.length ? (
// Every label shares one grid cell, so the cell is as wide as the
// widest. The sizers draw their text from an attribute: it stays out
// of the DOM text, the clipboard and the accessibility tree.
<span className="inline-grid font-normal">
{alignLabels.map((sizer) => (
<span
key={sizer}
aria-hidden
data-label={sizer}
className="invisible col-start-1 row-start-1 whitespace-nowrap before:content-[attr(data-label)]"
/>
))}
<span className="col-start-1 row-start-1 truncate">{label}</span>
</span>
) : (
<span className="truncate font-normal">{label}</span>
)}
{dotPosition === "end" ? dot : null}
</span>
);
}
src/components/data-table/data-table-cell/data-table-cell-timestamp.tsx
"use client";
import { HoverCardTimestamp } from "@/components/data-table/data-table-infinite/hover-card-timestamp";
export function DataTableCellTimestamp({
date,
color,
}: {
date: Date | string | number;
color?: string;
}) {
const d = date instanceof Date ? date : new Date(date);
return (
<span style={color ? { color } : undefined}>
<HoverCardTimestamp date={d} />
</span>
);
}
src/components/data-table/data-table-cell/data-table-cell-heatmap.tsx
import { hexToRgb } from "@/lib/colors";
const OPACITY = 0.1;
export function DataTableCellHeatmap({
value,
min,
max,
unit,
color,
}: {
value: number;
min: number;
max: number;
unit?: string;
color?: string;
}) {
const range = max - min;
const ratio =
range === 0 ? 0 : Math.max(0, Math.min(1, (value - min) / range));
const opacity = ratio * OPACITY;
const formatted = new Intl.NumberFormat("en-US", {
maximumFractionDigits: 3,
}).format(value);
return (
<div className="relative -m-2 p-2">
<div
className="absolute inset-0"
style={{
backgroundColor: color
? `rgba(${hexToRgb(color)}, ${opacity})`
: `var(--muted-foreground)`,
opacity: color ? undefined : opacity,
}}
/>
<span className="relative font-mono">
{formatted}
{unit && <span className="text-muted-foreground">{unit}</span>}
</span>
</div>
);
}
src/components/data-table/data-table-cell/data-table-cell-bar.tsx
import { hexToRgb } from "@/lib/colors";
const OPACITY = 0.5;
const BORDER_OPACITY = 1;
export function DataTableCellBar({
value,
min,
max,
unit,
color,
}: {
value: number;
min: number;
max: number;
unit?: string;
color?: string;
}) {
const range = max - min;
const ratio =
range === 0 ? 0 : Math.max(0, Math.min(1, (value - min) / range));
const formatted = new Intl.NumberFormat("en-US", {
maximumFractionDigits: 3,
}).format(value);
return (
<div className="relative -m-2 p-2">
<div
className="absolute inset-y-0 left-0"
style={{
width: `${ratio * 100}%`,
backgroundColor: color
? `rgba(${hexToRgb(color)}, ${OPACITY})`
: `var(--muted)`,
opacity: color ? undefined : OPACITY,
borderRight: `1px solid ${color ? `rgba(${hexToRgb(color)}, ${BORDER_OPACITY})` : `var(--border)`}`,
}}
/>
<span className="relative font-mono">
{formatted}
{unit && <span className="text-muted-foreground">{unit}</span>}
</span>
</div>
);
}
src/components/data-table/data-table-cell/data-table-cell-gauge.tsx
import { hexToRgb } from "@/lib/colors";
const BG_OPACITY = 0.2;
const FOREGROUND_OPACITY = 0.5;
export function DataTableCellGauge({
value,
min,
max,
unit,
color,
}: {
value: number;
min: number;
max: number;
unit?: string;
color?: string;
}) {
const range = max - min;
const ratio =
range === 0 ? 0 : Math.max(0, Math.min(1, (value - min) / range));
// SVG circle gauge calculations
const size = 20;
const strokeWidth = 4;
const viewBox = 120;
const radius = 50;
const circumference = 2 * Math.PI * radius;
const strokeDashoffset = circumference - ratio * circumference;
const bgStroke = color
? `rgba(${hexToRgb(color)}, ${BG_OPACITY})`
: `var(--muted)`;
const fgStroke = color
? `rgba(${hexToRgb(color)}, ${FOREGROUND_OPACITY})`
: `var(--muted-foreground)`;
const formatted = new Intl.NumberFormat("en-US", {
maximumFractionDigits: 3,
}).format(value);
return (
<div className="flex items-center gap-1.5">
<svg
fill="none"
height={size}
width={size}
viewBox={`0 0 ${viewBox} ${viewBox}`}
className="shrink-0 -rotate-90"
>
<circle
strokeWidth={strokeWidth * 4}
stroke={bgStroke}
fill="transparent"
r={radius}
cx={viewBox / 2}
cy={viewBox / 2}
/>
<circle
strokeWidth={strokeWidth * 4}
strokeDasharray={`${circumference} ${circumference}`}
strokeDashoffset={strokeDashoffset}
strokeLinecap="round"
stroke={fgStroke}
fill="transparent"
r={radius}
cx={viewBox / 2}
cy={viewBox / 2}
/>
</svg>
<span className="font-mono">
{formatted}
{unit && <span className="text-muted-foreground">{unit}</span>}
</span>
</div>
);
}
src/lib/colors.ts
export function hexToRgb(hex: string): string {
const normalizedHex = hex.trim().replace(/^#/, "");
let rgbHex: string;
if (/^[0-9a-fA-F]{3}$/.test(normalizedHex)) {
rgbHex = normalizedHex
.split("")
.map((char) => char + char)
.join("");
} else if (/^[0-9a-fA-F]{6}$/.test(normalizedHex)) {
rgbHex = normalizedHex;
} else if (/^[0-9a-fA-F]{8}$/.test(normalizedHex)) {
rgbHex = normalizedHex.substring(0, 6);
} else {
return "0, 0, 0";
}
const r = parseInt(rgbHex.substring(0, 2), 16);
const g = parseInt(rgbHex.substring(2, 4), 16);
const b = parseInt(rgbHex.substring(4, 6), 16);
if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) {
return "0, 0, 0";
}
return `${r}, ${g}, ${b}`;
}
src/lib/request/status-code.ts
export function getStatusColor(
value: number,
): Record<"text" | "bg" | "border", string> {
if (value < 100 || value >= 600)
return {
text: "text-gray-500",
bg: "bg-gray-100 dark:bg-gray-900/50",
border: "border-gray-200 dark:border-gray-800",
};
switch (value.toString().charAt(0)) {
case "1":
return {
text: "text-blue-500",
bg: "bg-blue-100 dark:bg-blue-900/50",
border: "border-blue-200 dark:border-blue-800",
};
case "2":
return {
text: "text-green-500",
bg: "bg-green-100 dark:bg-green-900/50",
border: "border-green-200 dark:border-green-800",
};
case "3":
return {
text: "text-yellow-500",
bg: "bg-yellow-100 dark:bg-yellow-900/50",
border: "border-yellow-200 dark:border-yellow-800",
};
case "4":
return {
text: "text-orange-500",
bg: "bg-orange-100 dark:bg-orange-900/50",
border: "border-orange-200 dark:border-orange-800",
};
case "5":
return {
text: "text-red-500",
bg: "bg-red-100 dark:bg-red-900/50",
border: "border-red-200 dark:border-red-800",
};
default:
return {
text: "text-gray-500",
bg: "bg-gray-100 dark:bg-gray-900/50",
border: "border-gray-200 dark:border-gray-800",
};
}
}
src/components/custom/text-with-tooltip.tsx
import {
TOOLTIP_DELAY,
TOOLTIP_NOT_HOVERABLE,
} from "@/components/data-table/ui-compat";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import React, { useEffect, useRef, useState } from "react";
interface TextWithTooltipProps {
text: string | number;
className?: string;
style?: React.CSSProperties;
}
export function TextWithTooltip({
text,
className,
style,
}: TextWithTooltipProps) {
const [isTruncated, setIsTruncated] = useState<boolean>(false);
const textRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const checkTruncation = () => {
if (textRef.current) {
const { scrollWidth, clientWidth } = textRef.current;
setIsTruncated(scrollWidth > clientWidth);
}
};
const resizeObserver = new ResizeObserver(() => {
checkTruncation();
});
if (textRef.current) {
resizeObserver.observe(textRef.current);
}
checkTruncation();
return () => {
resizeObserver.disconnect();
};
}, []);
return (
<TooltipProvider {...TOOLTIP_DELAY}>
<Tooltip {...TOOLTIP_NOT_HOVERABLE}>
<TooltipTrigger disabled={!isTruncated} asChild>
<div
ref={textRef}
className={cn(
"truncate",
!isTruncated && "pointer-events-none",
className,
)}
style={style}
>
{text}
</div>
</TooltipTrigger>
<TooltipContent>{text}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
src/components/data-table/data-table-infinite/hover-card-timestamp.tsx
"use client";
import { UTCDate } from "@date-fns/utc";
import { HOVER_CARD_DELAY } from "@/components/data-table/ui-compat";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard";
import { cn } from "@/lib/utils";
import { format, formatDistanceToNowStrict } from "date-fns";
import { Check, Copy } from "lucide-react";
import type { ComponentPropsWithoutRef } from "react";
type HoverCardContentProps = ComponentPropsWithoutRef<typeof HoverCardContent>;
interface HoverCardTimestampProps {
date: Date;
side?: HoverCardContentProps["side"];
sideOffset?: HoverCardContentProps["sideOffset"];
align?: HoverCardContentProps["align"];
alignOffset?: HoverCardContentProps["alignOffset"];
className?: string;
}
export function HoverCardTimestamp({
date,
side = "right",
align = "start",
alignOffset = -4,
sideOffset,
className,
}: HoverCardTimestampProps) {
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
return (
<HoverCard {...HOVER_CARD_DELAY}>
<HoverCardTrigger asChild>
<div className={cn("font-mono whitespace-nowrap", className)}>
{format(date, "LLL dd, y HH:mm:ss")}
</div>
</HoverCardTrigger>
{/* Portal is built into HoverCardContent in shadcn v4 */}
<HoverCardContent
className="z-10 w-auto p-2"
{...{ side, align, alignOffset, sideOffset }}
>
<dl className="flex flex-col gap-1">
<Row value={String(date.getTime())} label="Timestamp" />
<Row
value={format(new UTCDate(date), "LLL dd, y HH:mm:ss")}
label="UTC"
/>
<Row value={format(date, "LLL dd, y HH:mm:ss")} label={timezone} />
<Row
value={formatDistanceToNowStrict(date, { addSuffix: true })}
label="Relative"
/>
</dl>
</HoverCardContent>
</HoverCard>
);
}
function Row({ value, label }: { value: string; label: string }) {
const { copy, isCopied } = useCopyToClipboard();
return (
<div
className="group flex items-center justify-between gap-4 text-sm"
onClick={(e) => {
e.stopPropagation();
copy(value);
}}
>
<dt className="text-muted-foreground">{label}</dt>
<dd className="flex items-center gap-1 truncate font-mono">
<span className="invisible group-hover:visible">
{!isCopied ? (
<Copy className="h-3 w-3" />
) : (
<Check className="h-3 w-3" />
)}
</span>
{value}
</dd>
</div>
);
}
src/hooks/use-copy-to-clipboard.ts
"use client";
import { useCallback, useState } from "react";
import { toast } from "sonner";
export function useCopyToClipboard() {
const [text, setText] = useState<string | null>(null);
const copy = useCallback(
async (
text: string,
{ timeout, withToast }: { timeout?: number; withToast?: boolean } = {
timeout: 3000,
withToast: false,
},
) => {
if (!navigator?.clipboard) {
console.warn("Clipboard not supported");
return false;
}
try {
await navigator.clipboard.writeText(text);
setText(text);
if (timeout) {
setTimeout(() => {
setText(null);
}, timeout);
}
if (withToast) {
toast.success("Copied to clipboard");
}
return true;
} catch (error) {
console.warn("Copy failed", error);
setText(null);
return false;
}
},
[],
);
return { text, copy, isCopied: text !== null };
}