Zum Inhalt springen
UI Kit
Accent

Leerer Zustand

Erster Start, leere Suche, Fehler und erledigter Posteingang, mit nächstem Schritt.

Eine von 4 Components in Fortschritt und Zustände

3 Variants

Variants

Erster Start

Ein gezeichneter Kartenstapel und zwei Wege zu beginnen, in einer App.

Zustände

Erledigt, Fehler, gesperrt und offline als kleine Karten.

Details

Was Leerer Zustand mitbringt, aus Registry und Quellcode gelesen.

Bereich
Elements
Variants
3
Version
1.0.0 · seit 1.0.0
Datei
components/ui/empty-state.tsx
Bringt mit
utilskiticonbutton
npm-Pakete
Keine
Sprachen
Texte kommen über Props
Studio
Im Code anpassen

Props

Die Schnittstelle EmptyStateProps aus components/ui/empty-state.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.

  • iconoptional
    IconName

    Icon in the middle of the drawn stack, or alone when illustration is false

  • illustrationoptional
    boolean | React.ReactNode

    true draws a stack of cards behind the icon, a node replaces it with your own image

  • titlePflicht
    React.ReactNode
  • descriptionoptional
    React.ReactNode
  • primaryActionoptional
    EmptyStateAction
  • secondaryActionoptional
    EmptyStateAction
  • variantoptional
    EmptyStateVariant

    plaindashedcard

    plain: no frame. dashed: a drop-zone like frame. card: a raised surface.

  • toneoptional
    EmptyStateTone

    defaultsuccessdestructive

    Color of the icon: success for "all done", destructive for errors

  • sizeoptional
    'sm' | 'md'
  • asoptional
    'h2' | 'h3' | 'h4'

    Heading level for the title

  • childrenoptional
    React.ReactNode

    Extra 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
components/ui/empty-state.tsx
'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.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert