Linkliste
Listen von Links: ein Seitenmenü mit aktivem Eintrag, gruppierte Einstellungszeilen und eine ruhige Ressourcenliste.
Eine von 6 Components in Navigation
Details
Was Linkliste mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Navigation
- Bereich
- Elements
- Variants
- 3
- Version
- 1.0.0 · seit 1.0.0
- Datei
- components/ui/link-list.tsx
- Bringt mit
- utilskiticon
- npm-Pakete
- Keine
- Sprachen
- Texte kommen über Props
- Studio
- Im Code anpassen
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle LinkListProps aus components/ui/link-list.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
sectionsPflichtLinkListSection[]variantoptional'nav' | 'rows' | 'plain'nav: a side menu with an active item. rows: grouped rows with chevrons, like phone settings. plain: a quiet list of links with arrows.
labeloptionalstringAccessible name when it is a navigation
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Setzt aria-current und aria-label, damit Screenreader Zweck und Zustand ansagen.
- Zeigt bei Bedienung mit der Tastatur einen sichtbaren Fokusring.
- Kommt ohne Client-State aus. Der Server schreibt sie vollständig, sie ist auch ohne JavaScript lesbar.
Bewegung
- Bringt eigene Animationen mit, ruhig getaktet und ohne Wackeln.
- Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 4 Stellen im Code schalten Bewegung dann ab.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 184 Zeilen
'use client';
import { useRef, useState } from 'react';
import { cn, prefersReducedMotion, useInView } from '@/lib/kit';
import { Icon, type IconName } from '@/components/ui/icon';
export interface LinkListItem {
label: string;
href: string;
icon?: IconName;
description?: React.ReactNode;
/** A count or short status at the end */
badge?: React.ReactNode;
/** Text on the right, e.g. the current value of a setting */
value?: React.ReactNode;
active?: boolean;
external?: boolean;
destructive?: boolean;
}
export interface LinkListSection {
title?: string;
items: LinkListItem[];
}
export interface LinkListProps {
sections: LinkListSection[];
/** nav: a side menu with an active item. rows: grouped rows with chevrons, like phone settings. plain: a quiet list of links with arrows. */
variant?: 'nav' | 'rows' | 'plain';
/** Accessible name when it is a navigation */
label?: string;
className?: string;
}
/* Rows rise one after another when the list scrolls into view */
const STYLES =
'.kitfx-ll-in .kitfx-ll-row{animation:kitfx-ll-in .55s var(--ease-kit) both;animation-delay:calc(var(--i,0) * 45ms)}' +
'@keyframes kitfx-ll-in{from{opacity:0;transform:translateY(8px)}}' +
'.kitfx-ll-in .kitfx-ll-bar{animation:kitfx-ll-bar .5s cubic-bezier(.34,1.56,.64,1) both;animation-delay:calc(var(--i,0) * 45ms + 250ms)}' +
'@keyframes kitfx-ll-bar{from{transform:scaleY(0)}}' +
'@media (prefers-reduced-motion:reduce){.kitfx-ll-in .kitfx-ll-row,.kitfx-ll-in .kitfx-ll-bar{animation:none}}';
/** In a side menu one soft surface follows the pointer and the focus from row to row */
function useGlide() {
const [glide, setGlide] = useState<{ y: number; h: number; on: boolean; jump: boolean } | null>(null);
const to = (el: HTMLElement | null) => {
if (!el || prefersReducedMotion()) return;
const y = el.offsetTop;
const h = el.offsetHeight;
setGlide((g) => ({ y, h, on: true, jump: !g || !g.on }));
};
const off = () => setGlide((g) => (g ? { ...g, on: false } : g));
return [glide, to, off] as const;
}
function NavSection({ items, start }: { items: LinkListItem[]; start: number }) {
const [glide, to, off] = useGlide();
return (
<ul className="relative isolate grid gap-0.5" onPointerLeave={off} onBlur={(e) => !e.currentTarget.contains(e.relatedTarget as Node) && off()}>
{glide && (
<span
aria-hidden
className={cn(
'pointer-events-none absolute inset-x-0 top-0 -z-10 rounded-lg bg-[color-mix(in_oklab,var(--accent)_70%,transparent)] transition-[opacity,transform] ease-[var(--ease-kit)]',
glide.jump ? 'duration-150 [transition-property:opacity]' : 'duration-300',
glide.on ? 'opacity-100' : 'opacity-0',
)}
style={{ height: glide.h, transform: `translateY(${glide.y}px)` }}
/>
)}
{items.map((it, i) => (
<li
key={it.label}
className="kitfx-ll-row"
style={{ '--i': start + i } as React.CSSProperties}
onPointerEnter={(e) => e.pointerType === 'mouse' && to(e.currentTarget)}
onFocus={(e) => to(e.currentTarget)}
>
<Row it={it} variant="nav" order={start + i} />
</li>
))}
</ul>
);
}
function Row({ it, variant, order }: { it: LinkListItem; variant: 'nav' | 'rows' | 'plain'; order: number }) {
return (
<a
href={it.href}
aria-current={it.active ? 'page' : undefined}
{...(it.external ? { target: '_blank', rel: 'noreferrer' } : {})}
className={cn(
'group/link flex items-center gap-3 outline-none transition-colors focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring',
variant === 'nav' && cn('relative min-h-10 rounded-lg px-3 text-sm max-md:min-h-11', it.active ? 'bg-accent font-medium text-foreground' : 'text-muted-foreground hover:text-foreground'),
variant === 'rows' && cn('min-h-[3.25rem] px-4 py-2.5 text-[15px] hover:bg-[color-mix(in_oklab,var(--accent)_60%,transparent)]', it.destructive ? 'text-destructive' : 'text-foreground'),
variant === 'plain' && 'min-h-14 py-3 text-[15px] text-foreground hover:text-foreground',
)}
>
{variant === 'nav' && it.active && <span aria-hidden className="kitfx-ll-bar absolute inset-y-2.5 start-1 w-[3px] origin-center rounded-full bg-primary" style={{ '--i': order } as React.CSSProperties} />}
{it.icon &&
(variant === 'rows' ? (
<span className={cn('grid size-8 shrink-0 place-items-center rounded-lg border border-border bg-surface transition-[scale,color] duration-300 ease-[cubic-bezier(0.34,1.56,0.64,1)] group-hover/link:scale-105 motion-reduce:transition-none', it.destructive ? 'text-destructive' : 'text-muted-foreground group-hover/link:text-foreground')}>
<Icon name={it.icon} size={16} />
</span>
) : (
<Icon name={it.icon} size={17} className={cn('shrink-0 transition-[color,translate] duration-200 ease-[var(--ease-kit)] group-hover/link:translate-x-0.5 motion-reduce:transition-none', it.active ? 'text-foreground' : 'text-subtle-foreground group-hover/link:text-muted-foreground')} />
))}
<span className="min-w-0 flex-1">
<span className={cn('block truncate', variant === 'plain' && 'font-medium')}>
{variant === 'plain' ? (
// The underline grows from the start under the label
<span className="relative after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:h-px after:origin-left after:scale-x-0 after:bg-current after:opacity-50 after:transition-transform after:duration-300 after:ease-[var(--ease-kit)] group-hover/link:after:scale-x-100 group-focus-visible/link:after:scale-x-100 motion-reduce:after:transition-none">{it.label}</span>
) : (
it.label
)}
</span>
{it.description && <span className="mt-0.5 block text-pretty text-[13px] font-normal text-muted-foreground">{it.description}</span>}
</span>
{it.value && <span className="max-w-[45%] shrink-0 truncate text-sm text-muted-foreground">{it.value}</span>}
{it.badge !== undefined && (
<span className={cn('grid h-5 min-w-5 shrink-0 place-items-center rounded-full px-1.5 text-[11px] font-semibold tabular-nums', it.active ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground')}>{it.badge}</span>
)}
{variant !== 'nav' && (
<Icon
name={it.external ? 'arrow-up-right' : variant === 'rows' ? 'chevron-right' : 'arrow-right'}
size={16}
className={cn(
'shrink-0 text-subtle-foreground transition-[translate,color] duration-300 ease-[var(--ease-kit)] group-hover/link:text-foreground group-focus-visible/link:text-foreground motion-reduce:transition-none',
it.external ? 'group-hover/link:-translate-y-0.5 group-hover/link:translate-x-0.5' : 'group-hover/link:translate-x-1 group-focus-visible/link:translate-x-1',
)}
/>
)}
</a>
);
}
/**
* Lists of links: side navigation, settings rows or a resource list. Rows arrive one after another,
* arrows glide on hover, in the side menu a soft surface follows the pointer.
* <LinkList variant="rows" sections={[{ title: 'Account', items: [{ label: 'Profile', href: '#', icon: 'user', value: 'Mia' }] }]} />
*/
export function LinkList({ sections, variant = 'nav', label, className }: LinkListProps) {
const Wrap = (variant === 'nav' ? 'nav' : 'div') as 'nav';
const [ref, inView] = useInView<HTMLElement>();
const waited = useRef(false);
if (!inView) waited.current = true;
const entering = inView && waited.current;
let order = 0;
return (
<Wrap
ref={ref as React.Ref<HTMLElement>}
aria-label={variant === 'nav' ? label : undefined}
className={cn('grid w-full grid-cols-[minmax(0,1fr)]', variant === 'rows' ? 'gap-7' : 'gap-6', !inView && '[&_.kitfx-ll-row]:opacity-0', entering && 'kitfx-ll-in', className)}
>
<style href="kitfx-ll" precedence="default">
{STYLES}
</style>
{sections.map((sec, si) => {
const start = order;
order += sec.items.length + (sec.title ? 1 : 0);
return (
<div key={sec.title ?? si}>
{sec.title && (
<p className={cn('kitfx-ll-row text-[11px] font-medium uppercase tracking-[0.08em] text-subtle-foreground', variant === 'rows' ? 'mb-2 px-4' : 'mb-2 px-3')} style={{ '--i': start } as React.CSSProperties}>
{sec.title}
</p>
)}
{variant === 'nav' ? (
<NavSection items={sec.items} start={start + (sec.title ? 1 : 0)} />
) : (
<ul className={cn(variant === 'rows' && 'divide-y divide-border overflow-hidden rounded-xl border border-border bg-card shadow-[var(--highlight),var(--shadow-sm)]', variant === 'plain' && 'divide-y divide-border border-y border-border')}>
{sec.items.map((it, i) => (
<li key={it.label} className="kitfx-ll-row" style={{ '--i': start + (sec.title ? 1 : 0) + i } as React.CSSProperties}>
<Row it={it} variant={variant} order={start + i} />
</li>
))}
</ul>
)}
</div>
);
})}
</Wrap>
);
}Häufige Fragen
Wie installiere ich Linkliste?
Mit aiw add @ai-website/link-list. Der Befehl legt die Datei components/ui/link-list.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind icon. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Linkliste?
3 Varianten, Seitenmenü, Einstellungszeilen und Ressourcenliste. Du wählst sie mit der Prop variant, zum Beispiel variant="plain".
Kann ein KI-Assistent Linkliste einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Linkliste mit aiw_components und installieren sie mit aiw_add @ai-website/link-list. Nenn im Prompt die Variante, etwa variant="rows", dann schreibt der Assistent deine Inhalte direkt in die Props.