Menu

Change language

GitHub repo

Empty States

blocklayout

Gallery of six zero-data cards — no data, no search results, first-use, error recovery, no access, and sync complete — each with a matching isometric illustration, title, copy, and action pair. `variant` picks a single card, `size` compacts it, `layout` switches grid vs list. Override title, description, image, labels, hrefs, and callbacks. Pass `image=""` to hide the art.

Installation

$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/empty-states.json
Manifest:https://uipkge.dev/r/svelte/empty-states.jsonInstalls to:src/lib/components/blocks/empty-states/

Variants

Loading interactive previews…

npm dependencies

Includes

Files installed (1)

  • src/lib/components/blocks/empty-states/EmptyStates.svelte14 kB
    <script lang="ts" module>
      import type { Component } from 'svelte'
    
      export type EmptyStateVariant =
        | 'no-data'
        | 'no-search-results'
        | 'first-use'
        | 'error-recovery'
        | 'no-access'
        | 'sync-complete'
    
      export interface EmptyStatesProps {
        /** `'all'` renders the full gallery; a single variant renders just that card. */
        variant?: EmptyStateVariant | 'all'
        size?: 'sm' | 'default'
        layout?: 'grid' | 'list'
        class?: string
        title?: string
        description?: string
        /** Replaces the response panel on a single variant. Pass `''` to hide art. */
        image?: string
        imageAlt?: string
        primaryLabel?: string
        secondaryLabel?: string
        primaryHref?: string
        secondaryHref?: string
        showPrimary?: boolean
        showSecondary?: boolean
        primaryDisabled?: boolean
        secondaryDisabled?: boolean
        onPrimary?: () => void
        onSecondary?: () => void
      }
    </script>
    
    <script lang="ts">
      import { RotateCcw } from '@lucide/svelte'
      import { cn } from '$lib/utils'
      import { Button, buttonVariants, type ButtonVariants } from '$lib/components/ui/button'
    
      let {
        variant = 'all',
        size = 'default',
        layout = 'grid',
        class: className,
        title,
        description,
        image,
        imageAlt = '',
        primaryLabel,
        secondaryLabel,
        primaryHref,
        secondaryHref,
        showPrimary = true,
        showSecondary = true,
        primaryDisabled = false,
        secondaryDisabled = false,
        onPrimary,
        onSecondary,
      }: EmptyStatesProps = $props()
    
      const show = (v: EmptyStateVariant) => variant === 'all' || variant === v
      const copy = (fallback: string, override?: string) => (override === undefined ? fallback : override)
      const forVariant = (v: EmptyStateVariant, fallback: string, override?: string) =>
        variant === v ? copy(fallback, override) : fallback
    
      const titleNoData = $derived(forVariant('no-data', 'Nothing here yet', title))
      const descNoData = $derived(
        forVariant(
          'no-data',
          'Your workspace is empty. Create your first project or import existing data to get started.',
          description,
        ),
      )
      const imageNoData = $derived(forVariant('no-data', '/illustrations/empty-no-data.jpg', image))
      const primaryNoData = $derived(forVariant('no-data', 'Create', primaryLabel))
      const secondaryNoData = $derived(forVariant('no-data', 'Import data', secondaryLabel))
    
      const titleSearch = $derived(forVariant('no-search-results', 'No matches', title))
      const descSearch = $derived(
        forVariant(
          'no-search-results',
          'Nothing matched your current filters. Try different keywords or broaden the search.',
          description,
        ),
      )
      const imageSearch = $derived(forVariant('no-search-results', '/illustrations/empty-no-search.jpg', image))
      const primarySearch = $derived(forVariant('no-search-results', 'Clear filters', primaryLabel))
      const secondarySearch = $derived(forVariant('no-search-results', 'View all', secondaryLabel))
    
      const titleFirst = $derived(forVariant('first-use', 'Welcome to Acme', title))
      const descFirst = $derived(
        forVariant(
          'first-use',
          'This is your new dashboard. Set up your workspace and invite the team to start collaborating.',
          description,
        ),
      )
      const imageFirst = $derived(forVariant('first-use', '/illustrations/empty-first-use.jpg', image))
      const primaryFirst = $derived(forVariant('first-use', 'Get started', primaryLabel))
      const secondaryFirst = $derived(forVariant('first-use', 'Take a tour', secondaryLabel))
    
      const titleError = $derived(forVariant('error-recovery', 'Something broke', title))
      const descError = $derived(
        forVariant(
          'error-recovery',
          "We couldn't load your data. Your work is safe — check the connection and try again.",
          description,
        ),
      )
      const imageError = $derived(forVariant('error-recovery', '/illustrations/empty-error-recovery.jpg', image))
      const primaryError = $derived(forVariant('error-recovery', 'Retry', primaryLabel))
      const secondaryError = $derived(forVariant('error-recovery', 'Contact support', secondaryLabel))
    
      const titleAccess = $derived(forVariant('no-access', 'Ask your admin', title))
      const descAccess = $derived(
        forVariant(
          'no-access',
          "You don't have access to this workspace yet. Request permission and we'll notify you once it's granted.",
          description,
        ),
      )
      const imageAccess = $derived(forVariant('no-access', '/illustrations/empty-no-access.jpg', image))
      const primaryAccess = $derived(forVariant('no-access', 'Request access', primaryLabel))
      const secondaryAccess = $derived(forVariant('no-access', 'Switch account', secondaryLabel))
    
      const titleSync = $derived(forVariant('sync-complete', 'Everything synced', title))
      const descSync = $derived(
        forVariant(
          'sync-complete',
          'All changes were uploaded a few seconds ago. We will keep syncing in the background.',
          description,
        ),
      )
      const imageSync = $derived(forVariant('sync-complete', '/illustrations/empty-sync-complete.jpg', image))
      const secondarySync = $derived(forVariant('sync-complete', 'View activity', secondaryLabel))
    
      const cardClass = $derived(
        cn(
          'border-border bg-card overflow-hidden rounded-xl border text-left',
          size === 'sm' && 'rounded-lg',
          layout === 'list' && 'w-full max-w-md',
        ),
      )
      const imgClass = $derived(cn('w-full object-cover', size === 'sm' ? 'aspect-[16/9]' : 'aspect-[16/10]'))
      const panelClass = 'border-border bg-muted/40 border-b font-mono'
      const bodyClass = $derived(size === 'sm' ? 'p-5' : 'p-6')
      const titleClass = 'text-sm font-medium'
      const descClass = 'text-muted-foreground mt-1 max-w-xs text-sm'
      const actionsClass = $derived(cn('flex flex-wrap items-center gap-2', size === 'sm' ? 'mt-4' : 'mt-5'))
      const actionSize = $derived(size === 'sm' ? 'xs' : 'sm')
    </script>
    
    {#snippet actionButton(
      href: string | undefined,
      label: string,
      variantName: ButtonVariants['variant'] = 'default',
      disabled = false,
      onclick?: () => void,
      Icon?: Component,
    )}
      {#if href}
        <a {href} class={buttonVariants({ variant: variantName, size: actionSize })} {onclick} aria-disabled={disabled}>
          {#if Icon}
            <Icon />
          {/if}
          {label}
        </a>
      {:else}
        <Button variant={variantName} size={actionSize} {disabled} {onclick}>
          {#if Icon}
            <Icon />
          {/if}
          {label}
        </Button>
      {/if}
    {/snippet}
    
    <div
      data-slot="empty-states"
      data-layout={layout}
      class={cn(
        layout === 'list' ? 'flex flex-col items-center gap-4' : 'grid gap-4 sm:grid-cols-2 xl:grid-cols-3',
        className,
      )}
    >
      {#if show('no-data')}
        <article class={cardClass} data-slot="empty-states-card" data-variant="no-data" role="status">
          {#if imageNoData}
            <img src={imageNoData} alt={imageAlt} class={imgClass} />
          {:else if imageNoData === undefined}
            <div class={panelClass} aria-hidden="true">
              <div class="border-border text-muted-foreground truncate border-b px-3 py-2 text-xs">GET /v1/projects</div>
              <div class="space-y-1 p-3 text-xs leading-relaxed">
                <p><span class="text-muted-foreground">HTTP/1.1</span> 200 OK</p>
                <p class="text-muted-foreground">{'{ "data": [], "total": 0 }'}</p>
              </div>
            </div>
          {/if}
          <div class={bodyClass}>
            <h3 class={titleClass}>{titleNoData}</h3>
            <p class={descClass}>{descNoData}</p>
            {#if showPrimary || showSecondary}
              <div class={actionsClass}>
                {#if showPrimary}
                  {@render actionButton(primaryHref, primaryNoData, 'default', primaryDisabled, onPrimary)}
                {/if}
                {#if showSecondary}
                  {@render actionButton(secondaryHref, secondaryNoData, 'ghost', secondaryDisabled, onSecondary)}
                {/if}
              </div>
            {/if}
          </div>
        </article>
      {/if}
    
      {#if show('no-search-results')}
        <article class={cardClass} data-slot="empty-states-card" data-variant="no-search-results" role="status">
          {#if imageSearch}
            <img src={imageSearch} alt={imageAlt} class={imgClass} />
          {:else if imageSearch === undefined}
            <div class={panelClass} aria-hidden="true">
              <div class="border-border text-muted-foreground truncate border-b px-3 py-2 text-xs">
                GET /search?q=northwind
              </div>
              <div class="space-y-1 p-3 text-xs leading-relaxed">
                <p><span class="text-muted-foreground">HTTP/1.1</span> 200 OK</p>
                <p class="text-muted-foreground">{'{ "hits": 0 }'}</p>
              </div>
            </div>
          {/if}
          <div class={bodyClass}>
            <h3 class={titleClass}>{titleSearch}</h3>
            <p class={descClass}>{descSearch}</p>
            {#if showPrimary || showSecondary}
              <div class={actionsClass}>
                {#if showPrimary}
                  {@render actionButton(primaryHref, primarySearch, 'default', primaryDisabled, onPrimary)}
                {/if}
                {#if showSecondary}
                  {@render actionButton(secondaryHref, secondarySearch, 'ghost', secondaryDisabled, onSecondary)}
                {/if}
              </div>
            {/if}
          </div>
        </article>
      {/if}
    
      {#if show('first-use')}
        <article class={cardClass} data-slot="empty-states-card" data-variant="first-use" role="status">
          {#if imageFirst}
            <img src={imageFirst} alt={imageAlt} class={imgClass} />
          {:else if imageFirst === undefined}
            <div class={panelClass} aria-hidden="true">
              <div class="border-border text-muted-foreground truncate border-b px-3 py-2 text-xs">GET /v1/workspace</div>
              <div class="space-y-1 p-3 text-xs leading-relaxed">
                <p><span class="text-muted-foreground">HTTP/1.1</span> 200 OK</p>
                <p class="text-muted-foreground">{'{ "onboarded": false }'}</p>
              </div>
            </div>
          {/if}
          <div class={bodyClass}>
            <h3 class={titleClass}>{titleFirst}</h3>
            <p class={descClass}>{descFirst}</p>
            {#if showPrimary || showSecondary}
              <div class={actionsClass}>
                {#if showPrimary}
                  {@render actionButton(primaryHref, primaryFirst, 'default', primaryDisabled, onPrimary)}
                {/if}
                {#if showSecondary}
                  {@render actionButton(secondaryHref, secondaryFirst, 'ghost', secondaryDisabled, onSecondary)}
                {/if}
              </div>
            {/if}
          </div>
        </article>
      {/if}
    
      {#if show('error-recovery')}
        <article class={cardClass} data-slot="empty-states-card" data-variant="error-recovery" role="alert">
          {#if imageError}
            <img src={imageError} alt={imageAlt} class={imgClass} />
          {:else if imageError === undefined}
            <div class={panelClass} aria-hidden="true">
              <div class="border-border text-muted-foreground truncate border-b px-3 py-2 text-xs">GET /api/projects</div>
              <div class="space-y-1 p-3 text-xs leading-relaxed">
                <p>
                  <span class="text-muted-foreground">HTTP/1.1</span>
                  <span class="text-destructive">500 Internal Server Error</span>
                </p>
                <p class="text-muted-foreground">error: upstream_timeout</p>
              </div>
            </div>
          {/if}
          <div class={bodyClass}>
            <h3 class={titleClass}>{titleError}</h3>
            <p class={descClass}>{descError}</p>
            {#if showPrimary || showSecondary}
              <div class={actionsClass}>
                {#if showPrimary}
                  {@render actionButton(primaryHref, primaryError, 'default', primaryDisabled, onPrimary, RotateCcw)}
                {/if}
                {#if showSecondary}
                  {@render actionButton(secondaryHref, secondaryError, 'ghost', secondaryDisabled, onSecondary)}
                {/if}
              </div>
            {/if}
          </div>
        </article>
      {/if}
    
      {#if show('no-access')}
        <article class={cardClass} data-slot="empty-states-card" data-variant="no-access" role="status">
          {#if imageAccess}
            <img src={imageAccess} alt={imageAlt} class={imgClass} />
          {:else if imageAccess === undefined}
            <div class={panelClass} aria-hidden="true">
              <div class="border-border text-muted-foreground truncate border-b px-3 py-2 text-xs">GET /settings/billing</div>
              <div class="space-y-1 p-3 text-xs leading-relaxed">
                <p>
                  <span class="text-muted-foreground">HTTP/1.1</span>
                  <span class="text-warning">403 Forbidden</span>
                </p>
                <p class="text-muted-foreground">scope billing:read — missing</p>
              </div>
            </div>
          {/if}
          <div class={bodyClass}>
            <h3 class={titleClass}>{titleAccess}</h3>
            <p class={descClass}>{descAccess}</p>
            {#if showPrimary || showSecondary}
              <div class={actionsClass}>
                {#if showPrimary}
                  {@render actionButton(primaryHref, primaryAccess, 'default', primaryDisabled, onPrimary)}
                {/if}
                {#if showSecondary}
                  {@render actionButton(secondaryHref, secondaryAccess, 'ghost', secondaryDisabled, onSecondary)}
                {/if}
              </div>
            {/if}
          </div>
        </article>
      {/if}
    
      {#if show('sync-complete')}
        <article class={cardClass} data-slot="empty-states-card" data-variant="sync-complete" role="status">
          {#if imageSync}
            <img src={imageSync} alt={imageAlt} class={imgClass} />
          {:else if imageSync === undefined}
            <div class={panelClass} aria-hidden="true">
              <div class="border-border text-muted-foreground truncate border-b px-3 py-2 text-xs">POST /sync</div>
              <div class="space-y-1 p-3 text-xs leading-relaxed">
                <p><span class="text-muted-foreground">HTTP/1.1</span> 200 OK</p>
                <p class="text-muted-foreground">{'{ "synced": 128, "lag_ms": 40 }'}</p>
              </div>
            </div>
          {/if}
          <div class={bodyClass}>
            <h3 class={titleClass}>{titleSync}</h3>
            <p class={descClass}>{descSync}</p>
            {#if showPrimary || showSecondary}
              <div class={actionsClass}>
                {#if showSecondary}
                  {@render actionButton(secondaryHref, secondarySync, 'ghost', secondaryDisabled, onSecondary)}
                {/if}
              </div>
            {/if}
          </div>
        </article>
      {/if}
    </div>
    

Raw manifest:https://uipkge.dev/r/svelte/empty-states.json