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.

components/inbox.tsx
tsx
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

PropTypeDefaultDescription
namestring-Unique identifier that maps to the generated definition
emptybooleanfalseWhether to show the empty state
categorystringautoOverride the auto-detected category
copyCopyOverride-Partially or fully override generated copy
illustrationReactNode-Custom illustration component
onAction() => void-Callback when the CTA button is clicked
loadingbooleanfalseShow a skeleton loading state instead
render(state) => ReactNode-Render prop for full control over the empty state UI
classNamestring-Additional CSS class for the container
animatebooleantrueEnable fade-in animation on the empty state

Basic usage

The most common pattern: wrap a list component and pass a boolean condition.

components/task-list.tsx
tsx
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.

Event handling example
tsx
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.

Custom copy — partial override
tsx
<Hollow  name="user-inbox"  empty={messages.length === 0}  copy={{    headline: "Your inbox is clear!",    // description and cta stay auto-generated  }}>  <MessageList items={messages} /></Hollow>
Custom copy — full override
tsx
<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.

Custom illustration
tsx
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.

Render prop pattern
tsx
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.

Loading state
tsx
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.

TypeScript types
tsx
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>  )}