shadimage
bjork-ui
componentprimitive-data-tablesource: ok · hit

Data Table

Data Table is a production-ready ui component from BJORK UI, packaged for shadcn registries.

npx shadcn add "https://ui.isaiahbjork.com/primitive-data-table.json"
free

Where it goes

data-table

Use cases

dashboard

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
animationframer-motion
iconslucide-react

Files8 · source cached 10/8/2026, 10:24:53 PM

components/bjork-ui/primitives/data-table.tsx
"use client";

import { Search, SlidersHorizontal } from "lucide-react";
import { BjorkButton } from "./button";
import { BjorkInput } from "./input";
import { BjorkTable } from "./table";

export function BjorkDataTable() {
  return (
    <div className="w-[min(620px,100%)] space-y-3">
      <div className="flex items-center justify-between gap-3">
        <BjorkInput
          icon={<Search />}
          placeholder="Filter accounts"
          className="h-10 max-w-[300px]"
        />
        <BjorkButton variant="quiet" size="sm">
          <SlidersHorizontal aria-hidden="true" />
          View
        </BjorkButton>
      </div>
      <BjorkTable />
    </div>
  );
}
components/bjork-ui/primitives/button.tsx
"use client";

import * as React from "react";
import { motion, useReducedMotion, type HTMLMotionProps } from "framer-motion";
import { cva, type VariantProps } from "class-variance-authority";
import { useWebHaptics } from "web-haptics/react";
import type { HapticInput, TriggerOptions } from "web-haptics";
import { cn } from "@/lib/utils";

const bjorkButtonVariants = cva(
  "inline-flex shrink-0 transform-gpu items-center justify-center gap-2 whitespace-nowrap rounded-[13px] text-sm font-medium tracking-[-0.01em] outline-none backdrop-blur-md transition-[background-color,border-color,color,box-shadow,opacity] duration-150 ease-out focus-visible:ring-2 focus-visible:ring-[color:var(--bjork-accent)]/45 focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bjork-ring-offset)] disabled:pointer-events-none disabled:opacity-35 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
  {
    variants: {
      variant: {
        default:
          "border border-transparent bg-[var(--bjork-field)] text-[color:var(--bjork-text)] shadow-[var(--bjork-shadow-surface)] hover:bg-[var(--bjork-surface-hover)]",
        secondary:
          "border border-[color:var(--bjork-border)] bg-[var(--bjork-surface-active)] text-[color:var(--bjork-text)] shadow-[var(--bjork-shadow-soft)] hover:bg-[var(--bjork-surface-hover)]",
        outline:
          "border border-[color:var(--bjork-border)] bg-[var(--bjork-field-inset)] text-[color:var(--bjork-text-strong)] shadow-[var(--bjork-shadow-soft)] hover:border-[color:var(--bjork-border-strong)] hover:bg-[var(--bjork-surface)] hover:text-[color:var(--bjork-text)]",
        ghost:
          "border border-transparent bg-transparent text-[color:var(--bjork-text-muted)] hover:bg-[var(--bjork-surface-hover)] hover:text-[color:var(--bjork-text)]",
        quiet:
          "border border-transparent bg-[var(--bjork-field-muted)] text-[color:var(--bjork-text-soft)] hover:bg-[var(--bjork-surface-hover)] hover:text-[color:var(--bjork-text-medium)]",
        accent:
          "bjork-layered-button-accent tracking-tight text-white",
        raised:
          "bjork-layered-button-raised tracking-tight",
      },
      size: {
        sm: "h-8 px-3 text-xs has-[>svg:first-child]:pl-2.5 has-[>svg:last-child]:pr-2.5",
        md: "h-[45px] px-4 has-[>svg:first-child]:pl-3.5 has-[>svg:last-child]:pr-3.5",
        lg: "h-12 px-5 text-[15px] has-[>svg:first-child]:pl-4.5 has-[>svg:last-child]:pr-4.5",
        icon: "size-[45px] p-0",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "md",
    },
  },
);

export interface BjorkButtonProps
  extends Omit<HTMLMotionProps<"button">, "ref" | "size">,
    VariantProps<typeof bjorkButtonVariants> {
  haptics?: boolean;
  hapticPattern?: HapticInput;
  hapticOptions?: TriggerOptions;
}

export function BjorkButton({
  className,
  variant,
  size,
  disabled,
  haptics = true,
  hapticPattern = "light",
  hapticOptions,
  onPointerDown,
  tabIndex,
  ...props
}: BjorkButtonProps) {
  const shouldReduceMotion = useReducedMotion();
  const { trigger } = useWebHaptics();

  const handlePointerDown = React.useCallback(
    (event: React.PointerEvent<HTMLButtonElement>) => {
      if (!disabled && haptics) {
        void trigger(hapticPattern, hapticOptions);
      }

      onPointerDown?.(event);
    },
    [disabled, hapticOptions, hapticPattern, haptics, onPointerDown, trigger],
  );

  return (
    <motion.button
      data-slot="bjork-button"
      className={cn(bjorkButtonVariants({ variant, size, className }))}
      disabled={disabled}
      onPointerDown={handlePointerDown}
      whileHover={disabled || shouldReduceMotion ? undefined : { scale: 1.05 }}
      whileTap={disabled || shouldReduceMotion ? undefined : { scale: 0.95 }}
      transition={{ type: "spring", stiffness: 400, damping: 25 }}
      tabIndex={tabIndex ?? 0}
      {...props}
    />
  );
}

export { bjorkButtonVariants };
lib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}
components/bjork-ui/primitives/input.tsx
"use client";

