Skip to content
Product block

Record Detail Page

The read view arranged by what a reader came for - a verdict band of four facts, then details, then the related records a field dump can never show.

314 lineslucide-reactAdded 13 Sept 2026
  • crud
  • read
  • detail
  • record
  • admin

What's included

  • components/crud-read-page.tsx
  • Needs lucide-react

Works with

  • React
  • Next.js
  • Tailwind CSS
  • TypeScript

npx hoverlab add crud-read-page

Or over MCP, from your editor's agent — no account needed.

License

Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.

Was this useful?

Start a page with this section — add more, order them, and leave with the page source.

Preview

Northwind Trading Ltd

Approved

Supplier · Tier 2 · Active since January 2026

Contract value
£248,000
Annual, indexed to CPI
Owner
Rhea Patel
Procurement
Renews
1 April 2027
90-day notice period
Risk tier
Tier 2
Reviewed 14 Aug 2026

Details

Supplier id
SUP-0417
Registered name
Northwind Trading Ltd
Company number
09182736
Registered office
Rotterdam, Netherlands
Payment terms
Net 30
Currency
EUR
Insurance expiry
30 November 2026
Data processing agreement
Signed 3 March 2026

What changed

  1. Risk tier moved from 3 to 2

    Sam Okafor · 14 Aug 2026

  2. Payment terms changed to Net 30

    Rhea Patel · 2 Jul 2026

  3. Record created

    Import · finance-2026 · 11 Jan 2026

The annual review is raised automatically 60 days before the renewal date. Nothing on this page triggers it.

Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.

Source

components/crud-read-page.tsx
/**
 * <CrudReadPage> — Read, as a page: the record everything else links to.
 *
 * The R in CRUD is the one that gets built by accident. A create form is
 * obviously a form and a delete is obviously a dialog, but "show the record"
 * turns into a two-column dump of every column in the table, in schema order,
 * and stays that way for years.
 *
 * WHAT THIS ARRANGES INSTEAD
 *
 *  - **A verdict line, not a field list, at the top.** The four facts a
 *    reader came for — status, value, owner, next date — are a band above the
 *    fold. Everything else is below it. Schema order is the database's
 *    priority, not the reader's.
 *  - **Related records are first-class.** "What else is attached to this" is
 *    the question a detail page exists to answer and the one a field dump
 *    cannot. The related rail is a peer of the details, not a tab behind
 *    them.
 *  - **The trail is on the page.** Not in an audit-log screen somewhere else.
 *    "Why is this record like this" is answered by what changed and who
 *    changed it, and the answer is three items long.
 *
 * SERVER COMPONENT. There is no state here — no tabs, no disclosure, no
 * filter. A read page that hydrates is a read page paying for JavaScript it
 * never uses; the interactive cousins of this block are the drawer and the
 * modal, which have a reason.
 *
 * ACCESSIBILITY: one `<h1>`, sections named by their own headings, the fact
 * band as a `<dl>` so each label is programmatically tied to its value, and
 * the status carries a text label rather than colour alone. The action row
 * puts the destructive item last and outside the primary group.
 */

import * as React from 'react'
import {
  Building2,
  CalendarClock,
  ExternalLink,
  Pencil,
  ShieldCheck,
  Trash2,
  User,
} from 'lucide-react'

export interface ReadFact {
  label: string
  value: string
  note?: string
}

export interface ReadDetail {
  label: string
  value: string
  mono?: boolean
}

export interface ReadRelated {
  id: string
  kind: string
  title: string
  meta: string
}

export interface ReadEvent {
  id: string
  what: string
  who: string
  when: string
}

export interface CrudReadPageProps {
  title?: string
  subtitle?: string
  status?: { label: string; tone: 'good' | 'warn' | 'bad' }
  facts?: ReadFact[]
  details?: ReadDetail[]
  related?: ReadRelated[]
  events?: ReadEvent[]
  className?: string
}

const DEFAULT_FACTS: ReadFact[] = [
  { label: 'Contract value', value: '£248,000', note: 'Annual, indexed to CPI' },
  { label: 'Owner', value: 'Rhea Patel', note: 'Procurement' },
  { label: 'Renews', value: '1 April 2027', note: '90-day notice period' },
  { label: 'Risk tier', value: 'Tier 2', note: 'Reviewed 14 Aug 2026' },
]

const DEFAULT_DETAILS: ReadDetail[] = [
  { label: 'Supplier id', value: 'SUP-0417', mono: true },
  { label: 'Registered name', value: 'Northwind Trading Ltd' },
  { label: 'Company number', value: '09182736', mono: true },
  { label: 'Registered office', value: 'Rotterdam, Netherlands' },
  { label: 'Payment terms', value: 'Net 30' },
  { label: 'Currency', value: 'EUR' },
  { label: 'Insurance expiry', value: '30 November 2026' },
  { label: 'Data processing agreement', value: 'Signed 3 March 2026' },
]

const DEFAULT_RELATED: ReadRelated[] = [
  { id: 'po-2291', kind: 'Purchase order', title: 'PO-2291', meta: '£41,200 · Awaiting delivery' },
  { id: 'po-2264', kind: 'Purchase order', title: 'PO-2264', meta: '£18,900 · Received' },
  { id: 'inv-841', kind: 'Invoice', title: 'INV-2026-0841', meta: '£18,900 · Paid 31 Aug' },
  { id: 'rev-11', kind: 'Review', title: 'Annual supplier review', meta: 'Due 14 Feb 2027' },
]

const DEFAULT_EVENTS: ReadEvent[] = [
  { id: '1', what: 'Risk tier moved from 3 to 2', who: 'Sam Okafor', when: '14 Aug 2026' },
  { id: '2', what: 'Payment terms changed to Net 30', who: 'Rhea Patel', when: '2 Jul 2026' },
  { id: '3', what: 'Record created', who: 'Import · finance-2026', when: '11 Jan 2026' },
]

const TONE: Record<'good' | 'warn' | 'bad', string> = {
  good: 'bg-emerald-500/10 text-emerald-700 dark:text-emerald-400',
  warn: 'bg-amber-500/10 text-amber-700 dark:text-amber-400',
  bad: 'bg-destructive/10 text-destructive',
}

/*
  Per-instance id, hashed from props that differ between instances.

  A literal id is a latent duplicate the moment this block is rendered
  twice on one document -- two pages on a catalog hub, or one page using
  the section twice. `aria-labelledby` pointing at a duplicated id resolves
  to whichever element comes first, so the second copy is announced with
  the first copy's label. Server component, so no `useId`: hashing props
  gives each instance its own target and stays stable across server and
  client renders in a way a counter would not.
*/
function instanceId(...parts: (string | undefined)[]): string {
  const text = parts.filter(Boolean).join('|')
  let hash = 0
  for (let i = 0; i < text.length; i++) hash = (Math.imul(hash, 31) + text.charCodeAt(i)) | 0
  return (hash >>> 0).toString(36).slice(0, 6)
}

export function CrudReadPage({
  title = 'Northwind Trading Ltd',
  subtitle = 'Supplier · Tier 2 · Active since January 2026',
  status = { label: 'Approved', tone: 'good' },
  facts = DEFAULT_FACTS,
  details = DEFAULT_DETAILS,
  related = DEFAULT_RELATED,
  events = DEFAULT_EVENTS,
  className = '',
}: CrudReadPageProps) {
  const uid = instanceId(title, subtitle)

  return (
    <section className={`bg-background px-4 py-10 sm:px-6 ${className}`}>
      <div className="mx-auto max-w-5xl">
        <nav aria-label="Breadcrumb" className="text-xs text-muted-foreground">
          <ol className="flex items-center gap-1.5">
            <li>
              <a href="#" className="transition-colors hover:text-foreground">
                Suppliers
              </a>
            </li>
            <li aria-hidden>/</li>
            <li aria-current="page" className="text-foreground">
              {title}
            </li>
          </ol>
        </nav>

        <header className="mt-3 flex flex-wrap items-start justify-between gap-4">
          <div className="min-w-0">
            <div className="flex flex-wrap items-center gap-2.5">
              <h1 className="text-2xl font-bold tracking-tight">{title}</h1>
              <span
                className={`rounded px-1.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide ${TONE[status.tone]}`}
              >
                {status.label}
              </span>
            </div>
            <p className="mt-1 flex items-center gap-1.5 text-sm text-muted-foreground">
              <Building2 aria-hidden className="h-4 w-4" />
              {subtitle}
            </p>
          </div>

          <div className="flex flex-wrap items-center gap-2">
            <button
              type="button"
              className="inline-flex items-center gap-1.5 rounded-lg bg-primary px-3 py-2 text-sm font-semibold text-primary-foreground transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
            >
              <Pencil aria-hidden className="h-4 w-4" />
              Edit
            </button>
            <button
              type="button"
              className="rounded-lg border border-border px-3 py-2 text-sm font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
            >
              Raise an order
            </button>
            {/* Destructive last, and out of the primary group. */}
            <button
              type="button"
              className="inline-flex items-center gap-1.5 rounded-lg border border-transparent px-3 py-2 text-sm font-medium text-destructive transition-colors hover:border-destructive/40 hover:bg-destructive/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
            >
              <Trash2 aria-hidden className="h-4 w-4" />
              Archive
            </button>
          </div>
        </header>

        {/* The verdict line. Four facts, above everything the schema wants. */}
        <dl className="mt-6 grid grid-cols-2 gap-px overflow-hidden rounded-2xl border border-border bg-border lg:grid-cols-4">
          {facts.map((fact) => (
            <div key={fact.label} className="min-w-0 bg-card p-4">
              <dt className="truncate text-xs font-medium uppercase tracking-wide text-muted-foreground">
                {fact.label}
              </dt>
              <dd className="mt-1.5 break-words text-lg font-semibold tracking-tight">{fact.value}</dd>
              {fact.note ? (
                <dd className="mt-0.5 break-words text-xs text-muted-foreground">{fact.note}</dd>
              ) : null}
            </div>
          ))}
        </dl>

        <div className="mt-8 grid gap-8 lg:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]">
          <div className="space-y-8">
            <section aria-labelledby={`crud-read-details-${uid}`}>
              <h2 id={`crud-read-details-${uid}`} className="text-base font-semibold">
                Details
              </h2>
              <dl className="mt-3 divide-y divide-border/70 rounded-xl border border-border bg-card">
                {details.map((row) => (
                  <div
                    key={row.label}
                    className="flex flex-wrap items-baseline justify-between gap-3 px-4 py-3 text-sm"
                  >
                    <dt className="text-muted-foreground">{row.label}</dt>
                    <dd className={row.mono ? 'font-mono text-xs' : 'font-medium'}>
                      {row.value}
                    </dd>
                  </div>
                ))}
              </dl>
            </section>

            <section aria-labelledby={`crud-read-trail-${uid}`}>
              <h2
                id={`crud-read-trail-${uid}`}
                className="flex items-center gap-2 text-base font-semibold"
              >
                <ShieldCheck aria-hidden className="h-4 w-4 text-primary" />
                What changed
              </h2>
              <ol className="mt-3 space-y-4 rounded-xl border border-border bg-card p-4">
                {events.map((event) => (
                  <li key={event.id} className="flex gap-3">
                    <span
                      aria-hidden
                      className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-primary border border-transparent"
                    />
                    <div className="min-w-0">
                      <p className="text-sm">{event.what}</p>
                      <p className="mt-0.5 flex items-center gap-1.5 text-xs text-muted-foreground">
                        <User aria-hidden className="h-3 w-3" />
                        {event.who} · {event.when}
                      </p>
                    </div>
                  </li>
                ))}
              </ol>
            </section>
          </div>

          {/* Related records as a peer of the details, not a tab behind them. */}
          <section aria-labelledby={`crud-read-related-${uid}`}>
            <h2 id={`crud-read-related-${uid}`} className="text-base font-semibold">
              Attached to this record
            </h2>
            <ul className="mt-3 space-y-2">
              {related.map((item) => (
                <li key={item.id}>
                  <a
                    href="#"
                    className="flex items-center justify-between gap-3 rounded-xl border border-border bg-card p-3 transition-colors hover:border-primary/40 hover:bg-muted/50"
                  >
                    <span className="min-w-0">
                      <span className="block text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
                        {item.kind}
                      </span>
                      <span className="mt-0.5 block truncate text-sm font-medium">
                        {item.title}
                      </span>
                      <span className="block truncate text-xs text-muted-foreground">
                        {item.meta}
                      </span>
                    </span>
                    <ExternalLink
                      aria-hidden
                      className="h-4 w-4 shrink-0 text-muted-foreground"
                    />
                  </a>
                </li>
              ))}
            </ul>

            <p className="mt-4 flex items-start gap-2 rounded-xl border border-dashed border-border p-3 text-xs text-muted-foreground">
              <CalendarClock aria-hidden className="mt-0.5 h-3.5 w-3.5 shrink-0" />
              The annual review is raised automatically 60 days before the
              renewal date. Nothing on this page triggers it.
            </p>
          </section>
        </div>
      </div>
    </section>
  )
}

