Svelte

The Svelte adapter provides a <Hollow> component that integrates with Svelte's reactivity model, using stores, slots, and event dispatching for empty state rendering.

Installation

Import from the hollows-ui/svelte subpath. The adapter supports Svelte 4 and Svelte 5 (runes mode).

Terminal
bash
npm install --save-dev hollows-ui

Import the generated registry in your app entry point:

src/main.ts (Vite + Svelte)
typescript
import './hollows/registry'import App from './App.svelte'const app = new App({  target: document.getElementById('app')!,})export default app

Basic usage

Wrap any data-dependent section with the <Hollow> component. When empty is true, it renders the auto-generated empty state with illustration, copy, and CTA.

src/components/TaskList.svelte
svelte
1<script lang="ts">2  import { Hollow } from 'hollows-ui/svelte'3  import TaskCard from './TaskCard.svelte'45  export let tasks: Task[] = []6  export let isLoading = false78  $: isEmpty = !isLoading && tasks.length === 09</script>1011<div class="task-list">12  <h2>My Tasks</h2>13  <Hollow14    name="task-list"15    empty={isEmpty}16    loading={isLoading}17    on:action={() => goto('/tasks/new')}18  >19    {#each tasks as task (task.id)}20      <TaskCard {task} />21    {/each}22  </Hollow>23</div>

Props

PropTypeDefaultDescription
namestring-Unique identifier matching the generated definition
emptybooleanfalseWhether to show the empty state
loadingbooleanfalseShow skeleton loading state
categorystringautoOverride the auto-detected category
copyCopyOverride-Override generated copy
animatebooleantrueEnable transition animations
classstring-Additional CSS class for the container

Event dispatching

The component dispatches an action event when the CTA button is clicked. Listen with Svelte's on: directive.

Event handling
svelte
<script lang="ts">  import { Hollow } from 'hollows-ui/svelte'  import { goto } from '$app/navigation'  export let projects: Project[] = []</script><Hollow  name="project-list"  empty={projects.length === 0}  on:action={() => goto('/projects/new')}>  {#each projects as project (project.id)}    <ProjectCard {project} />  {/each}</Hollow>

You can also forward the event to a parent component:

Forwarding the action event
svelte
<script lang="ts">  import { createEventDispatcher } from 'svelte'  import { Hollow } from 'hollows-ui/svelte'  const dispatch = createEventDispatcher()  export let items: Item[] = []</script><Hollow  name="item-list"  empty={items.length === 0}  on:action={() => dispatch('create')}>  {#each items as item (item.id)}    <ItemRow {item} />  {/each}</Hollow>

Slot customization

Use named slots to customize the empty state rendering. The empty slot receives the generated state as slot props.

Custom empty state via slots
svelte
1<Hollow name="notifications" empty={notifications.length === 0}>2  <!-- Default slot: normal content -->3  {#each notifications as notification (notification.id)}4    <NotificationItem {notification} />5  {/each}67  <!-- Named slot: custom empty state -->8  <svelte:fragment slot="empty" let:state>9    <div class="flex flex-col items-center py-12 text-center">10      <BellOff class="w-16 h-16 text-gray-400 mb-4" />11      <h3 class="text-lg font-semibold">{state.copy.headline}</h3>12      <p class="text-gray-500 mt-2 max-w-xs">13        {state.copy.description}14      </p>15      <button16        class="mt-6 px-4 py-2 bg-amber-500 text-black rounded-lg"17        on:click={() => enableNotifications()}18      >19        {state.copy.cta.label}20      </button>21    </div>22  </svelte:fragment>2324  <!-- Named slot: custom illustration -->25  <svelte:fragment slot="illustration">26    <LottiePlayer src="/animations/empty-bell.json" width={200} />27  </svelte:fragment>28</Hollow>

Store integration

Svelte stores work naturally with the empty prop. Use derived stores for complex empty state logic.

Store-based empty state
svelte
1<script lang="ts">2  import { derived } from 'svelte/store'3  import { Hollow } from 'hollows-ui/svelte'4  import { items, searchQuery } from '$lib/stores'56  const filteredItems = derived(7    [items, searchQuery],8    ([$items, $query]) =>9      $items.filter((item) =>10        item.name.toLowerCase().includes($query.toLowerCase())11      )12  )1314  const isEmpty = derived(15    [filteredItems, searchQuery],16    ([$filtered, $query]) => $query.length > 0 && $filtered.length === 017  )18</script>1920<Hollow21  name="search-results"22  empty={$isEmpty}23  copy={{ headline: `No results for "${$searchQuery}"` }}24  on:action={() => searchQuery.set('')}25>26  {#each $filteredItems as item (item.id)}27    <ItemCard {item} />28  {/each}29</Hollow>

SvelteKit

With SvelteKit, import the registry in your root layout and use the hollow component in any page or component.

src/routes/+layout.svelte
svelte
<script>  // Import once at the root layout  import '../hollows/registry'</script><slot />
src/routes/inbox/+page.svelte
svelte
1<script lang="ts">2  import { Hollow } from 'hollows-ui/svelte'3  import type { PageData } from './$types'45  export let data: PageData67  $: messages = data.messages8</script>910<div class="max-w-2xl mx-auto p-6">11  <h1 class="text-2xl font-bold mb-6">Inbox</h1>12  <Hollow13    name="user-inbox"14    empty={messages.length === 0}15    on:action={() => goto('/compose')}16  >17    {#each messages as message (message.id)}18      <MessageCard {message} />19    {/each}20  </Hollow>21</div>

For server-side rendered pages, the hollow component renders the empty state on the server when the empty prop is true during SSR. The illustration is inlined as an SVG, so no additional network requests are needed.

TypeScript

The Svelte adapter ships with full TypeScript definitions. Use lang="ts" in your script blocks for type checking.

Type imports
typescript
import type { HollowProps, HollowState, CopyOverride } from 'hollows-ui/svelte'// Types are available for use in your components// All props and events are fully typed