Zum Inhalt springen
UI Kit
Accent

Baumansicht

Verschachtelte Listen, die auf- und zugehen, als Dateien mit Änderungsmarken, Doku-Navigation, Rechte mit gemischten Häkchen oder Gliederung mit Leitlinien. Volle Tastatur nach dem Baum-Muster.

Eine von 4 Components in Listen, Bäume und Timelines

4 Variants

Variants

Dateien

Ordner öffnen sich mit weicher Höhe, Dateien tragen Größe und Änderungsmarke (neu, geändert, gelöscht). Pfad und gezeichnete Datei folgen der Auswahl, Pfeile und Buchstaben bewegen durch den Baum.

Im Studio öffnen

Navigation

Eine Doku-Seitenleiste. Abschnitte klappen auf, Seiten sind Links, die aktuelle Seite trägt einen Balken auf der Leitlinie und aria-current. Der Artikel daneben wechselt mit weicher Unschärfe.

Im Studio öffnen

Häkchen

Rechte einer Rolle. Ein Zweig hakt alle Kinder ab, ein teilweise gewählter Zweig zeigt einen Strich, gesperrte Rechte bleiben sichtbar. Alle öffnen und schließen stehen darüber, ein Zähler folgt.

Im Studio öffnen

Gliederung

Leitlinien verbinden jede Ebene, Anzahlen stehen am Ende. Die Wahl eines Teams aktualisiert die Karte daneben, die beim Scrollen der Gliederung stehen bleibt.

Im Studio öffnen

Details

Was Baumansicht mitbringt, aus Registry und Quellcode gelesen.

Bereich
Elements
Variants
4
Version
1.0.0 · seit 1.1.0
Datei
components/ui/tree-view.tsx
Bringt mit
utilskiticon
npm-Pakete
Keine
Sprachen
Englisch und Deutsch, 8 eigene Texte

Props

Die Schnittstelle TreeViewProps aus components/ui/tree-view.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.

  • variantoptional
    TreeViewVariant

    filesnavigationcheckboxoutline

  • itemsPflicht
    TreeNode[]
  • labeloptional
    string

    Accessible name of the tree

  • defaultExpandedoptional
    string[]

    Ids of open branches at the start

  • expandedoptional
    string[]
  • onExpandedChangeoptional
    (ids: string[]) => void
  • defaultSelectedoptional
    string

    The chosen node (files, outline) or the current page (navigation)

  • selectedoptional
    string
  • onSelectoptional
    (id: string, node: TreeNode) => void
  • defaultCheckedoptional
    string[]

    checkbox: ids of checked leaves

  • checkedoptional
    string[]
  • onCheckedChangeoptional
    (ids: string[]) => void
  • guidesoptional
    boolean

    Lines that connect children to their parent. Default on in outline.

  • toolbaroptional
    boolean

    A bar above the tree with open all and close all

Eingebaute Texte

Diese Texte schreibt Baumansicht selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.

Alle 8 Texte anzeigen
treeEnglisch TreeDeutsch Baum
expandAllEnglisch Open allDeutsch Alle öffnen
collapseAllEnglisch Close allDeutsch Alle schließen
addedEnglisch addedDeutsch neu
modifiedEnglisch modifiedDeutsch geändert
deletedEnglisch deletedDeutsch gelöscht
selectedEnglisch selectedDeutsch ausgewählt
itemsEnglisch itemsDeutsch Einträge

Barrierefreiheit

Aus dem Quellcode der Component abgeleitet.

  • Baumansicht reagiert selbst auf die Tasten Enter, Pfeil hoch, Pfeil runter, Pfeil links, Pfeil rechts, Pos1, Ende und Leertaste.
  • Setzt aria-checked, aria-current, aria-disabled, aria-expanded, aria-label, aria-level, aria-live, aria-multiselectable, aria-posinset, aria-selected und aria-setsize, damit Screenreader Zweck und Zustand ansagen.
  • Nutzt die Rollen group, tree und treeitem.
  • Meldet Änderungen über eine Live-Region, ohne den Fokus zu verschieben.
  • Ergänzt Text für Screenreader, wo Icons und Grafiken allein stehen.
  • Zeigt bei Bedienung mit der Tastatur einen sichtbaren Fokusring.
  • Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.

