Vue

The Vue adapter provides the <Hollow> component for Vue 3 with full Composition API support, reactive empty state rendering, and slot-based customization.

Installation

The Vue adapter is included in the main hollows-ui package. Import from the hollows-ui/vue subpath.

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

Register the generated hollows in your app entry:

src/main.ts
typescript
import { createApp } from 'vue'import App from './App.vue'// Import the generated registryimport './hollows/registry'createApp(App).mount('#app')

Basic usage

Import the Hollow component and wrap any data-dependent section of your template.

components/InboxView.vue
vue
1<script setup lang="ts">2import { Hollow } from 'hollows-ui/vue'3import { useMessages } from '@/composables/useMessages'45const { messages, isLoading } = useMessages()6</script>78<template>9  <div class="inbox-container">10    <h2>Inbox</h2>11    <Hollow12      name="user-inbox"13      :empty="!isLoading && messages.length === 0"14      :loading="isLoading"15      @action="$router.push('/compose')"16    >17      <MessageCard18        v-for="msg in messages"19        :key="msg.id"20        :message="msg"21      />22    </Hollow>23  </div>24</template>

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

Events: The component emits an action event when the CTA button is clicked.

Composition API

The Hollow component integrates naturally with Vue's reactivity system. Computed properties and refs work seamlessly for the empty prop.

components/FilteredList.vue
vue
1<script setup lang="ts">2import { ref, computed } from 'vue'3import { Hollow } from 'hollows-ui/vue'45const items = ref<Item[]>([])6const filter = ref('')78const filteredItems = computed(() =>9  items.value.filter((item) =>10    item.name.toLowerCase().includes(filter.value.toLowerCase())11  )12)1314const isEmpty = computed(() => filteredItems.value.length === 0)15const isSearchEmpty = computed(() =>16  filter.value.length > 0 && isEmpty.value17)18</script>1920<template>21  <div>22    <input v-model="filter" placeholder="Search items..." />2324    <Hollow25      name="filtered-items"26      :empty="isSearchEmpty"27      :copy="{ headline: `No results for &quot;${filter}&quot;` }"28      @action="filter = ''"29    >30      <ItemCard31        v-for="item in filteredItems"32        :key="item.id"33        :item="item"34      />35    </Hollow>36  </div>37</template>

Event handling

Use the @action event to handle CTA clicks. You can call router methods, emit events, or trigger any side effect.

Event handling
vue
<script setup lang="ts">import { useRouter } from 'vue-router'import { Hollow } from 'hollows-ui/vue'const router = useRouter()function handleCreate() {  router.push('/projects/new')}</script><template>  <Hollow    name="project-list"    :empty="projects.length === 0"    @action="handleCreate"  >    <ProjectCard      v-for="project in projects"      :key="project.id"      :project="project"    />  </Hollow></template>

Slots

Use named slots for full control over the empty state rendering. The empty slot receives the generated state as slot props.

Custom empty state with slots
vue
1<template>2  <Hollow name="notifications" :empty="notifications.length === 0">3    <!-- Default slot: your normal content -->4    <NotificationItem5      v-for="n in notifications"6      :key="n.id"7      :notification="n"8    />910    <!-- Empty slot: custom empty state -->11    <template #empty="{ state }">12      <div class="flex flex-col items-center py-12">13        <BellOffIcon class="w-16 h-16 text-gray-400 mb-4" />14        <h3 class="text-lg font-semibold">{{ state.copy.headline }}</h3>15        <p class="text-gray-500 mt-2">{{ state.copy.description }}</p>16      </div>17    </template>1819    <!-- Illustration slot: override just the illustration -->20    <template #illustration>21      <LottieAnimation name="empty-bell" :width="200" :height="180" />22    </template>23  </Hollow>24</template>

Custom copy

Override the generated copy with reactive values. Partial overrides are merged with defaults.

Reactive custom copy
vue
<script setup lang="ts">import { computed } from 'vue'import { Hollow } from 'hollows-ui/vue'const props = defineProps<{ searchQuery: string }>()const customCopy = computed(() => ({  headline: props.searchQuery    ? `No results for "${props.searchQuery}"`    : 'Nothing here yet',}))</script><template>  <Hollow    name="search-results"    :empty="results.length === 0"    :copy="customCopy"  >    <ResultItem v-for="r in results" :key="r.id" :result="r" />  </Hollow></template>

useHollow composable

For advanced use cases, access the hollow state data directly with the useHollow composable. This is useful when you need the generated data outside the component template.

useHollow composable
vue
1<script setup lang="ts">2import { useHollow } from 'hollows-ui/vue'34const { state, isEmpty } = useHollow('user-inbox')56// Access generated data reactively7console.log(state.value?.copy.headline) // "No messages yet"8console.log(state.value?.category)       // "list"9</script>1011<template>12  <div v-if="isEmpty">13    <p>{{ state?.copy.description }}</p>14  </div>15</template>

TypeScript

The Vue adapter includes full type definitions. Import types for use in your own components.

Type imports
typescript
import type { HollowProps, HollowState, CopyOverride } from 'hollows-ui/vue'// All props are typed, including the emit events// <Hollow name="..." :empty="..." @action="..." />