Skip to content
← Blocks

Record Detail Panel

A working record detail panel with illustrative data and frontend interactions.

Open ↗

Make it yours.

Install the editable source and its dependencies into your React + Tailwind project.

Scroll horizontally for long lines
pnpm dlx shadcn@4.0.8 add http://localhost:3000/r/record-detail-panel.json

Use it

Scroll horizontally for long lines
"use client";
import { RecordDetailPanel } from "@/components/jez-ui/blocks/record-detail-panel";

export default function Example() {
  return (
    <>
      <RecordDetailPanel />
    </>
  );
}

Compose your own

These styled parts are included in the same install. Use children to build your layout; add className only when you want an override.

Scroll horizontally for long lines
"use client";
import {
  RecordDetailPanel,
  RecordDetailPanelHeading,
  RecordDetailPanelDetails,
  RecordDetailPanelNote,
} from "@/components/jez-ui/blocks/record-detail-panel";

export default function Example() {
  return (
    <RecordDetailPanel>
      <RecordDetailPanelHeading />
      <RecordDetailPanelDetails />
      <RecordDetailPanelNote />
    </RecordDetailPanel>
  );
}
Scroll horizontally for long lines
import { RecordDetailPanel, RecordDetailPanelHeader, RecordDetailPanelTitle, RecordDetailPanelList, RecordDetailPanelDescription, RecordDetailPanelHeading, RecordDetailPanelDetails, RecordDetailPanelNote } from "@/components/jez-ui/blocks/record-detail-panel";
Read the composition guide →

Props & types

Scroll horizontally for long lines
export type RecordDetailPanelOptions = {
  name?: string;
  className?: string;
  items?: typeof RecordDetailPanelDefaultItems;
  description?: React.ReactNode;
};

export type RecordDetailPanelProps = Omit<
  React.ComponentProps<"section">,
  keyof RecordDetailPanelOptions
> &
  RecordDetailPanelOptions;

RecordDetailPanelProps

Source

View 3 source files

registry/blocks/record-detail-panel.tsx

Scroll horizontally for long lines
"use client";
import * as React from "react";
import { cn } from "../ui/utils";
import { Badge } from "../ui/badge";
export type RecordDetailPanelOptions = {
  name?: string;
  className?: string;
  items?: typeof RecordDetailPanelDefaultItems;
  description?: React.ReactNode;
};
export type RecordDetailPanelProps = Omit<
  React.ComponentProps<"section">,
  keyof RecordDetailPanelOptions
> &
  RecordDetailPanelOptions;
const RecordDetailPanelDefaultItems = [
  ["Email", "alex@example.com"],
  ["Plan", "Team"],
  ["Joined", "12 August 2026"],
  ["Projects", "4"],
  ["Last active", "Today"],
  ["Demo revenue", "£240"],
];
function useRecordDetailPanelModel({
  items = RecordDetailPanelDefaultItems,
  description = "Illustrative customer record.",
  name = "Alex Morgan",
  className,
  children,
  ...rootProps
}: RecordDetailPanelProps) {
  return { items, description, name, className, children, rootProps };
}
const RecordDetailPanelCompositionContext = React.createContext<ReturnType<
  typeof useRecordDetailPanelModel
> | null>(null);
function useRecordDetailPanelComposition() {
  const context = React.useContext(RecordDetailPanelCompositionContext);
  if (!context)
    throw new Error(
      "RecordDetailPanel parts must be inside RecordDetailPanel.",
    );
  return context;
}
export function RecordDetailPanel(props: RecordDetailPanelProps) {
  const model = useRecordDetailPanelModel(props);
  const { className, rootProps, children } = model;
  return (
    <RecordDetailPanelCompositionContext.Provider value={model}>
      <section
        {...rootProps}
        className={cn(
          "grid max-w-xl gap-6 rounded-xl border border-border p-6",
          className,
        )}
      >
        {children !== undefined ? (
          children
        ) : (
          <>
            <RecordDetailPanelHeading />
            <RecordDetailPanelDetails />
            <RecordDetailPanelNote />
          </>
        )}
      </section>
    </RecordDetailPanelCompositionContext.Provider>
  );
}