Bewegung

  • Bringt eigene Animationen mit, ruhig getaktet und ohne Wackeln.
  • Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 3 Stellen im Code schalten Bewegung dann ab.

Quellcode

Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.

Datei anzeigen, 402 Zeilen
components/ui/tree-view.tsx
'use client';

import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
import { cn, useInView, useTexts } from '@/lib/kit';
import { Icon, type IconName } from '@/components/ui/icon';

/*
 * A tree after the WAI-ARIA APG tree view pattern: one tab stop, arrows move,
 * Right opens or steps into a branch, Left closes or steps to the parent,
 * Home and End jump, * opens all siblings, typing a letter jumps to the next
 * match, Enter or Space selects (or checks). Branches open with a soft
 * height change. Four looks: a file tree with change marks, a docs
 * navigation with links, a permission tree with checkboxes that know their
 * mixed state, and an outline with guide lines and counts.
 */

const TEXTS = {
  en: { tree: 'Tree', expandAll: 'Open all', collapseAll: 'Close all', added: 'added', modified: 'modified', deleted: 'deleted', selected: 'selected', items: 'items' },
  de: { tree: 'Baum', expandAll: 'Alle öffnen', collapseAll: 'Alle schließen', added: 'neu', modified: 'geändert', deleted: 'gelöscht', selected: 'ausgewählt', items: 'Einträge' },
};

export interface TreeNode {
  id: string;
  label: string;
  /** Icon before the label. Files and folders get one by default in the files look. */
  icon?: IconName;
  /** Quiet text at the end, e.g. a size, a count or a role */
  meta?: string;
  /** files: a change mark */
  status?: 'added' | 'modified' | 'deleted';
  /** navigation: the page this node links to */
  href?: string;
  /** checkbox: shown but cannot be changed */
  disabled?: boolean;
  children?: TreeNode[];
}

export type TreeViewVariant = 'files' | 'navigation' | 'checkbox' | 'outline';

export interface TreeViewProps {
  variant?: TreeViewVariant;
  items: TreeNode[];
  /** Accessible name of the tree */
  label?: string;
  /** Ids of open branches at the start */
  defaultExpanded?: string[];
  expanded?: string[];
  onExpandedChange?: (ids: string[]) => void;
  /** The chosen node (files, outline) or the current page (navigation) */
  defaultSelected?: string;
  selected?: string;
  onSelect?: (id: string, node: TreeNode) => void;
  /** checkbox: ids of checked leaves */
  defaultChecked?: string[];
  checked?: string[];
  onCheckedChange?: (ids: string[]) => void;
  /** Lines that connect children to their parent. Default on in outline. */
  guides?: boolean;
  /** A bar above the tree with open all and close all */
  toolbar?: boolean;
  labels?: Partial<(typeof TEXTS)['en']>;
  className?: string;
}

interface Flat {
  node: TreeNode;
  level: number;
  parent: string | null;
  posinset: number;
  setsize: number;
}

/** Visible rows in order: only children of open branches */
function flatten(items: TreeNode[], open: Set<string>, level = 1, parent: string | null = null, out: Flat[] = []) {
  items.forEach((node, i) => {
    out.push({ node, level, parent, posinset: i + 1, setsize: items.length });
    if (node.children?.length && open.has(node.id)) flatten(node.children, open, level + 1, node.id, out);
  });
  return out;
}

const leavesOf = (n: TreeNode): TreeNode[] => (n.children?.length ? n.children.flatMap(leavesOf) : [n]);
const allBranches = (items: TreeNode[]): string[] => items.flatMap((n) => (n.children?.length ? [n.id, ...allBranches(n.children)] : []));
const findPath = (items: TreeNode[], id: string, path: string[] = []): string[] | null => {
  for (const n of items) {
    if (n.id === id) return path;
    if (n.children) {
      const p = findPath(n.children, id, [...path, n.id]);
      if (p) return p;
    }
  }
  return null;
};

const FILE_ICON: Record<string, IconName> = { tsx: 'file-code', ts: 'file-code', js: 'file-code', css: 'file-code', json: 'file-code', md: 'file', png: 'image', jpg: 'image', svg: 'image', pdf: 'file' };
const fileIcon = (n: TreeNode, open: boolean): IconName => n.icon ?? (n.children ? (open ? 'folder-open' : 'folder') : FILE_ICON[n.label.split('.').pop() ?? ''] ?? 'file');

