Github
Authoring

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.

components/example/Component.tsx
export type ComponentProps = {
  label: string
  onSelect?: () => void
}

export default function Component({ label, onSelect }: ComponentProps) {
  return <div onClick={onSelect}>{label}</div>
}
components/example/example.handoff.ts
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 ComponentPreview
  • Preview<typeof item> types each preview's args with 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.

components/badge/badge.handoff.ts
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:

components/badge/badge.handoff.cjs
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.

components/card/card.handoff.ts
import { defineCatalogItem, fromCSF } from 'handoff-app/react'

export default defineCatalogItem({
  id: 'card',
  name: 'Card',
  implementation: fromCSF('./Card.stories.tsx'),
})
  • Each story's args merge over meta.args.
  • Handoff keeps story names, argTypes, and render functions, and the options of select and radio controls.
  • Handoff finds the component through meta.component.
  • Meta and StoryObj come from handoff-app/react, so the file needs no Storybook dependency.
  • The server renders a CSF preview, and the browser does not hydrate it.
  • A .stories.tsx file name does not select CSF by itself. Use fromCSF().

Previews

Each named export of a declaration is a preview. Previews keep their source export order in builds, publish, and checkout.

FieldPurpose
argsProps or template data for this state
nameDisplay title. The default is the export name in start case.
usageUsage snippet shown for this preview
renderReact 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.

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

On this page