Leerer Zustand
Erster Start, leere Suche, Fehler und erledigter Posteingang, mit nächstem Schritt.
Eine von 4 Components in Fortschritt und Zustände
Variants
Details
Was Leerer Zustand mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Fortschritt und Zustände
- Bereich
- Elements
- Variants
- 3
- Version
- 1.0.0 · seit 1.0.0
- Datei
- components/ui/empty-state.tsx
- 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 EmptyStateProps aus components/ui/empty-state.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
iconoptionalIconNameIcon in the middle of the drawn stack, or alone when illustration is false
illustrationoptionalboolean | React.ReactNodetrue draws a stack of cards behind the icon, a node replaces it with your own image
titlePflichtReact.ReactNodedescriptionoptionalReact.ReactNodeprimaryActionoptionalEmptyStateActionsecondaryActionoptionalEmptyStateActionvariantoptionalEmptyStateVariantplaindashedcard
plain: no frame. dashed: a drop-zone like frame. card: a raised surface.
toneoptionalEmptyStateTonedefaultsuccessdestructive
Color of the icon: success for "all done", destructive for errors
sizeoptional'sm' | 'md'asoptional'h2' | 'h3' | 'h4'Heading level for the title
childrenoptionalReact.ReactNodeExtra content under the actions, e.g. suggestion chips
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- 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. 8 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 } from 'react';
import { cn, useInView } from '@/lib/kit';
import { Button, type ButtonVariant } from '@/components/ui/button';
import { Icon, type IconName } from '@/components/ui/icon';
export type EmptyStateVariant = 'plain' | 'dashed' | 'card';
export type EmptyStateTone = 'default' | 'success' | 'destructive';
export interface EmptyStateAction {
label: string;
href?: string;
onClick?: () => void;
icon?: IconName;
iconLeft?: IconName;
}
export interface EmptyStateProps {
/** Icon in the middle of the drawn stack, or alone when illustration is false */
icon?: IconName;
/** true draws a stack of cards behind the icon, a node replaces it with your own image */
illustration?: boolean | React.ReactNode;
title: React.ReactNode;
description?: React.ReactNode;
primaryAction?: EmptyStateAction;
secondaryAction?: EmptyStateAction;
/** plain: no frame. dashed: a drop-zone like frame. card: a raised surface. */
variant?: EmptyStateVariant;
/** Color of the icon: success for "all done", destructive for errors */
tone?: EmptyStateTone;
size?: 'sm' | 'md';
/** Heading level for the title */
as?: 'h2' | 'h3' | 'h4';
/** Extra content under the actions, e.g. suggestion chips */
children?: React.ReactNode;
className?: string;
}
const TONE: Record<EmptyStateTone, string> = {
default: 'text-foreground',
success: 'text-success-foreground',
destructive: 'text-[color-mix(in_oklab,var(--destructive)_88%,var(--foreground))]',
};
const TONE_BG: Record<EmptyStateTone, string> = {
default: 'bg-card',
success: 'bg-[color-mix(in_oklab,var(--success)_10%,var(--card))]',
destructive: 'bg-[color-mix(in_oklab,var(--destructive)_9%,var(--card))]',
};
/* The illustration floats quietly, its glow breathes with it. Nothing moves with reduced motion. */
const STYLES =
'.kitfx-empty-float{animation:kitfx-empty-float 5.5s ease-in-out infinite}@keyframes kitfx-empty-float{50%{transform:translateY(-5px)}}' +
'.kitfx-empty-glow{animation:kitfx-empty-glow 5.5s ease-in-out infinite}@keyframes kitfx-empty-glow{50%{opacity:.55;transform:scaleX(.86)}}' +
'@media (prefers-reduced-motion:reduce){.kitfx-empty-float,.kitfx-empty-glow{animation:none}}';
const SPRING = 'ease-[cubic-bezier(0.34,1.4,0.64,1)]';
/** Three cards fanned out behind the icon, drawn with the kit tokens. They fan open when the state scrolls in, and a little wider on hover. */
function Stack({ icon, tone, shown, entering }: { icon: IconName; tone: EmptyStateTone; shown: boolean; entering: boolean }) {
return (
<div className="relative mx-auto h-[104px] w-[150px]" aria-hidden>
<div className="kitfx-empty-glow absolute inset-x-6 bottom-0 h-10 rounded-full bg-[var(--glow)] blur-2xl" />
<div
className={cn(
'absolute left-1/2 top-3 h-[76px] w-[96px] rounded-xl border border-border bg-surface-2 shadow-[var(--shadow-sm)] transition-[translate,rotate,opacity] duration-700 motion-reduce:transition-none',
SPRING,
shown ? '-translate-x-[84%] -rotate-[10deg] group-hover/empty:-translate-x-[90%] group-hover/empty:-rotate-[14deg]' : '-translate-x-1/2 rotate-0 opacity-0',
)}
style={{ transitionDelay: entering ? '150ms' : undefined }}
>
<span className="absolute left-3 top-3 h-1.5 w-8 rounded-full bg-border-strong" />
<span className="absolute left-3 top-6 h-1.5 w-12 rounded-full bg-border" />
</div>
<div
className={cn(
'absolute left-1/2 top-3 h-[76px] w-[96px] rounded-xl border border-border bg-surface-2 shadow-[var(--shadow-sm)] transition-[translate,rotate,opacity] duration-700 motion-reduce:transition-none',
SPRING,
shown ? '-translate-x-[16%] rotate-[10deg] group-hover/empty:-translate-x-[10%] group-hover/empty:rotate-[14deg]' : '-translate-x-1/2 rotate-0 opacity-0',
)}
style={{ transitionDelay: entering ? '220ms' : undefined }}
>
<span className="absolute right-3 top-3 h-1.5 w-8 rounded-full bg-border-strong" />
<span className="absolute right-3 top-6 h-1.5 w-10 rounded-full bg-border" />
</div>
<div className={cn('absolute left-1/2 top-0 -translate-x-1/2 transition-[opacity,scale] duration-700 motion-reduce:transition-none', SPRING, !shown && 'scale-90 opacity-0')}>
<div className={cn('kitfx-empty-float grid h-[84px] w-[100px] place-items-center rounded-xl border border-border-strong shadow-[var(--highlight),var(--shadow-md)]', TONE_BG[tone])}>
<span className={cn('grid size-10 place-items-center rounded-lg border border-border bg-background shadow-[var(--shadow-sm)] transition-[scale,rotate] duration-500 group-hover/empty:-rotate-6 group-hover/empty:scale-110 motion-reduce:transition-none', SPRING, TONE[tone])}>
<Icon name={icon} size={20} />
</span>
</div>
</div>
</div>
);
}
function Action({ action, variant, size }: { action: EmptyStateAction; variant: ButtonVariant; size: 'sm' | 'md' }) {
// Small buttons grow to a full 44 px tap target on phones
const common = { variant, size, icon: action.icon, iconLeft: action.iconLeft, className: size === 'sm' ? 'max-sm:h-11 max-sm:min-h-11' : undefined } as const;
return action.href ? (
<Button {...common} href={action.href}>
{action.label}
</Button>
) : (
<Button {...common} onClick={action.onClick}>
{action.label}
</Button>
);
}
/**
* What a page or panel shows when there is nothing yet, nothing found or something went wrong.
* It arrives when it scrolls into view: the drawn stack fans open, then title, text and actions rise.
*/
export function EmptyState({ icon = 'inbox', illustration = true, title, description, primaryAction, secondaryAction, variant = 'plain', tone = 'default', size = 'md', as: Tag = 'h3', children, className }: EmptyStateProps) {
const small = size === 'sm';
const [ref, inView] = useInView<HTMLDivElement>();
const waited = useRef(false);
if (!inView) waited.current = true;
const entering = inView && waited.current;
const rise = (step: number) => ({
className: cn('transition-[opacity,translate] duration-700 ease-[var(--ease-kit)] motion-reduce:transition-none', !inView && 'translate-y-3 opacity-0'),
style: { transitionDelay: entering ? `${200 + step * 80}ms` : undefined } as React.CSSProperties,
});
const r1 = rise(0);
const r2 = rise(1);
const r3 = rise(2);
const r4 = rise(3);
return (
<div
ref={ref}
className={cn(
'group/empty flex flex-col items-center text-center',
small ? 'px-5 py-8' : 'px-6 py-12 sm:py-16',
variant === 'dashed' && 'rounded-xl border border-dashed border-border-strong bg-surface transition-colors duration-300 hover:border-[color-mix(in_oklab,var(--primary)_45%,var(--border-strong))]',
variant === 'card' && 'kit-card',
className,
)}
>
<style href="kitfx-empty" precedence="default">
{STYLES}
</style>
{illustration === true ? (
small ? (
<span
className={cn('grid size-11 place-items-center rounded-xl border border-border shadow-[var(--highlight),var(--shadow-sm)] transition-[opacity,scale,rotate] duration-700 group-hover/empty:-rotate-6 motion-reduce:transition-none', SPRING, TONE_BG[tone], TONE[tone], !inView && 'scale-50 opacity-0')}
aria-hidden
>
<Icon name={icon} size={20} />
</span>
) : (
<Stack icon={icon} tone={tone} shown={inView} entering={entering} />
)
) : illustration ? (
<div aria-hidden className={cn('transition-[opacity,scale] duration-700 motion-reduce:transition-none', SPRING, !inView && 'scale-90 opacity-0')}>
<div className="kitfx-empty-float">{illustration}</div>
</div>
) : (
<span aria-hidden className={cn('grid transition-[opacity,scale] duration-700 motion-reduce:transition-none', SPRING, !inView && 'scale-50 opacity-0')}>
<Icon name={icon} size={24} className={cn('kitfx-empty-float text-subtle-foreground', tone !== 'default' && TONE[tone])} />
</span>
)}
<Tag className={cn('text-balance font-semibold tracking-[-0.015em] text-foreground', small ? 'mt-4 text-[15px]' : 'mt-7 text-h3', r1.className)} style={r1.style}>
{title}
</Tag>
{description && (
<p className={cn('mt-2 max-w-[26rem] text-pretty text-muted-foreground', small ? 'text-[13px]' : 'text-sm sm:text-[15px]', r2.className)} style={r2.style}>
{description}
</p>
)}
{(primaryAction || secondaryAction) && (
<div className={cn('flex w-full flex-col-reverse justify-center gap-2.5 sm:w-auto sm:flex-row', small ? 'mt-5' : 'mt-7', r3.className)} style={r3.style}>
{secondaryAction && <Action action={secondaryAction} variant="outline" size={small ? 'sm' : 'md'} />}
{primaryAction && <Action action={primaryAction} variant="primary" size={small ? 'sm' : 'md'} />}
</div>
)}
{children && (
<div className={cn('w-full', small ? 'mt-5' : 'mt-8', r4.className)} style={r4.style}>
{children}
</div>
)}
</div>
);
}Häufige Fragen
Wie installiere ich Leerer Zustand?
Mit aiw add @ai-website/empty-state. Der Befehl legt die Datei components/ui/empty-state.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind icon und button. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Leerer Zustand?
3 Varianten, Erster Start, Keine Treffer und Zustände. Du wählst sie mit der Prop variant, zum Beispiel variant="card".
Kann ein KI-Assistent Leerer Zustand einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Leerer Zustand mit aiw_components und installieren sie mit aiw_add @ai-website/empty-state. Nenn im Prompt die Variante, etwa variant="dashed", dann schreibt der Assistent deine Inhalte direkt in die Props.