const STATUS_MARK: Record<string, { letter: string; tone: string }> = {
  added: { letter: 'A', tone: 'text-success-foreground' },
  modified: { letter: 'M', tone: 'text-warning-foreground' },
  deleted: { letter: 'D', tone: 'text-destructive' },
};

/** The checkbox state of a node: leaves by their id, branches from their leaves */
function checkState(n: TreeNode, checked: Set<string>): 'on' | 'off' | 'mixed' {
  if (!n.children?.length) return checked.has(n.id) ? 'on' : 'off';
  const leaves = leavesOf(n);
  const on = leaves.filter((l) => checked.has(l.id)).length;
  return on === 0 ? 'off' : on === leaves.length ? 'on' : 'mixed';
}

function Box({ state, disabled }: { state: 'on' | 'off' | 'mixed'; disabled?: boolean }) {
  const on = state !== 'off';
  return (
    <span
      aria-hidden
      className={cn(
        'grid size-[17px] shrink-0 place-items-center rounded-[5px] border transition-[background-color,border-color] duration-200 motion-reduce:transition-none',
        on ? 'border-primary bg-primary text-primary-foreground' : 'border-border-strong bg-background',
        disabled && 'opacity-50',
      )}
    >
      <svg viewBox="0 0 24 24" width={12} height={12} fill="none" className="overflow-visible">
        <path d="m5 12.5 4.5 4.5L19 7.5" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" className={cn('transition-opacity duration-150', state === 'on' ? 'opacity-100' : 'opacity-0')} />
        <path d="M6 12h12" stroke="currentColor" strokeWidth="3" strokeLinecap="round" className={cn('transition-opacity duration-150', state === 'mixed' ? 'opacity-100' : 'opacity-0')} />
      </svg>
    </span>
  );
}

/**
 * A tree of files, pages, permissions or an outline, with full keyboard support.
 *   <TreeView label="Files" items={files} defaultExpanded={['src']} defaultSelected="src/app.tsx" />
 *   <TreeView variant="navigation" label="Docs" items={pages} selected="install" />
 *   <TreeView variant="checkbox" label="Permissions" items={rights} defaultChecked={['read']} />
 *   <TreeView variant="outline" label="Teams" items={org} />
 */
