Property Section

Compact key/value grid for inspector panels — labels, sublabels, optional collapse. Install Property Section from the VLLNT UI registry with the shadcn CLI.

Report a bug

Compact key/value grid for inspector panels — labels, sublabels, optional collapse. Part of the content family in VLLNT UI, it ships as a machine-readable registry entry — copy the source directly into your app with the shadcn CLI and own it, no runtime dependency on a component library.

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Installation

Add Property Section to your project with the shadcn CLI. The source lands in your codebase, ready to adapt:

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/property-section.json

Source

"use client"; import type { ComponentPropsWithoutRef, ReactNode } from "react"; import { cn } from "../../lib/utils"; /** * One row in the property grid. * * @public */ export type PropertyEntry = { /** Stable id (used as React key + analytics hook). */ id: string; /** Property label (left column). */ label: ReactNode; /** Optional sublabel rendered beneath the label. */ sublabel?: ReactNode; /** Property value (right column). */ value: ReactNode; }; /** * Localizable strings. * * @public */ export type PropertySectionLabels = { /** Aria-label for the section. Defaults to `"Properties"`. */ region?: string; }; const DEFAULT_LABELS = { region: "Properties", } as const satisfies Required<PropertySectionLabels>; /** * Props for {@link PropertySection}. * * @public */ export type PropertySectionProps = { /** Optional collapsible affordance. Pass `false` to render the body inline (default). */ collapsible?: boolean; /** Property entries in render order. */ entries: PropertyEntry[]; /** Localizable strings. */ labels?: PropertySectionLabels; /** Optional title rendered as a small uppercase heading above the grid. */ title?: ReactNode; } & ComponentPropsWithoutRef<"section">; const Grid = (props: { entries: PropertyEntry[] }): React.ReactElement => ( <dl className="grid grid-cols-[max-content_1fr] gap-x-4 gap-y-1.5 px-3 pb-3 pt-1 text-xs" data-property-grid > {props.entries.map((entry) => ( <div className="contents" key={entry.id}> <dt className="flex flex-col text-muted-foreground" data-property-id={entry.id} > <span>{entry.label}</span> {entry.sublabel ? ( <span className="text-[10px] uppercase tracking-wide text-muted-foreground/70"> {entry.sublabel} </span> ) : null} </dt> <dd className="text-right text-foreground">{entry.value}</dd> </div> ))} </dl> ); const Body = (props: { collapsible: boolean; entries: PropertyEntry[]; title?: ReactNode; }): React.ReactElement => { const grid = <Grid entries={props.entries} />; if (!props.title) { return grid; } if (props.collapsible) { return ( <details className="group" open> <summary className="flex cursor-pointer items-center justify-between gap-2 px-3 py-2 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground" data-property-summary > <span>{props.title}</span> <span aria-hidden="true" className="text-muted-foreground/70 group-open:rotate-90" > </span> </summary> {grid} </details> ); } return ( <> <header className="flex items-center justify-between gap-2 border-b border-border px-3 py-2 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground" data-property-header > {props.title} </header> {grid} </> ); }; /** * Compact key / value grid for an inspector panel. Use one * `PropertySection` per logical group (Identity, Layout, State, etc.) * so the right dock stays scannable. Pure presentation — the host * computes the entries from the current selection. * * @example * ```tsx * <PropertySection * title="Layout" * entries={[ * { id: "x", label: "X", value: "120" }, * { id: "y", label: "Y", value: "80" }, * { id: "size", label: "Size", value: "240 × 120" }, * ]} * /> * ``` * * @public */ export const PropertySection = ({ ref, ...props }: PropertySectionProps & { ref?: React.Ref<HTMLElement> }) => { const { className, collapsible = false, entries, labels, title, ...rest } = props; const resolvedLabels = { ...DEFAULT_LABELS, ...labels }; return ( <section aria-label={resolvedLabels.region} className={cn( "rounded-lg border bg-background text-foreground", className, )} data-property-section ref={ref} {...rest} > <Body collapsible={collapsible} entries={entries} title={title} /> </section> ); }; PropertySection.displayName = "PropertySection";

Stories

Explore every variant and state in the interactive Storybook:

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Dependencies

  • @vllnt/ui@^0.3.0