import * as React from "react";
import { motion, useReducedMotion, type HTMLMotionProps } from "framer-motion";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";

const bjorkInputVariants = cva(
  "flex w-full min-w-0 rounded-[13px] border-0 bg-[var(--bjork-field)] text-[color:var(--bjork-text)] outline-none backdrop-blur-md transition-[background-color,border-color,box-shadow,opacity,transform] duration-200 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-[color:var(--bjork-text-soft)] selection:bg-[#ec5c13]/30 selection:text-[color:var(--bjork-text)] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-35 focus-visible:ring-2 focus-visible:ring-[color:var(--bjork-border-strong)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bjork-ring-offset)]",
  {
    variants: {
      variant: {
        default:
          "shadow-[var(--bjork-shadow-surface)] hover:bg-[var(--bjork-surface-hover)]",
        quiet:
          "bg-[var(--bjork-field-muted)] text-[color:var(--bjork-text-strong)] shadow-[var(--bjork-shadow-soft)] hover:bg-[var(--bjork-surface-hover)]",
        inset:
          "border border-[color:var(--bjork-border)] bg-[var(--bjork-field-inset)] shadow-[var(--bjork-shadow-inset)] hover:bg-[var(--bjork-panel)]",
        ghost:
          "bg-transparent shadow-none hover:bg-transparent",
        error:
          "border border-[#ec5c13]/42 bg-[var(--bjork-error-bg)] shadow-[inset_0_7px_14px_rgba(236,92,19,0.035),0_8px_14px_-13px_rgba(236,92,19,0.28)] focus-visible:ring-[color:var(--bjork-border-strong)]",
      },
      inputSize: {
        sm: "h-9 px-3 text-xs",
        md: "h-[45px] px-4 text-sm",
        lg: "h-12 px-4 text-[15px]",
      },
    },
    defaultVariants: {
      variant: "default",
      inputSize: "md",
    },
  },
);

export interface BjorkInputProps
  extends Omit<HTMLMotionProps<"input">, "ref" | "size">,
    VariantProps<typeof bjorkInputVariants> {
  icon?: React.ReactNode;
}

export function BjorkInput({
  className,
  icon,
  variant,
  inputSize,
  type = "text",
  ...props
}: BjorkInputProps) {
  const shouldReduceMotion = useReducedMotion();

  const input = (
    <motion.input
      data-slot="bjork-input"
      type={type}
      className={cn(
        bjorkInputVariants({
          variant,
          inputSize,
          className: cn(icon && "pl-9", className),
        })
      )}
      whileFocus={shouldReduceMotion ? undefined : { y: -1 }}
      transition={{ type: "spring", stiffness: 360, damping: 30 }}
      {...props}
    />
  );

  if (!icon) return input;

  return (
    <div className="relative w-full min-w-0">
      <span className="pointer-events-none absolute left-3 top-1/2 z-10 -translate-y-1/2 text-[color:var(--bjork-text-soft)] [&>svg]:size-4">
        {icon}
      </span>
      {input}
    </div>
  );
}

export { bjorkInputVariants };
components/bjork-ui/primitives/table.tsx
"use client";

import * as React from "react";
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";
import { cn } from "@/lib/utils";
import { BjorkBadge } from "./badge";
import { bjorkCardVariants } from "./card";

const defaultRows = [
  { account: "Northwind", status: "Active", value: "$12,480" },
  { account: "Acme", status: "Queued", value: "$8,140" },
  { account: "Linear", status: "Review", value: "$6,920" },
];