Before you paste

  • Styling is Tailwind utility classes on semantic tokens (bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.
  • Install: npm i lucide-react
  • Every prop has a default, so it renders standalone before you wire it up.

Where it goes

Drop it at components/crud-read-page.tsx and import it where you need the section:

import { CrudReadPage } from '@/components/crud-read-page'

Customize

2 of this block’s props are simple enough to drive from here. Change them and the block below re-renders — it is the same component whose source is above, not a mock of it. Everything else it accepts is in the table underneath.

Props

Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.

PropTypeDefault
titlestring'Northwind Trading Ltd'
subtitlestring'Supplier · Tier 2 · Active since January 2026'
status{ label: string; tone: 'good' | 'warn' | 'bad' }{ label: 'Approved', tone: 'good' }
factsReadFact[]DEFAULT_FACTS
detailsReadDetail[]DEFAULT_DETAILS
relatedReadRelated[]DEFAULT_RELATED
eventsReadEvent[]DEFAULT_EVENTS
classNamestring''

Not using React?

The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.

crud-read-page.html
<!--
  Record Detail Page — markup from the Hoverlab catalog.

  This is the block rendered once to HTML and wrapped as a component
  file. It is not a port of the React source: the Tailwind classes carry
  the design, which is the part that took the work, and they are the same
  in every framework.

  This block has no interactive behaviour, so nothing is missing.
-->
<section class="bg-background px-4 py-10 sm:px-6 ">
  <div class="mx-auto max-w-5xl">
    <nav aria-label="Breadcrumb" class="text-xs text-muted-foreground">
      <ol class="flex items-center gap-1.5">
        <li>
          <a href="#" class="transition-colors hover:text-foreground">Suppliers</a>
        </li>
        <li aria-hidden="true">/</li>
        <li aria-current="page" class="text-foreground">Northwind Trading Ltd</li>
      </ol>
    </nav>
    <header class="mt-3 flex flex-wrap items-start justify-between gap-4">
      <div class="min-w-0">
        <div class="flex flex-wrap items-center gap-2.5">
          <h1 class="text-2xl font-bold tracking-tight">Northwind Trading Ltd</h1>
          <span class="rounded px-1.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide bg-emerald-500/10 text-emerald-700 dark:text-emerald-400">Approved</span>
        </div>
        <p class="mt-1 flex items-center gap-1.5 text-sm text-muted-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-building2 lucide-building-2 h-4 w-4" aria-hidden="true"><path d="M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z"></path><path d="M6 12H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2"></path><path d="M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2"></path><path d="M10 6h4"></path><path d="M10 10h4"></path><path d="M10 14h4"></path><path d="M10 18h4"></path></svg>Supplier · Tier 2 · Active since January 2026</p>
      </div>
      <div class="flex flex-wrap items-center gap-2">
        <button type="button" class="inline-flex items-center gap-1.5 rounded-lg bg-primary px-3 py-2 text-sm font-semibold text-primary-foreground transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pencil h-4 w-4" aria-hidden="true"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"></path><path d="m15 5 4 4"></path></svg>Edit</button>
        <button type="button" class="rounded-lg border border-border px-3 py-2 text-sm font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">Raise an order</button>
        <button type="button" class="inline-flex items-center gap-1.5 rounded-lg border border-transparent px-3 py-2 text-sm font-medium text-destructive transition-colors hover:border-destructive/40 hover:bg-destructive/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash2 lucide-trash-2 h-4 w-4" aria-hidden="true"><path d="M3 6h18"></path><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path><line x1="10" x2="10" y1="11" y2="17"></line><line x1="14" x2="14" y1="11" y2="17"></line></svg>Archive</button>
      </div>
    </header>
    <dl class="mt-6 grid grid-cols-2 gap-px overflow-hidden rounded-2xl border border-border bg-border lg:grid-cols-4">
      <div class="min-w-0 bg-card p-4">
        <dt class="truncate text-xs font-medium uppercase tracking-wide text-muted-foreground">Contract value</dt>
        <dd class="mt-1.5 break-words text-lg font-semibold tracking-tight">£248,000</dd>
        <dd class="mt-0.5 break-words text-xs text-muted-foreground">Annual, indexed to CPI</dd>
      </div>
      <div class="min-w-0 bg-card p-4">
        <dt class="truncate text-xs font-medium uppercase tracking-wide text-muted-foreground">Owner</dt>
        <dd class="mt-1.5 break-words text-lg font-semibold tracking-tight">Rhea Patel</dd>
        <dd class="mt-0.5 break-words text-xs text-muted-foreground">Procurement</dd>
      </div>
      <div class="min-w-0 bg-card p-4">
        <dt class="truncate text-xs font-medium uppercase tracking-wide text-muted-foreground">Renews</dt>
        <dd class="mt-1.5 break-words text-lg font-semibold tracking-tight">1 April 2027</dd>
        <dd class="mt-0.5 break-words text-xs text-muted-foreground">90-day notice period</dd>
      </div>
      <div class="min-w-0 bg-card p-4">
        <dt class="truncate text-xs font-medium uppercase tracking-wide text-muted-foreground">Risk tier</dt>
        <dd class="mt-1.5 break-words text-lg font-semibold tracking-tight">Tier 2</dd>
        <dd class="mt-0.5 break-words text-xs text-muted-foreground">Reviewed 14 Aug 2026</dd>
      </div>
    </dl>
    <div class="mt-8 grid gap-8 lg:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]">
      <div class="space-y-8">
        <section aria-labelledby="crud-read-details-l8yfhq">
          <h2 id="crud-read-details-l8yfhq" class="text-base font-semibold">Details</h2>
          <dl class="mt-3 divide-y divide-border/70 rounded-xl border border-border bg-card">
            <div class="flex flex-wrap items-baseline justify-between gap-3 px-4 py-3 text-sm">
              <dt class="text-muted-foreground">Supplier id</dt>
              <dd class="font-mono text-xs">SUP-0417</dd>
            </div>
            <div class="flex flex-wrap items-baseline justify-between gap-3 px-4 py-3 text-sm">
              <dt class="text-muted-foreground">Registered name</dt>
              <dd class="font-medium">Northwind Trading Ltd</dd>
            </div>
            <div class="flex flex-wrap items-baseline justify-between gap-3 px-4 py-3 text-sm">
              <dt class="text-muted-foreground">Company number</dt>
              <dd class="font-mono text-xs">09182736</dd>
            </div>
            <div class="flex flex-wrap items-baseline justify-between gap-3 px-4 py-3 text-sm">
              <dt class="text-muted-foreground">Registered office</dt>
              <dd class="font-medium">Rotterdam, Netherlands</dd>
            </div>
            <div class="flex flex-wrap items-baseline justify-between gap-3 px-4 py-3 text-sm">
              <dt class="text-muted-foreground">Payment terms</dt>
              <dd class="font-medium">Net 30</dd>
            </div>
            <div class="flex flex-wrap items-baseline justify-between gap-3 px-4 py-3 text-sm">
              <dt class="text-muted-foreground">Currency</dt>
              <dd class="font-medium">EUR</dd>
            </div>
            <div class="flex flex-wrap items-baseline justify-between gap-3 px-4 py-3 text-sm">
              <dt class="text-muted-foreground">Insurance expiry</dt>
              <dd class="font-medium">30 November 2026</dd>
            </div>
            <div class="flex flex-wrap items-baseline justify-between gap-3 px-4 py-3 text-sm">
              <dt class="text-muted-foreground">Data processing agreement</dt>
              <dd class="font-medium">Signed 3 March 2026</dd>
            </div>
          </dl>
        </section>
        <section aria-labelledby="crud-read-trail-l8yfhq">
          <h2 id="crud-read-trail-l8yfhq" class="flex items-center gap-2 text-base font-semibold"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-shield-check h-4 w-4 text-primary" aria-hidden="true"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"></path><path d="m9 12 2 2 4-4"></path></svg>What changed</h2>
          <ol class="mt-3 space-y-4 rounded-xl border border-border bg-card p-4">
            <li class="flex gap-3">
              <span aria-hidden="true" class="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-primary border border-transparent"></span>
              <div class="min-w-0">
                <p class="text-sm">Risk tier moved from 3 to 2</p>
                <p class="mt-0.5 flex items-center gap-1.5 text-xs text-muted-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user h-3 w-3" aria-hidden="true"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>Sam Okafor · 14 Aug 2026</p>
              </div>
            </li>
            <li class="flex gap-3">
              <span aria-hidden="true" class="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-primary border border-transparent"></span>
              <div class="min-w-0">
                <p class="text-sm">Payment terms changed to Net 30</p>
                <p class="mt-0.5 flex items-center gap-1.5 text-xs text-muted-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user h-3 w-3" aria-hidden="true"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>Rhea Patel · 2 Jul 2026</p>
              </div>
            </li>
            <li class="flex gap-3">
              <span aria-hidden="true" class="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-primary border border-transparent"></span>
              <div class="min-w-0">
                <p class="text-sm">Record created</p>
                <p class="mt-0.5 flex items-center gap-1.5 text-xs text-muted-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user h-3 w-3" aria-hidden="true"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>Import · finance-2026 · 11 Jan 2026</p>
              </div>
            </li>
          </ol>
        </section>
      </div>
      <section aria-labelledby="crud-read-related-l8yfhq">
        <h2 id="crud-read-related-l8yfhq" class="text-base font-semibold">Attached to this record</h2>
        <ul class="mt-3 space-y-2">
          <li>
            <a href="#" class="flex items-center justify-between gap-3 rounded-xl border border-border bg-card p-3 transition-colors hover:border-primary/40 hover:bg-muted/50">
              <span class="min-w-0">
                <span class="block text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">Purchase order</span>
                <span class="mt-0.5 block truncate text-sm font-medium">PO-2291</span>
                <span class="block truncate text-xs text-muted-foreground">£41,200 · Awaiting delivery</span>
              </span>
              <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-external-link h-4 w-4 shrink-0 text-muted-foreground" aria-hidden="true">
                <path d="M15 3h6v6"></path>
                <path d="M10 14 21 3"></path>
                <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
              </svg>
            </a>
          </li>
          <li>
            <a href="#" class="flex items-center justify-between gap-3 rounded-xl border border-border bg-card p-3 transition-colors hover:border-primary/40 hover:bg-muted/50">
              <span class="min-w-0">
                <span class="block text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">Purchase order</span>
                <span class="mt-0.5 block truncate text-sm font-medium">PO-2264</span>
                <span class="block truncate text-xs text-muted-foreground">£18,900 · Received</span>
              </span>
              <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-external-link h-4 w-4 shrink-0 text-muted-foreground" aria-hidden="true">
                <path d="M15 3h6v6"></path>
                <path d="M10 14 21 3"></path>
                <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
              </svg>
            </a>
          </li>
          <li>
            <a href="#" class="flex items-center justify-between gap-3 rounded-xl border border-border bg-card p-3 transition-colors hover:border-primary/40 hover:bg-muted/50">
              <span class="min-w-0">
                <span class="block text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">Invoice</span>
                <span class="mt-0.5 block truncate text-sm font-medium">INV-2026-0841</span>
                <span class="block truncate text-xs text-muted-foreground">£18,900 · Paid 31 Aug</span>
              </span>
              <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-external-link h-4 w-4 shrink-0 text-muted-foreground" aria-hidden="true">
                <path d="M15 3h6v6"></path>
                <path d="M10 14 21 3"></path>
                <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
              </svg>
            </a>
          </li>
          <li>
            <a href="#" class="flex items-center justify-between gap-3 rounded-xl border border-border bg-card p-3 transition-colors hover:border-primary/40 hover:bg-muted/50">
              <span class="min-w-0">
                <span class="block text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">Review</span>
                <span class="mt-0.5 block truncate text-sm font-medium">Annual supplier review</span>
                <span class="block truncate text-xs text-muted-foreground">Due 14 Feb 2027</span>
              </span>
              <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-external-link h-4 w-4 shrink-0 text-muted-foreground" aria-hidden="true">
                <path d="M15 3h6v6"></path>
                <path d="M10 14 21 3"></path>
                <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
              </svg>
            </a>
          </li>
        </ul>
        <p class="mt-4 flex items-start gap-2 rounded-xl border border-dashed border-border p-3 text-xs text-muted-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-calendar-clock mt-0.5 h-3.5 w-3.5 shrink-0" aria-hidden="true"><path d="M16 14v2.2l1.6 1"></path><path d="M16 2v4"></path><path d="M21 7.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3.5"></path><path d="M3 10h5"></path><path d="M8 2v4"></path><circle cx="16" cy="16" r="6"></circle></svg>The annual review is raised automatically 60 days before the renewal date. Nothing on this page triggers it.</p>
      </section>
    </div>
  </div>
</section>
  • This is rendered HTML, not a translation of the React source. The Tailwind classes carry the design and work in any framework.
  • It is one frame: the component in its initial state, with no props applied beyond the defaults.
  • Requires Tailwind, and the design tokens the classes reference (bg-card, text-muted-foreground, and so on). The template ZIPs ship a globals.css that defines them.

What each framework gets across the whole catalog — effects convert properly; this rung is markup.

For AI

The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.

See the prompt

11 more blocks in CRUD

All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.

Browse CRUD

Shipping one commercially

Copying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.

  • A commercial licence for everything in the catalog
  • Unlimited bundle exports, in Vue, Svelte and Tailwind
  • One payment — no subscription, nothing to renew
Pro — $79 once

More CRUD blocks

View category
Open the full page for this block

Create Record Form

A full-page create form with a guidance rail and a Save-and-create-another that keeps the shared fields - the shape a record with more than six fields actually needs.

CRUD408 lines1 dep
Open the full page for this block

Create Record Modal

Three fields over the list they add to, with the new row appearing behind the dialog rather than after it closes.

CRUD262 lines1 dep
Open the full page for this block

Create Record Drawer

A long create form in a side drawer: the fields scroll, the header and the action bar do not, and the list stays legible beside it.

CRUD336 lines1 dep
Open the full page for this block

Record Quick Look

The verification modal from a table row: copyable identifiers, no editing, nothing scrolling, and the row still marked behind it.

CRUD244 lines1 dep
Open the full page for this block

Edit Record Form

An edit page that shows the diff - per-field change markers carrying the previous value, a per-field revert, and a Save button that counts what it will write.

CRUD259 lines1 dep
Open the full page for this block

Edit Record Modal

The edit modal including the parts nobody draws: the in-flight state, the rejected save that keeps your typing, and the conflict when someone else got there first.

CRUD248 lines1 dep
Open the full page for this block

Field-Level Edit Drawer

The save-on-blur model, with per-field confirmation, per-field failure and a written-in-this-session log - the only honest answer to what did I just change.

CRUD327 lines1 dep
Open the full page for this block

Cascade Delete Dialog

Deleting a record other records depend on: dependents counted and grouped by fate - deleted, orphaned, retained - with a guard that scales to the blast radius.

CRUD262 lines1 dep
Open the full page for this block

Record Created State

The screen after the write: the reference copyable, next actions ranked rather than listed, and an undo window that counts down honestly and then says why it is gone.

CRUD260 lines1 dep
Open the full page for this block

Bulk Edit Drawer

One change applied to many records that disagree - mixed values shown as mixed, untouched fields never written, and skipped rows counted before you commit.

CRUD229 lines1 dep
Open the full page for this block

Archive and Restore

The bin with the clock on it: days until purge per row, who archived it, and a restore disabled with a reason when the parent has already gone.

CRUD277 lines1 dep