Empty States
blocklayoutGallery 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
$pnpm dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/empty-states.json$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/empty-states.json$yarn dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/empty-states.json$bunx shadcn-svelte@latest add https://uipkge.dev/r/svelte/empty-states.jsonManifest:
https://uipkge.dev/r/svelte/empty-states.jsonInstalls to:src/lib/components/blocks/empty-states/Variants
Loading interactive previews…
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