export function BjorkTable({
  rows = defaultRows,
}: {
  rows?: typeof defaultRows;
}) {
  return (
    <div
      data-slot="bjork-table"
      className={cn(
        "w-[min(620px,100%)] overflow-hidden",
        bjorkCardVariants({ variant: "surface", padding: "none" }),
      )}
    >
      <Table>
        <TableHeader className="bg-[var(--bjork-field-muted)] shadow-[var(--bjork-shadow-soft)] dark:bg-[var(--bjork-field-inset)] dark:shadow-[var(--bjork-shadow-inset)]">
          <TableRow className="border-[color:var(--bjork-border-muted)] hover:bg-transparent">
            <TableHead className="h-10 px-4 text-xs font-medium uppercase tracking-[0.08em] text-[color:var(--bjork-text-soft)]">
              Account
            </TableHead>
            <TableHead className="h-10 px-4 text-xs font-medium uppercase tracking-[0.08em] text-[color:var(--bjork-text-soft)]">
              Status
            </TableHead>
            <TableHead className="h-10 px-4 text-right text-xs font-medium uppercase tracking-[0.08em] text-[color:var(--bjork-text-soft)]">
              Value
            </TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {rows.map((row) => (
            <TableRow
              key={row.account}
              className="border-[color:var(--bjork-border-muted)] transition-colors hover:bg-[var(--bjork-surface-hover)]"
            >
              <TableCell className="px-4 py-3 text-[color:var(--bjork-text-medium)]">{row.account}</TableCell>
              <TableCell className="px-4 py-3">
                <BjorkBadge variant={row.status === "Active" ? "accent" : "muted"}>
                  {row.status}
                </BjorkBadge>
              </TableCell>
              <TableCell className="px-4 py-3 text-right font-mono text-[color:var(--bjork-text-muted)]">
                {row.value}
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </div>
  );
}
components/ui/table.tsx
"use client"

import * as React from "react"

import { cn } from "@/lib/utils"

function Table({ className, ...props }: React.ComponentProps<"table">) {
  return (
    <div
      data-slot="table-container"
      className="relative w-full overflow-x-auto"
    >
      <table
        data-slot="table"
        className={cn("w-full caption-bottom text-sm", className)}
        {...props}
      />
    </div>
  )
}

function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
  return (
    <thead
      data-slot="table-header"
      className={cn("[&_tr]:border-b", className)}
      {...props}
    />
  )
}

function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
  return (
    <tbody
      data-slot="table-body"
      className={cn("[&_tr:last-child]:border-0", className)}
      {...props}
    />
  )
}

function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
  return (
    <tfoot
      data-slot="table-footer"
      className={cn(
        "bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
        className
      )}
      {...props}
    />
  )
}

function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
  return (
    <tr
      data-slot="table-row"
      className={cn(
        "hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
        className
      )}
      {...props}
    />
  )
}

function TableHead({ className, ...props }: React.ComponentProps<"th">) {
  return (
    <th
      data-slot="table-head"
      className={cn(
        "text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
        className
      )}
      {...props}
    />
  )
}

function TableCell({ className, ...props }: React.ComponentProps<"td">) {
  return (
    <td
      data-slot="table-cell"
      className={cn(
        "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
        className
      )}
      {...props}
    />
  )
}

function TableCaption({
  className,
  ...props
}: React.ComponentProps<"caption">) {
  return (
    <caption
      data-slot="table-caption"
      className={cn("text-muted-foreground mt-4 text-sm", className)}
      {...props}
    />
  )
}

export {
  Table,
  TableHeader,
  TableBody,
  TableFooter,
  TableHead,
  TableRow,
  TableCell,
  TableCaption,
}
components/bjork-ui/primitives/badge.tsx
"use client";

import * as React from "react";
import { motion, useReducedMotion, type HTMLMotionProps } from "framer-motion";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";

