Descriptions
Schema-driven read-only detail grid with the same field kinds as Table, responsive columns, and a loading skeleton.
Plus Registry
Descriptions ships on the authenticated @f-ui-plus registry. Configure FUI_PLUS_REGISTRY_TOKEN as described in Installation — Plus Registry.
Descriptions is the read-only detail grid: FieldConfig fields rendered through the same renderCellByKind path as Table. Derive items with descriptionsFieldsFromSchema so list and detail stay in sync. Compose a compact grid in Page content, or a full grid inside Section Card. Layout rules: Object Page Cards and CRUD Page Patterns — Descriptions Layout And Header KPIs.
When To Use
- Object Page / drawer read fields that share a schema with the list (order number, status, amount).
- Default
layout="vertical"(label above value). Usehorizontalonly for dense short scalars (Contact, Fulfillment). - Header Amount / Quantity / Balance are Statistic in
PageHeader.extraContent— never hero type on Descriptions values. - Do not pass
borderedinside a Section Card / Related List — the card already owns the shell. - Do not pass
titlewhen the field grid already sits in a titled card. The card owns the name.
Features
| Area | Behavior |
|---|---|
| Fields | FieldConfig + key / optional span / tooltip. Spread a schema field as { ...schema.definition.amount, key: "amount" } |
| Schema | descriptionsFieldsFromSchema(schema, keys?) drops hidden and access-hidden keys |
| Format | Same kinds as Table — currency, status, copyable, empty dash |
| Layout | Vertical default; horizontal for short scalars; column is a number or breakpoint map (max 4) |
| Empty | null / "" → display dash, unless emptyText is set. Access masked / pending ignore emptyText |
| Loading | Field skeleton. Do not flash raw values while the record is in flight |
| Access | Masked / pending / hidden follow Access. Pass fieldAccess / accessPendingIds on the schema helper — Descriptions does not auto-wire like Table |
Installing
pnpm dlx shadcn@latest add https://ui.isaacfei.com/api/plus/r/descriptions.jsonnpx shadcn@latest add https://ui.isaacfei.com/api/plus/r/descriptions.jsonyarn dlx shadcn@latest add https://ui.isaacfei.com/api/plus/r/descriptions.jsonbunx shadcn@latest add https://ui.isaacfei.com/api/plus/r/descriptions.jsonWith the @f-ui-plus namespace configured in components.json, shadcn add @f-ui-plus/descriptions also works.
Runtime npm dep: lucide-react. registryDependencies: @f-ui-plus/data-list-view, https://ui.isaacfei.com/r/access.json, shadcn skeleton, tooltip, utils, https://ui.isaacfei.com/r/link.json.
Usage
import { Descriptions } from "@/components/f-ui/descriptions/descriptions";
import { descriptionsFieldsFromSchema } from "@/components/f-ui/descriptions/lib/descriptions-field";
<Descriptions
record={order}
fields={descriptionsFieldsFromSchema(schema)}
/>Examples
Schema-Driven Vertical Grid
The kit default: label above value, responsive columns, copyable Order #, empty Notes as a display dash. Unbordered inside the card — Section Card owns the shell.
Order details
- Order #
- ORD-1001
- Customer
- Acme Corp
- Status
- Confirmed
- Amount
- USD 1,280.00
- Notes
- —
"use client";
import { defineDataListSchema } from "@/components/f-ui/data-list-internals/schema/define-data-list-schema";
import { Descriptions } from "@/components/f-ui/descriptions/descriptions";
import { descriptionsFieldsFromSchema } from "@/components/f-ui/descriptions/lib/descriptions-field";
import { f } from "@/components/f-ui/field-types/catalog";
import { SectionCard } from "@/components/f-ui/section-card/section-card";
type Order = {
orderNumber: string;
customer: string;
status: "confirmed" | "shipped";
amount: number;
notes: string;
};
const schema = defineDataListSchema<Order>({
orderNumber: f.text({ label: "Order #", copyable: true }),
customer: f.text({ label: "Customer" }),
status: f.enum({
render: "status",
label: "Status",
variants: {
confirmed: { label: "Confirmed", tone: "success" },
shipped: { label: "Shipped", tone: "info" },
},
}),
amount: f.currency({ label: "Amount", currency: "USD" }),
notes: f.text({ label: "Notes" }),
});
const record: Order = {
orderNumber: "ORD-1001",
customer: "Acme Corp",
status: "confirmed",
amount: 1280,
notes: "",
};
const fields = descriptionsFieldsFromSchema(schema);
/** Schema-driven vertical grid — kit default. Empty notes render the display dash. */
export function DescriptionsDemo() {
return (
<SectionCard title="Order details">
<Descriptions record={record} fields={fields} />
</SectionCard>
);
}Horizontal Short Scalars
Contact-style fields sit label beside value. Keep long notes and addresses vertical, or give them a wider span.
Contact
- Name:
- Ada Lovelace
- Email:
- ada@example.com
- Phone:
- 555-0100
- City:
- London
"use client";
import { defineDataListSchema } from "@/components/f-ui/data-list-internals/schema/define-data-list-schema";
import { Descriptions } from "@/components/f-ui/descriptions/descriptions";
import { descriptionsFieldsFromSchema } from "@/components/f-ui/descriptions/lib/descriptions-field";
import { f } from "@/components/f-ui/field-types/catalog";
import { SectionCard } from "@/components/f-ui/section-card/section-card";
type Contact = {
name: string;
email: string;
phone: string;
city: string;
};
const schema = defineDataListSchema<Contact>({
name: f.text({ label: "Name" }),
email: f.text({ label: "Email" }),
phone: f.text({ label: "Phone" }),
city: f.text({ label: "City" }),
});
const record: Contact = {
name: "Ada Lovelace",
email: "ada@example.com",
phone: "555-0100",
city: "London",
};
/** Horizontal layout for dense short scalars. Long notes stay vertical. */
export function DescriptionsHorizontalDemo() {
return (
<SectionCard title="Contact">
<Descriptions
record={record}
fields={descriptionsFieldsFromSchema(schema)}
layout="horizontal"
column={{ base: 1, md: 2 }}
/>
</SectionCard>
);
}Loading Skeleton
Pass loading until the record is applied. The grid shows field-shaped skeletons instead of an empty dash wall.
Order details
"use client";
import { Descriptions } from "@/components/f-ui/descriptions/descriptions";
import { SectionCard } from "@/components/f-ui/section-card/section-card";
const PLACEHOLDER_FIELDS = [
{ key: "orderNumber", label: "Order #", kind: "text" as const },
{ key: "customer", label: "Customer", kind: "text" as const },
{ key: "owner", label: "Owner", kind: "text" as const },
{ key: "amount", label: "Amount", kind: "text" as const },
];
/** First-fetch skeleton. Flip `loading` off after `record` is applied. */
export function DescriptionsLoadingDemo() {
return (
<SectionCard title="Order details">
<Descriptions record={{}} fields={PLACEHOLDER_FIELDS} loading />
</SectionCard>
);
}Composition
Descriptions
├─ header? (title + extra)
└─ dl grid
└─ item (label + optional tooltip + value via renderCellByKind)API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
record | T | — | Source object. |
fields | DescriptionsField<T>[] | — | Items to render. Prefer descriptionsFieldsFromSchema. |
column | number | { base?, sm?, md?, lg?, xl? } | { base: 1, md: 2, lg: 3 } | Column count per breakpoint, clamped to 1–4. A lone number maps to { base: 1, md: n }. |
layout | "vertical" | "horizontal" | "vertical" | Label above value, or label beside value. |
bordered | boolean | false | Standalone card chrome. Do not use inside a Section Card. |
size | "small" | "middle" | "large" | "middle" | Value type scale stays body (text-xs / text-sm / text-base). |
title | ReactNode | — | Grid heading. Omit when a card already names the section. |
extra | ReactNode | — | Actions on the grid header row. |
loading | boolean | false | Field skeleton. |
linkComponent | LinkComponent | — | Link override for f.link values. |
emptyText | ReactNode | — | Fallback for null / "". Ignored when masked or pending. |
colon | boolean | true when horizontal | Trailing colon on labels. |
className | string | — | Root. |
classNames | { root, header, title, grid, item, label, value } | — | Per-slot classes. |
descriptionsFieldsFromSchema
descriptionsFieldsFromSchema(schema, keys?, {
fieldAccess,
accessPendingIds,
})Omit keys to include every non-hidden field. Pass fieldAccess / accessPendingIds from Access so list-scope mask and pending decisions reach the grid — Table / Query List auto-wire this; Descriptions does not.