Beschreibungsliste
Begriffe und Werte als echte dl, in gestapelten Zeilen, zwei Spalten, einem Raster aus Haarlinien, einer Metazeile oder einem Datenblatt mit Gruppen. Werte lassen sich kopieren, verlinken und mit Status versehen.
Eine von 4 Components in Listen, Bäume und Timelines
Variants
Details
Was Beschreibungsliste mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Listen, Bäume und Timelines
- Bereich
- Elements
- Variants
- 5
- Version
- 1.0.0 · seit 1.1.0
- Datei
- components/ui/description-list.tsx
- Bringt mit
- utilskiticoncopy-button
- npm-Pakete
- Keine
- Sprachen
- Englisch und Deutsch, 3 eigene Texte
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle DescriptionListProps aus components/ui/description-list.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
variantoptionalDescriptionListVariantstackedcolumnsgridinlinespec
itemsPflichtDescriptionItem[]titleoptionalstringA heading above the list
descriptionoptionalstringA line under the heading
actionoptionalReact.ReactNodeAn edit link or button at the end of the heading row
columnsoptional2 | 3 | 4grid: columns on large screens. Default 3.
visibleoptionalnumberspec and stacked: rows shown before "Show all"
Eingebaute Texte
Diese Texte schreibt Beschreibungsliste selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.
Alle 3 Texte anzeigen
| Schlüssel | Englisch | Deutsch |
|---|---|---|
| more | Englisch Show all | Deutsch Alle zeigen |
| less | Englisch Show fewer | Deutsch Weniger zeigen |
| copy | Englisch Copy | Deutsch Kopieren |
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Setzt aria-controls und aria-expanded, damit Screenreader Zweck und Zustand ansagen.
- Zeigt bei Bedienung mit der Tastatur einen sichtbaren Fokusring.
- Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.
Bewegung
- Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 1 Stellen im Code schalten Bewegung dann ab.
- Ohne Animationen, sie steht sofort ruhig da.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 287 Zeilen
'use client';
import { useId, useState } from 'react';
import { cn, useInView, useTexts } from '@/lib/kit';
import { Icon, type IconName } from '@/components/ui/icon';
import { CopyButton } from '@/components/ui/copy-button';
/*
* Terms and their values, as a real <dl>. For detail pages, order and
* account summaries, product specs and the meta line under a heading.
* Five looks: stacked rows, two columns, a grid of tiles, an inline meta
* row and a spec sheet with groups. Values can be copied, linked, marked
* with a status, and long lists fold away behind "Show all".
*/
const TEXTS = {
en: { more: 'Show all', less: 'Show fewer', copy: 'Copy' },
de: { more: 'Alle zeigen', less: 'Weniger zeigen', copy: 'Kopieren' },
};
export type DescriptionTone = 'neutral' | 'success' | 'warning' | 'danger' | 'info';
export interface DescriptionItem {
term: string;
value: string;
/** A second, quieter line under the value */
description?: string;
/** A small icon before the term */
icon?: IconName;
/** A copy button for the value */
copy?: boolean;
/** The value links here */
href?: string;
/** Shows the value as a status with a dot */
tone?: DescriptionTone;
/** spec: a group heading above this item */
group?: string;
/** Any node instead of the plain value, e.g. avatars or a progress bar */
node?: React.ReactNode;
}
export type DescriptionListVariant = 'stacked' | 'columns' | 'grid' | 'inline' | 'spec';
export interface DescriptionListProps {
variant?: DescriptionListVariant;
items: DescriptionItem[];
/** A heading above the list */
title?: string;
/** A line under the heading */
description?: string;
/** An edit link or button at the end of the heading row */
action?: React.ReactNode;
/** grid: columns on large screens. Default 3. */
columns?: 2 | 3 | 4;
/** spec and stacked: rows shown before "Show all" */
visible?: number;
labels?: Partial<(typeof TEXTS)['en']>;
className?: string;
}
const TONE: Record<DescriptionTone, string> = {
neutral: 'bg-[color-mix(in_oklab,var(--foreground)_45%,transparent)]',
success: 'bg-success',
warning: 'bg-warning',
danger: 'bg-destructive',
info: 'bg-info',
};
function Value({ item, strong }: { item: DescriptionItem; strong?: boolean }) {
if (item.node) return <>{item.node}</>;
const text = item.tone ? (
<span className="inline-flex items-center gap-2">
<span aria-hidden className={cn('size-2 shrink-0 rounded-full', TONE[item.tone])} />
{item.value}
</span>
) : (
item.value
);
return (
<span className={cn('min-w-0 break-words', strong ? 'font-medium text-foreground' : 'text-foreground')}>
{item.href ? (
<a href={item.href} className="rounded-sm underline decoration-[color-mix(in_oklab,var(--foreground)_30%,transparent)] underline-offset-[3px] transition-colors hover:decoration-foreground focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-2 focus-visible:outline-primary">
{text}
</a>
) : (
text
)}
</span>
);
}
function Term({ item, className }: { item: DescriptionItem; className?: string }) {
return (
<dt className={cn('flex items-center gap-2 text-[13px] text-muted-foreground', className)}>
{item.icon && <Icon name={item.icon} size={15} className="shrink-0 text-subtle-foreground" />}
{item.term}
</dt>
);
}
/** Value, its second line and the copy button, in one dd */
function Detail({ item, className, strong }: { item: DescriptionItem; className?: string; strong?: boolean }) {
return (
<dd className={cn('flex min-w-0 items-start justify-between gap-3 text-sm', className)}>
<span className="min-w-0">
<Value item={item} strong={strong} />
{item.description && <span className="mt-0.5 block text-[13px] text-muted-foreground">{item.description}</span>}
</span>
{item.copy && <CopyButton value={item.value} size="xs" className="-my-1 shrink-0 max-sm:-my-3 max-sm:size-11" />}
</dd>
);
}
/**
* Terms and values for detail pages, summaries, specs and meta lines.
* <DescriptionList title="Account" items={[{ term: 'Plan', value: 'Studio' }]} />
* <DescriptionList variant="columns" items={…} />
* <DescriptionList variant="grid" columns={4} items={…} />
* <DescriptionList variant="inline" items={…} />
* <DescriptionList variant="spec" visible={6} items={…} />
*/
export function DescriptionList({ variant = 'stacked', items, title, description, action, columns = 3, visible, labels, className }: DescriptionListProps) {
const t = useTexts(TEXTS, labels);
const [ref, inView] = useInView<HTMLDivElement>();
const [all, setAll] = useState(false);
const listId = useId();
const foldable = (variant === 'spec' || variant === 'stacked' || variant === 'columns') && visible !== undefined && items.length > visible;
const head = (title || action) && (
<div className={cn('flex items-start justify-between gap-4', variant === 'grid' || variant === 'inline' ? 'mb-4' : 'mb-2')}>
<div className="min-w-0">
{title && <h3 className="text-[15px] font-semibold text-foreground">{title}</h3>}
{description && <p className="mt-0.5 text-[13px] text-muted-foreground">{description}</p>}
</div>
{action && <div className="shrink-0">{action}</div>}
</div>
);
// Rows rise one after another once the list scrolls into view
const rise = (i: number) => ({
className: cn('transition-[opacity,translate] duration-500 ease-[var(--ease-kit)] motion-reduce:transition-none', !inView && 'translate-y-2 opacity-0'),
style: { transitionDelay: inView ? `${Math.min(i, 10) * 45}ms` : '0ms' } as React.CSSProperties,
});
const folded = (i: number) => foldable && !all && i >= (visible ?? 0);
const more = foldable && (
<button
type="button"
aria-expanded={all}
aria-controls={listId}
onClick={() => setAll(!all)}
className={cn(variant === 'spec' ? 'ms-1.5' : '-ms-1.5', 'mt-2 inline-flex min-h-11 items-center gap-1 rounded-md px-1.5 text-[13px] font-medium text-foreground outline-none hover:underline focus-visible:shadow-[0_0_0_2px_color-mix(in_oklab,var(--primary)_55%,transparent)] sm:min-h-8')}
>
{all ? t.less : `${t.more} (${items.length})`}
<Icon name="chevron-down" size={14} className={cn('transition-[rotate] duration-300 ease-[var(--ease-kit)]', all && 'rotate-180')} />
</button>
);
/* ---------- inline: the meta line under a heading ---------- */
if (variant === 'inline') {
return (
<div ref={ref} className={cn('w-full', className)}>
{head}
<dl className="grid grid-cols-2 gap-x-6 gap-y-4 sm:flex sm:flex-wrap sm:items-stretch sm:gap-0">
{items.map((item, i) => (
<div key={i} {...rise(i)} className={cn(rise(i).className, 'min-w-0 sm:px-6 sm:first:ps-0 sm:[&+&]:border-s sm:[&+&]:border-border')}>
<Term item={item} className="text-[12px]" />
<dd className="mt-1 flex items-center gap-1.5 text-sm">
<Value item={item} strong />
{item.copy && <CopyButton value={item.value} size="xs" className="-my-1 max-sm:-my-3 max-sm:size-11" />}
</dd>
</div>
))}
</dl>
</div>
);
}
/* ---------- grid: tiles on a hairline grid ---------- */
if (variant === 'grid') {
const cols = { 2: 'sm:grid-cols-2', 3: 'sm:grid-cols-2 lg:grid-cols-3', 4: 'sm:grid-cols-2 lg:grid-cols-4' }[columns];
// Fill the last row with empty tiles, so the hairlines meet the edge everywhere and no grey block shows
const fillSm = (2 - (items.length % 2)) % 2;
const fillLg = (columns - (items.length % columns)) % columns;
const fillers = Array.from({ length: Math.max(fillSm, fillLg) }, (_, k) => cn('hidden bg-card', k < fillSm && 'sm:block', columns > 2 && (k < fillLg ? 'lg:block' : 'lg:hidden')));
return (
<div ref={ref} className={cn('w-full', className)}>
{head}
<dl className={cn('grid grid-cols-[minmax(0,1fr)] gap-px overflow-hidden rounded-xl border border-border bg-border', cols)}>
{items.map((item, i) => (
<div key={i} className="group/tile relative min-w-0 bg-card p-4 transition-colors duration-200 hover:bg-[color-mix(in_oklab,var(--foreground)_2.5%,var(--card))] sm:p-5">
<div {...rise(i)}>
<Term item={item} />
<Detail item={item} strong className="mt-1.5 text-[15px]" />
</div>
</div>
))}
{fillers.map((c, k) => (
<div key={`f${k}`} aria-hidden className={c} />
))}
</dl>
</div>
);
}
/* ---------- spec: groups with zebra rows ---------- */
if (variant === 'spec') {
// One dl per group: a heading inside a dl is not allowed, so it sits above its own list
const groups: { name?: string; rows: { item: DescriptionItem; i: number }[] }[] = [];
items.forEach((item, i) => {
const last = groups[groups.length - 1];
if (!last || (item.group && item.group !== last.name)) groups.push({ name: item.group ?? last?.name, rows: [{ item, i }] });
else last.rows.push({ item, i });
});
return (
<div ref={ref} id={listId} className={cn('w-full', className)}>
{head}
{groups.map((g, gi) => {
const rows = g.rows.filter((r) => !folded(r.i));
if (!rows.length) return null;
return (
<div key={gi}>
{g.name && <p className={cn('px-3 pb-2 text-[11px] font-medium uppercase tracking-[0.09em] text-subtle-foreground', gi > 0 ? 'pt-6' : 'pt-1')}>{g.name}</p>}
<dl className="grid grid-cols-[minmax(0,1fr)]">
{/* Zebra counts within its group, so every group starts on a light row */}
{rows.map(({ item, i }, k) => (
<div
key={i}
{...rise(i)}
className={cn(
rise(i).className,
'grid grid-cols-[minmax(0,1fr)] items-start gap-0.5 rounded-md px-3 py-2.5 sm:grid-cols-[minmax(0,2fr)_minmax(0,3fr)] sm:gap-4 starting:opacity-0',
k % 2 === 1 && 'bg-[color-mix(in_oklab,var(--foreground)_3.5%,transparent)]',
)}
>
<Term item={item} />
<Detail item={item} />
</div>
))}
</dl>
</div>
);
})}
{more}
</div>
);
}
/* ---------- columns: term left, value right ---------- */
if (variant === 'columns') {
return (
<div ref={ref} className={cn('w-full', className)}>
{head}
<dl id={listId} className="grid grid-cols-[minmax(0,1fr)] border-t border-border">
{items.map((item, i) =>
folded(i) ? null : (
<div
key={i}
{...rise(i)}
className={cn(rise(i).className, 'grid gap-1 border-b border-border py-3.5 sm:grid-cols-[minmax(0,1fr)_minmax(0,2fr)] sm:gap-6 sm:py-4 starting:opacity-0')}
>
<Term item={item} className="sm:pt-px" />
<Detail item={item} />
</div>
),
)}
</dl>
{more}
</div>
);
}
/* ---------- stacked: term above value ---------- */
return (
<div ref={ref} className={cn('w-full', className)}>
{head}
<dl id={listId} className="grid grid-cols-[minmax(0,1fr)] divide-y divide-border">
{items.map((item, i) =>
folded(i) ? null : (
<div key={i} {...rise(i)} className={cn(rise(i).className, 'py-3.5 starting:opacity-0')}>
<Term item={item} />
<Detail item={item} className="mt-1" strong />
</div>
),
)}
</dl>
{more}
</div>
);
}Häufige Fragen
Wie installiere ich Beschreibungsliste?
Mit aiw add @ai-website/description-list. Der Befehl legt die Datei components/ui/description-list.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind icon und copy-button. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Beschreibungsliste?
5 Varianten, Gestapelt, Zwei Spalten, Raster, Metazeile und Datenblatt. Du wählst sie mit der Prop variant, zum Beispiel variant="grid".
Kann ich die Texte von Beschreibungsliste übersetzen?
Ja. Beschreibungsliste bringt 3 eigene Texte auf Deutsch und Englisch mit, etwa „Alle zeigen“, „Weniger zeigen“ und „Kopieren“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.
Kann ein KI-Assistent Beschreibungsliste einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Beschreibungsliste mit aiw_components und installieren sie mit aiw_add @ai-website/description-list. Nenn im Prompt die Variante, etwa variant="columns", dann schreibt der Assistent deine Inhalte direkt in die Props.
Kann ich Beschreibungsliste ohne Code anpassen?
Ja, im Studio. Alle 5 Varianten lassen sich dort bearbeiten, Texte, Listen und Schalter. Das Studio schreibt den passenden DescriptionList-Code zum Kopieren.