const bjorkBadgeVariants = cva(
  "inline-flex w-fit shrink-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-[9px] border font-medium outline-none transition-[background-color,border-color,color,box-shadow,opacity,transform] duration-200 focus-visible:ring-2 focus-visible:ring-[color:var(--bjork-accent-muted)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bjork-ring-offset)] [&_svg]:size-3",
  {
    variants: {
      variant: {
        default:
          "border-transparent bg-[var(--bjork-text)] text-[color:var(--bjork-inverted-text)] shadow-[var(--bjork-shadow-soft)]",
        secondary:
          "border-transparent bg-[var(--bjork-surface-active)] text-[color:var(--bjork-text)] shadow-[var(--bjork-shadow-soft)]",
        outline:
          "border-[color:var(--bjork-border)] bg-[var(--bjork-field-inset)] text-[color:var(--bjork-text-medium)]",
        muted:
          "border-transparent bg-[var(--bjork-field-muted)] text-[color:var(--bjork-text-soft)]",
        accent:
          "border-[color:var(--bjork-accent-muted)] bg-[var(--bjork-accent-badge)] text-[color:var(--bjork-accent-badge-foreground)] shadow-[var(--bjork-shadow-soft)]",
      },
      size: {
        sm: "px-1.5 py-0.5 text-[11px] has-[>svg:first-child]:pl-1 has-[>svg:last-child]:pr-1",
        md: "px-2 py-1 text-xs has-[>svg:first-child]:pl-1.5 has-[>svg:last-child]:pr-1.5",
        lg: "px-2.5 py-1.5 text-sm has-[>svg:first-child]:pl-2 has-[>svg:last-child]:pr-2",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "md",
    },
  },
);

export interface BjorkBadgeProps
  extends Omit<HTMLMotionProps<"span">, "ref">,
    VariantProps<typeof bjorkBadgeVariants> {}

export function BjorkBadge({
  className,
  variant,
  size,
  ...props
}: BjorkBadgeProps) {
  const shouldReduceMotion = useReducedMotion();

  return (
    <motion.span
      data-slot="bjork-badge"
      className={cn(bjorkBadgeVariants({ variant, size, className }))}
      whileHover={shouldReduceMotion ? undefined : { y: -1, opacity: 0.92 }}
      transition={{ type: "spring", stiffness: 420, damping: 30 }}
      {...props}
    />
  );
}

export { bjorkBadgeVariants };
components/bjork-ui/primitives/card.tsx
"use client";

import * as React from "react";
import { motion, useReducedMotion, type HTMLMotionProps } from "framer-motion";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";

const bjorkCardVariants = cva(
  "relative flex flex-col overflow-hidden rounded-[20px] border text-[color:var(--bjork-text)] outline-none transition-[background-color,border-color,box-shadow,opacity,transform] duration-300 focus-visible:ring-2 focus-visible:ring-[#ec5c13]/45 focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bjork-ring-offset)]",
  {
    variants: {
      variant: {
        surface:
          "border-[color:var(--bjork-border-muted)] bg-[var(--bjork-surface)] shadow-[var(--bjork-shadow-surface)]",
        elevated:
          "border-[color:var(--bjork-border-muted)] bg-[var(--bjork-surface)] shadow-[var(--bjork-shadow-surface)]",
        quiet:
          "border-[color:var(--bjork-border-muted)] bg-[var(--bjork-panel)] shadow-[var(--bjork-shadow-soft)]",
        interactive:
          "cursor-pointer border-[color:var(--bjork-border-muted)] bg-[var(--bjork-surface)] shadow-[var(--bjork-shadow-surface)] hover:border-[color:var(--bjork-border)] hover:bg-[var(--bjork-surface-hover)]",
      },
      padding: {
        none: "p-0",
        sm: "p-3",
        md: "p-4",
        lg: "p-5",
      },
    },
    defaultVariants: {
      variant: "surface",
      padding: "md",
    },
  },
);

export interface BjorkCardProps
  extends Omit<HTMLMotionProps<"div">, "ref">,
    VariantProps<typeof bjorkCardVariants> {}

export function BjorkCard({
  className,
  variant,
  padding,
  ...props
}: BjorkCardProps) {
  const shouldReduceMotion = useReducedMotion();
  const isInteractive = variant === "interactive" || props.role === "button";

  return (
    <motion.div
      data-slot="bjork-card"
      className={cn(bjorkCardVariants({ variant, padding, className }))}
      whileHover={isInteractive && !shouldReduceMotion ? { y: -2, scale: 1.005 } : undefined}
      whileTap={isInteractive && !shouldReduceMotion ? { scale: 0.995 } : undefined}
      transition={{ type: "spring", stiffness: 260, damping: 28, mass: 0.8 }}
      {...props}
    />
  );
}

export function BjorkCardHeader({
  className,
  ...props
}: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="bjork-card-header"
      className={cn("flex items-start justify-between gap-4", className)}
      {...props}
    />
  );
}

export function BjorkCardTitle({
  className,
  ...props
}: React.ComponentProps<"h3">) {
  return (
    <h3
      data-slot="bjork-card-title"
      className={cn("text-base font-semibold leading-none tracking-[-0.02em] text-[color:var(--bjork-text)]", className)}
      {...props}
    />
  );
}

export function BjorkCardDescription({
  className,
  ...props
}: React.ComponentProps<"p">) {
  return (
    <p
      data-slot="bjork-card-description"
      className={cn("text-sm leading-6 text-[color:var(--bjork-text-soft)]", className)}
      {...props}
    />
  );
}

export function BjorkCardContent({
  className,
  ...props
}: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="bjork-card-content"
      className={cn("mt-5 text-[color:var(--bjork-text-medium)]", className)}
      {...props}
    />
  );
}

export function BjorkCardFooter({
  className,
  ...props
}: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="bjork-card-footer"
      className={cn("mt-5 flex items-center gap-3", className)}
      {...props}
    />
  );
}

export { bjorkCardVariants };

More from bjork-ui

Browse all components in this library →