Zum Inhalt springen
UI Kit
Accent

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

5 Variants

Variants

Gestapelt

Begriff über Wert, für schmale Seitenpanels wie eine Bestellübersicht. Ein Statuspunkt, eine zweite Zeile, ein Kopierknopf, der zum Haken wird, und ein Link zur Rechnung.

Im Studio öffnen

Zwei Spalten

Begriff links, Wert rechts auf Haarlinien, auf dem Telefon einspaltig. Für Profil- und Einstellungsseiten, lange Listen klappen hinter Alle zeigen.

Im Studio öffnen

Raster

Eckdaten als Kacheln auf einem Raster aus Haarlinien, mit Icon und leiser zweiter Zeile. Passt zu Exposés, Fallstudien und Produktseiten, die Kacheln steigen beim Hereinscrollen auf.

Im Studio öffnen

Metazeile

Die Zeile mit Fakten unter einer Seitenüberschrift, getrennt durch Haarlinien. Auf dem Telefon wird sie ein zweispaltiges Raster, die Rechnungsnummer kopiert sich mit einem Tipp.

Im Studio öffnen

Datenblatt

Gruppierte Daten mit sanften Zebrazeilen für Produktseiten. Die ersten Zeilen stehen da, Alle zeigen klappt den Rest auf, neue Zeilen blenden ein.

Im Studio öffnen

Details

Was Beschreibungsliste mitbringt, aus Registry und Quellcode gelesen.

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

Props

Die Schnittstelle DescriptionListProps aus components/ui/description-list.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.

  • variantoptional
    DescriptionListVariant

    stackedcolumnsgridinlinespec

  • itemsPflicht
    DescriptionItem[]
  • titleoptional
    string

    A heading above the list

  • descriptionoptional
    string

    A line under the heading

  • actionoptional
    React.ReactNode

    An edit link or button at the end of the heading row

  • columnsoptional
    2 | 3 | 4

    grid: columns on large screens. Default 3.

  • visibleoptional
    number

    spec 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
moreEnglisch Show allDeutsch Alle zeigen
lessEnglisch Show fewerDeutsch Weniger zeigen
copyEnglisch CopyDeutsch 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
components/ui/description-list.tsx
'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.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert