shadimage
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://data-table.openstatus.dev/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 · light
lucide-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/8/2026, 2:51:41 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 };
}

More from data-table-filters

Browse all components in this library →