React
The React adapter provides the <Hollow> component — a drop-in wrapper that renders auto-generated empty states when your data is absent.
Quick start
Import the Hollow component from the React adapter and wrap any data-dependent section of your UI.
import { Hollow } from 'hollows-ui/react'export function Inbox({ messages }) { return ( <Hollow name="user-inbox" empty={messages.length === 0}> <MessageList items={messages} /> </Hollow> )}When empty is true, the component renders the pre-generated empty state (illustration, headline, description, and CTA). When false, it renders the children as normal.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| name | string | - | Unique identifier that maps to the generated definition |
| empty | boolean | false | Whether to show the empty state |
| category | string | auto | Override the auto-detected category |
| copy | CopyOverride | - | Partially or fully override generated copy |
| illustration | ReactNode | - | Custom illustration component |
| onAction | () => void | - | Callback when the CTA button is clicked |
| loading | boolean | false | Show a skeleton loading state instead |
| render | (state) => ReactNode | - | Render prop for full control over the empty state UI |
| className | string | - | Additional CSS class for the container |
| animate | boolean | true | Enable fade-in animation on the empty state |
Basic usage
The most common pattern: wrap a list component and pass a boolean condition.
1import { Hollow } from 'hollows-ui/react'2import { useTasks } from '@/hooks/use-tasks'34export function TaskList() {5 const { tasks, isLoading } = useTasks()67 return (8 <div className="task-list-container">9 <h2>My Tasks</h2>10 <Hollow11 name="task-list"12 empty={!isLoading && tasks.length === 0}13 loading={isLoading}14 onAction={() => openNewTaskDialog()}15 >16 {tasks.map((task) => (17 <TaskCard key={task.id} task={task} />18 ))}19 </Hollow>20 </div>21 )22}Event handling
The onAction prop fires when the user clicks the generated CTA button. Use it to navigate, open a dialog, or trigger any action relevant to the empty state.
import { Hollow } from 'hollows-ui/react'import { useRouter } from 'next/navigation'export function ProjectList({ projects }) { const router = useRouter() return ( <Hollow name="project-list" empty={projects.length === 0} onAction={() => router.push('/projects/new')} > <ProjectGrid items={projects} /> </Hollow> )}If you don't provide onAction, the CTA button is still rendered but behaves as a passive element. This is useful when the empty state is informational only.
Custom copy
Override any part of the generated copy while keeping the rest. Partial overrides are merged with the generated defaults.
<Hollow name="user-inbox" empty={messages.length === 0} copy={{ headline: "Your inbox is clear!", // description and cta stay auto-generated }}> <MessageList items={messages} /></Hollow><Hollow name="user-inbox" empty={messages.length === 0} copy={{ headline: "All caught up", description: "You've read every message. Take a break!", cta: { label: "Refresh", action: "secondary" }, }}> <MessageList items={messages} /></Hollow>Custom illustrations
Replace the generated illustration with any React component — an SVG, a Lottie animation, or even a canvas element.
import { Hollow } from 'hollows-ui/react'import { InboxIllustration } from '@/illustrations/inbox'export function Inbox({ messages }) { return ( <Hollow name="user-inbox" empty={messages.length === 0} illustration={<InboxIllustration className="w-48 h-40 text-amber-400" />} > <MessageList items={messages} /> </Hollow> )}Render prop
For full control over the empty state layout, use the render prop. It receives the generated state data and lets you build a completely custom UI.
1<Hollow2 name="search-results"3 empty={results.length === 0}4 render={(state) => (5 <div className="flex flex-col items-center py-16">6 <img7 src={state.illustration.src}8 alt={state.illustration.alt}9 className="w-32 h-32 mb-6 opacity-60"10 />11 <h3 className="text-xl font-semibold mb-2">12 {state.copy.headline}13 </h3>14 <p className="text-gray-500 text-center max-w-xs mb-6">15 {state.copy.description}16 </p>17 <button18 onClick={clearFilters}19 className="px-4 py-2 bg-amber-500 text-black rounded-lg"20 >21 {state.copy.cta.label}22 </button>23 </div>24 )}25>26 <SearchResults items={results} />27</Hollow>Loading states
Pass loading={true} to show a skeleton placeholder instead of the empty state or children. The skeleton layout matches the dimensions of the empty state container.
export function Dashboard({ data, isLoading }) { return ( <Hollow name="analytics-panel" empty={!isLoading && data.length === 0} loading={isLoading} > <AnalyticsGrid data={data} /> </Hollow> )}TypeScript
The Hollow component is fully typed. You can also import the prop types for use in your own abstractions.
import type { HollowProps, HollowState, CopyOverride } from 'hollows-ui/react'// Use in a wrapper componentinterface CustomEmptyProps extends Pick<HollowProps, 'name' | 'empty' | 'onAction'> { children: React.ReactNode}export function CustomEmpty({ name, empty, onAction, children }: CustomEmptyProps) { return ( <Hollow name={name} empty={empty} onAction={onAction} animate={false}> {children} </Hollow> )}