export function RecordDetailPanelHeader({
  className,
  ...props
}: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="record-detail-panel-header"
      className={cn("flex items-center justify-between gap-4", className)}
      {...props}
    />
  );
}
export function RecordDetailPanelTitle({
  className,
  ...props
}: React.ComponentProps<"h2">) {
  return (
    <h2
      data-slot="record-detail-panel-title"
      className={cn("text-lg font-semibold", className)}
      {...props}
    />
  );
}
export function RecordDetailPanelList({
  className,
  ...props
}: React.ComponentProps<"dl">) {
  return (
    <dl
      data-slot="record-detail-panel-list"
      className={cn(
        "grid grid-cols-[minmax(0,1fr)_minmax(0,1.3fr)] gap-y-5 border-t border-border pt-6 text-sm",
        className,
      )}
      {...props}
    />
  );
}
export function RecordDetailPanelDescription({
  className,
  ...props
}: React.ComponentProps<"p">) {
  return (
    <p
      data-slot="record-detail-panel-description"
      className={cn("text-xs text-muted-foreground", className)}
      {...props}
    />
  );
}

export function RecordDetailPanelHeading({
  children,
  ...props
}: Partial<React.ComponentProps<typeof RecordDetailPanelHeader>> & {
  children?: React.ReactNode;
}) {
  const { name } = useRecordDetailPanelComposition();
  return (
    <RecordDetailPanelHeader {...props}>
      {children === undefined ? (
        <>
          <div className="flex items-center gap-3">
            <span className="grid size-12 place-items-center rounded-full bg-primary/10 text-lg text-primary">
              {name
                .split(" ")
                .map((n) => n[0])
                .join("")}
            </span>
            <div>
              <RecordDetailPanelTitle>{name}</RecordDetailPanelTitle>
              <p className="mt-1 text-xs text-muted-foreground">
                Customer since August 2026
              </p>
            </div>
          </div>
          <Badge>Active</Badge>
        </>
      ) : (
        children
      )}
    </RecordDetailPanelHeader>
  );
}
export function RecordDetailPanelDetails({
  children,
  ...props
}: Partial<React.ComponentProps<typeof RecordDetailPanelList>> & {
  children?: React.ReactNode;
}) {
  const { items } = useRecordDetailPanelComposition();
  return (
    <RecordDetailPanelList {...props}>
      {children === undefined
        ? items.map(([k, v]) => (
            <React.Fragment key={k}>
              <dt className="text-muted-foreground">{k}</dt>
              <dd className="break-words font-medium">{v}</dd>
            </React.Fragment>
          ))
        : children}
    </RecordDetailPanelList>
  );
}
export function RecordDetailPanelNote({
  children,
  ...props
}: Partial<React.ComponentProps<typeof RecordDetailPanelDescription>> & {
  children?: React.ReactNode;
}) {
  const { description } = useRecordDetailPanelComposition();
  return (
    <RecordDetailPanelDescription {...props}>
      {children === undefined ? description : children}
    </RecordDetailPanelDescription>
  );
}

registry/ui/utils.ts

Scroll horizontally for long lines
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...values: ClassValue[]) {
  return twMerge(clsx(values));
}

registry/ui/badge.tsx

Scroll horizontally for long lines
"use client";
import * as React from "react";
import { cn } from "./utils";
export function Badge({
  className,
  tone = "neutral",
  ...props
}: React.ComponentProps<"span"> & {
  tone?: "neutral" | "accent" | "positive" | "warning";
}) {
  return (
    <span
      className={cn(
        "inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium",
        {
          "bg-muted text-foreground": tone === "neutral",
          "bg-primary/10 text-primary": tone === "accent",
          "bg-accent text-[#293620]": tone === "positive",
          "bg-[#f6e5c1] text-[#704517]": tone === "warning",
        },
        className,
      )}
      {...props}
    />
  );
}

Dependencies

clsx@2.1.1 · tailwind-merge@3.5.0

Accessibility & behaviour

Provide meaningful labels and preserve keyboard focus styles. Check contrast when customising theme colours.

Read the accessibility and performance guide →