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.
npm install --save-dev hollows-uiRegister the generated hollows in your app entry:
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.
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
| Prop | Type | Default | Description |
|---|---|---|---|
| name | string | - | Unique identifier matching the generated definition |
| empty | boolean | false | Whether to show the empty state |
| loading | boolean | false | Show skeleton loading state |
| category | string | auto | Override the auto-detected category |
| copy | CopyOverride | - | Override generated copy |
| animate | boolean | true | Enable transition animations |
| class | string | - | 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.
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 "${filter}"` }"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.
<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.
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.
<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.
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.
import type { HollowProps, HollowState, CopyOverride } from 'hollows-ui/vue'// All props are typed, including the emit events// <Hollow name="..." :empty="..." @action="..." />