Catalog items
Declare React, Handlebars, and CSF components with defineCatalogItem, and export their previews.
A component is a catalog item with an implementation. The declaration gives the stable id, the
documentation metadata, the supporting files, and the previews.
React
Handoff finds the implementation file from its import, so you do not repeat the path. It builds the item's CSS from the stylesheets that the import chain loads, descendants included.
export type ComponentProps = {
label: string
onSelect?: () => void
}
export default function Component({ label, onSelect }: ComponentProps) {
return <div onClick={onSelect}>{label}</div>
}import { defineCatalogItem, type Preview } from 'handoff-app/react'
import Component from './Component'
const item = defineCatalogItem({
id: 'component-id',
name: 'Component name',
description: 'Usage guidance for the component.',
group: 'Group name',
implementation: Component,
})
export default item
type ComponentPreview = Preview<typeof item>
export const Default = {
args: { label: 'Example' },
} satisfies ComponentPreviewPreview<typeof item>types each preview'sargswith the component's props.- A file path also works:
implementation: './Component.tsx'. - Keep implementation files inside the item directory. A file outside it cannot be published.
- The browser hydrates a direct React implementation, so interactive previews work.
Handlebars
The implementation is the path to the template. Pass an argument type to defineCatalogItem to
type the previews.
import { defineCatalogItem, type Preview } from 'handoff-app/handlebars'
type BadgeArgs = { variant: 'primary' | 'secondary'; children: string }
const item = defineCatalogItem<BadgeArgs>({
id: 'badge',
name: 'Badge',
implementation: './Badge.hbs',
entries: { scss: './badge.scss' },
})
export default item
export const Primary = {
args: { variant: 'primary', children: 'New' },
} satisfies Preview<typeof item>CommonJS works too, in a .handoff.js or .handoff.cjs file:
const { defineCatalogItem } = require('handoff-app/handlebars')
exports.default = defineCatalogItem({
name: 'Badge',
implementation: './Badge.hbs',
})
exports.Primary = { args: { children: 'New' } }Handlebars previews cannot use render. To add template helpers, use the
registerHandlebarsHelpers hook.
CSF stories
A React item can reuse an existing Storybook CSF story file. The story file does not change, and its named exports become the previews.
import { defineCatalogItem, fromCSF } from 'handoff-app/react'
export default defineCatalogItem({
id: 'card',
name: 'Card',
implementation: fromCSF('./Card.stories.tsx'),
})import { type Meta, type StoryObj } from 'handoff-app/react'
import Card from './Card'
const meta = {
component: Card,
args: { tone: 'neutral' },
} satisfies Meta<typeof Card>
export default meta
export const Primary: StoryObj<typeof meta> = {
args: { title: 'Example' },
}- Each story's
argsmerge overmeta.args. - Handoff keeps story names,
argTypes, andrenderfunctions, and the options ofselectandradiocontrols. - Handoff finds the component through
meta.component. MetaandStoryObjcome fromhandoff-app/react, so the file needs no Storybook dependency.- The server renders a CSF preview, and the browser does not hydrate it.
- A
.stories.tsxfile name does not select CSF by itself. UsefromCSF().
Previews
Each named export of a declaration is a preview. Previews keep their source export order in builds, publish, and checkout.
| Field | Purpose |
|---|---|
args | Props or template data for this state |
name | Display title. The default is the export name in start case. |
usage | Usage snippet shown for this preview |
render | React only. Renders the preview in place of the implementation. |
Handoff serializes args. A preview that needs an icon, a callback, JSX children, or its own
state declares render instead. The item still documents the implementation, so a wrapper never
becomes the published API.
import { useState } from 'react'
import { defineCatalogItem, type Preview } from 'handoff-app/react'
import Component from './Component'
const item = defineCatalogItem({ id: 'component-id', name: 'Component name', implementation: Component })
export default item
export const Interactive = {
name: 'Interactive state',
render: function Interactive() {
const [label, setLabel] = useState('Example')
return <Component label={label} onSelect={() => setLabel('Selected')} />
},
} satisfies Preview<typeof item>render is a component, so a name that starts with an uppercase letter lets it use hooks.
Properties
The component page shows a property table built from the implementation's types. Handoff reads
them with the tsconfig.json in the working directory; see
Add to an existing app. A component
imported from another file gets its property types from that file. reactDocgen.maxDepth and
reactDocgen.excludeDirectories in the config tune the type scan.
In a registry, readers can save their own states of a component as custom previews.