export function TreeView({
  variant = 'files', items, label, defaultExpanded, expanded: expandedProp, onExpandedChange, defaultSelected, selected: selectedProp, onSelect,
  defaultChecked, checked: checkedProp, onCheckedChange, guides, toolbar, labels, className,
}: TreeViewProps) {
  const t = useTexts(TEXTS, labels);
  const baseId = useId();
  // The current page opens its parents by itself
  const [innerOpen, setInnerOpen] = useState<string[]>(() => {
    const start = new Set(defaultExpanded ?? []);
    const sel = selectedProp ?? defaultSelected;
    if (sel) findPath(items, sel)?.forEach((id) => start.add(id));
    return [...start];
  });
  const openIds = expandedProp ?? innerOpen;
  const open = useMemo(() => new Set(openIds), [openIds]);
  const [innerSel, setInnerSel] = useState(defaultSelected);
  const selected = selectedProp ?? innerSel;
  const [innerChecked, setInnerChecked] = useState<string[]>(defaultChecked ?? []);
  const checked = useMemo(() => new Set(checkedProp ?? innerChecked), [checkedProp, innerChecked]);
  const rows = useMemo(() => flatten(items, open), [items, open]);
  const [focusId, setFocusId] = useState<string | null>(null);
  const tabId = focusId && rows.some((r) => r.node.id === focusId) ? focusId : selected && rows.some((r) => r.node.id === selected) ? selected : rows[0]?.node.id;
  const treeRef = useRef<HTMLUListElement>(null);
  const typed = useRef({ text: '', at: 0 });
  const [said, setSaid] = useState('');
  const [ref, inView] = useInView<HTMLDivElement>();
  const showGuides = guides ?? variant === 'outline';
  const [settled, setSettled] = useState(false);
  useEffect(() => {
    if (!inView) return;
    const id = window.setTimeout(() => setSettled(true), 900);
    return () => window.clearTimeout(id);
  }, [inView]);

  const setOpen = useCallback(
    (next: string[]) => {
      setInnerOpen(next);
      onExpandedChange?.(next);
    },
    [onExpandedChange],
  );
  const toggleOpen = (id: string, to?: boolean) => {
    const isOpen = open.has(id);
    const want = to ?? !isOpen;
    if (want === isOpen) return;
    setOpen(want ? [...openIds, id] : openIds.filter((x) => x !== id));
  };
  const focusRow = (id: string) => {
    setFocusId(id);
    requestAnimationFrame(() => document.getElementById(`${baseId}-${id}`)?.focus());
  };
  const select = (n: TreeNode) => {
    setInnerSel(n.id);
    onSelect?.(n.id, n);
  };
  const toggleCheck = (n: TreeNode) => {
    if (n.disabled) return;
    const leaves = leavesOf(n).filter((l) => !l.disabled);
    const state = checkState(n, checked);
    const next = new Set(checked);
    leaves.forEach((l) => (state === 'on' ? next.delete(l.id) : next.add(l.id)));
    const list = [...next];
    setInnerChecked(list);
    onCheckedChange?.(list);
    setSaid(`${n.label}: ${state === 'on' ? '0' : leaves.length}/${leaves.length}`);
  };
  const activate = (n: TreeNode) => {
    if (variant === 'checkbox') toggleCheck(n);
    else if (variant === 'navigation' && n.children?.length) toggleOpen(n.id);
    else {
      select(n);
      if (n.children?.length) toggleOpen(n.id);
    }
  };

  const onKeyDown = (e: React.KeyboardEvent, r: Flat, i: number) => {
    const n = r.node;
    const branch = Boolean(n.children?.length);
    const go = (j: number) => {
      e.preventDefault();
      const row = rows[Math.max(0, Math.min(rows.length - 1, j))];
      if (row) focusRow(row.node.id);
    };
    switch (e.key) {
      case 'ArrowDown': return go(i + 1);
      case 'ArrowUp': return go(i - 1);
      case 'Home': return go(0);
      case 'End': return go(rows.length - 1);
      case 'ArrowRight':
        e.preventDefault();
        if (branch && !open.has(n.id)) toggleOpen(n.id, true);
        else if (branch) go(i + 1);
        return;
      case 'ArrowLeft':
        e.preventDefault();
        if (branch && open.has(n.id)) toggleOpen(n.id, false);
        else if (r.parent) focusRow(r.parent);
        return;
      case '*': {
        e.preventDefault();
        const siblings = rows.filter((x) => x.parent === r.parent && x.node.children?.length).map((x) => x.node.id);
        setOpen([...new Set([...openIds, ...siblings])]);
        return;
      }
      case 'Enter':
      case ' ':
        e.preventDefault();
        // A page of the navigation opens its link
        if (variant === 'navigation' && n.href && !branch) {
          select(n);
          return (e.currentTarget as HTMLElement).querySelector<HTMLAnchorElement>(':scope > a')?.click();
        }
        return activate(n);
    }
    // Type to jump: the next row that starts with the typed letters
    if (e.key.length === 1 && /\S/.test(e.key) && !e.ctrlKey && !e.metaKey && !e.altKey) {
      const now = Date.now();
      typed.current = { text: now - typed.current.at < 600 ? typed.current.text + e.key.toLowerCase() : e.key.toLowerCase(), at: now };
      const q = typed.current.text;
      const order = [...rows.slice(i + 1), ...rows.slice(0, i + 1)];
      const hit = order.find((x) => x.node.label.toLowerCase().startsWith(q));
      if (hit) focusRow(hit.node.id);
    }
  };

  /* ---------- one row ---------- */
  const renderNode = (node: TreeNode, level: number, parent: string | null, posinset: number, setsize: number, index: { i: number }): React.ReactNode => {
    const branch = Boolean(node.children?.length);
    const isOpen = branch && open.has(node.id);
    const i = rows.findIndex((r) => r.node.id === node.id);
    const flat = rows[i];
    const isSel = selected === node.id;
    const state = variant === 'checkbox' ? checkState(node, checked) : null;
    // The first entrance is staggered, branches opened later come in at once
    const delay = settled ? 0 : Math.min(index.i++, 14) * 28;
    const indent = variant === 'outline' ? 20 : 16;
    const pad = 8 + (level - 1) * indent;
    const Tag = variant === 'navigation' && node.href && !branch ? 'a' : 'div';

    const content = (
      <>
        {/* Chevron for branches, a spacer for leaves, so labels line up */}
        <span aria-hidden className="grid size-4 shrink-0 place-items-center text-subtle-foreground">
          {branch && <Icon name="chevron-right" size={14} strokeWidth={2} className={cn('transition-[rotate] duration-300 ease-[var(--ease-kit)] motion-reduce:transition-none', isOpen && 'rotate-90')} />}
        </span>
        {state && <Box state={state} disabled={node.disabled} />}
        {variant === 'files' && <Icon name={fileIcon(node, isOpen)} size={16} className={cn('shrink-0', branch ? 'text-[color-mix(in_oklab,var(--primary)_70%,var(--muted-foreground))]' : 'text-muted-foreground')} />}
        {variant !== 'files' && node.icon && <Icon name={node.icon} size={16} className="shrink-0 text-muted-foreground" />}
        <span className={cn('min-w-0 flex-1 truncate', node.status === 'deleted' && 'text-muted-foreground line-through decoration-[color-mix(in_oklab,var(--destructive)_60%,transparent)]', variant === 'outline' && level === 1 && 'font-medium')}>{node.label}</span>
        {node.meta && <span className="shrink-0 text-xs tabular-nums text-subtle-foreground">{node.meta}</span>}
        {node.status && (
          <span className={cn('w-3 shrink-0 text-center text-[11px] font-semibold', STATUS_MARK[node.status].tone)}>
            {STATUS_MARK[node.status].letter}
            <span className="sr-only">, {t[node.status]}</span>
          </span>
        )}
      </>
    );

    return (
      <li
        key={node.id}
        id={`${baseId}-${node.id}`}
        role="treeitem"
        aria-level={level}
        aria-setsize={setsize}
        aria-posinset={posinset}
        aria-expanded={branch ? isOpen : undefined}
        aria-selected={variant === 'checkbox' ? undefined : variant === 'navigation' ? undefined : isSel}
        aria-current={variant === 'navigation' && isSel ? 'page' : undefined}
        aria-checked={state ? (state === 'mixed' ? 'mixed' : state === 'on') : undefined}
        aria-disabled={node.disabled || undefined}
        tabIndex={node.id === tabId ? 0 : -1}
        onFocus={(e) => {
          if (e.target === e.currentTarget) setFocusId(node.id);
        }}
        onKeyDown={(e) => {
          if (e.target !== e.currentTarget && !(e.target as HTMLElement).matches('a')) return;
          e.stopPropagation();
          if (flat) onKeyDown(e, flat, i);
        }}
        className="group/node relative outline-none"
      >
        <Tag
          {...(Tag === 'a' ? { href: node.href, tabIndex: -1 } : {})}
          onClick={(e: React.MouseEvent) => {
            e.stopPropagation();
            if (Tag === 'a') {
              select(node);
              return;
            }
            setFocusId(node.id);
            activate(node);
          }}
          style={{ paddingInlineStart: pad, '--d': `${delay}ms` } as React.CSSProperties}
          className={cn(
            'kitfx-tree-row relative flex min-h-11 cursor-default select-none items-center gap-2 rounded-md pe-2.5 text-sm text-foreground transition-[background-color,color] duration-150 sm:min-h-8',
            'hover:bg-accent group-focus-visible/node:shadow-[inset_0_0_0_2px_color-mix(in_oklab,var(--primary)_55%,transparent)]',
            variant === 'navigation' && 'text-muted-foreground hover:text-foreground',
            variant === 'navigation' && isSel && 'font-medium text-foreground',
            variant !== 'navigation' && variant !== 'checkbox' && isSel && 'bg-[color-mix(in_oklab,var(--primary)_12%,transparent)] hover:bg-[color-mix(in_oklab,var(--primary)_16%,transparent)]',
            node.disabled && 'cursor-not-allowed opacity-60',
            Tag === 'a' && 'cursor-pointer',
            inView ? '' : 'kitfx-tree-wait',
          )}
        >
          {/* navigation: the current page gets a bar on the guide line */}
          {variant === 'navigation' && isSel && <span aria-hidden className="absolute inset-y-1.5 w-[2px] rounded-full bg-primary transition-transform duration-300 starting:scale-y-0" style={{ insetInlineStart: pad - 8 }} />}
          {content}
        </Tag>
        {branch && (
          // Grid rows from 0fr to 1fr give the branch a soft height without measuring
          <div inert={!isOpen || undefined} className={cn('grid transition-[grid-template-rows,opacity] duration-300 ease-[var(--ease-kit)] motion-reduce:transition-none', isOpen ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0')}>
            <ul role="group" className="relative min-h-0 overflow-hidden">
              {(showGuides || variant === 'navigation') && (
                <span aria-hidden className="pointer-events-none absolute bottom-1 top-0 w-px bg-border" style={{ insetInlineStart: pad + 7.5 }} />
              )}
              {node.children!.map((c, k) => renderNode(c, level + 1, node.id, k + 1, node.children!.length, index))}
            </ul>
          </div>
        )}
      </li>
    );
  };

  const counter = { i: 0 };
  const total = variant === 'checkbox' ? items.flatMap(leavesOf).length : 0;

  return (
    <div ref={ref} className={cn('w-full', className)}>
      <style href="kitfx-tree" precedence="default">{TREE_CSS}</style>
      {toolbar && (
        <div className="mb-2 flex items-center justify-between gap-2 px-1">
          <span className="text-xs text-subtle-foreground tabular-nums">
            {variant === 'checkbox' ? `${[...checked].filter((id) => items.flatMap(leavesOf).some((l) => l.id === id)).length}/${total} ${t.selected}` : `${rows.length} ${t.items}`}
          </span>
          <span className="flex gap-1">
            <button type="button" onClick={() => setOpen(allBranches(items))} className="inline-flex h-11 items-center rounded-md px-2 text-xs font-medium text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:bg-accent focus-visible:shadow-[inset_0_0_0_2px_color-mix(in_oklab,var(--primary)_55%,transparent)] sm:h-7">
              {t.expandAll}
            </button>
            <button type="button" onClick={() => setOpen([])} className="inline-flex h-11 items-center rounded-md px-2 text-xs font-medium text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:bg-accent focus-visible:shadow-[inset_0_0_0_2px_color-mix(in_oklab,var(--primary)_55%,transparent)] sm:h-7">
              {t.collapseAll}
            </button>
          </span>
        </div>
      )}
      <ul ref={treeRef} role="tree" aria-label={label ?? t.tree} aria-multiselectable={variant === 'checkbox' || undefined} className="relative">
        {items.map((n, k) => renderNode(n, 1, null, k + 1, items.length, counter))}
      </ul>
      <span aria-live="polite" className="sr-only">
        {said}
      </span>
    </div>
  );
}

/* Rows rise in one after another the first time the tree scrolls into view, and when a branch opens */
const TREE_CSS = `
@keyframes kitfx-tree-in { from { opacity: 0; transform: translateX(-6px); } }
@media (prefers-reduced-motion: no-preference) {
  .kitfx-tree-row { animation: kitfx-tree-in .42s var(--ease-kit) both; animation-delay: var(--d, 0ms); }
  .kitfx-tree-wait { animation-play-state: paused; }
}
@media (scripting: none) { .kitfx-tree-row { animation: none; } }
`;

Häufige Fragen

Wie installiere ich Baumansicht?

Mit aiw add @ai-website/tree-view. Der Befehl legt die Datei components/ui/tree-view.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind icon. Weitere npm-Pakete braucht sie nicht.

Welche Varianten hat Baumansicht?

4 Varianten, Dateien, Navigation, Häkchen und Gliederung. Du wählst sie mit der Prop variant, zum Beispiel variant="checkbox".

Kann ich die Texte von Baumansicht übersetzen?

Ja. Baumansicht bringt 8 eigene Texte auf Deutsch und Englisch mit, etwa „Baum“, „Alle öffnen“ und „Alle schließen“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.

Kann ein KI-Assistent Baumansicht einbauen?

Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Baumansicht mit aiw_components und installieren sie mit aiw_add @ai-website/tree-view. Nenn im Prompt die Variante, etwa variant="navigation", dann schreibt der Assistent deine Inhalte direkt in die Props.

Kann ich Baumansicht ohne Code anpassen?

Ja, im Studio. Alle 4 Varianten lassen sich dort bearbeiten, Texte, Listen und Schalter. Das Studio schreibt den passenden TreeView-Code zum Kopieren.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert