Github
Getting started

Add to an existing app

Document the components of an existing application, with declarations next to the components and one dependency tree.

Declarations sit next to the components they document, so the catalog points into the application source:

handoff.config.ts
catalog: { include: ['src/components', 'src/blocks'] },

Two rules for every layout

handoff-app must be a dependency of the package that holds the *.handoff.ts files. A declaration imports handoff-app/react, and Handoff resolves that import from the declaration's directory, not the config's. With npm, hoisting usually hides a missing dependency. With pnpm, Handoff skips the item with Could not resolve "handoff-app/react", and the build still exits with code 0.

Two tsconfig.json files do different jobs.

  • The config nearest to the declarations type checks them. Editors read only the nearest config.
  • The config in the working directory produces the property tables. Without it, Handoff reports TypeScript config not found and every property table is empty.

If the working directory is not the project root, you need both files.

Choose a layout

Each layout keeps one dependency tree, so the catalog renders the real components with the same React instance. A separate install with its own node_modules is not supported.

LayoutWorking directoryUse when
Subdirectory (recommended)handoff/, set by HANDOFF_WORKING_PATHThe application must not change
Project rootThe project rootThe bundler can move its public/ directory (not Next.js)
Workspace packagehandoff/, with its own package.jsonThe repository is already an npm or pnpm workspace

Subdirectory layout

Install in the application package

Terminal
npm install handoff-app

Create the config

Paths are relative to the working directory, so they start with ../. entries.scss loads the application's global stylesheet into every preview.

handoff/handoff.config.ts
import { defineConfig } from 'handoff-app'

export default defineConfig({
  app: { title: 'Example Design System' },
  catalog: { include: ['../src/components'] },
  entries: { scss: '../src/styles/main.scss' },
})

npx handoff-app eject:config writes the full default config instead.

Add a tsconfig for the property tables

handoff/tsconfig.json
{
  "extends": "../tsconfig.json",
  "include": ["../src", "handoff.config.ts"],
  "exclude": ["node_modules", "out", "public"]
}

Add scripts to the application

package.json
{
  "scripts": {
    "handoff:dev": "HANDOFF_WORKING_PATH=handoff handoff-app start",
    "handoff:build": "HANDOFF_WORKING_PATH=handoff handoff-app build"
  }
}

On Windows, an inline variable does not work in an npm script. Install cross-env and prefix the command with cross-env HANDOFF_WORKING_PATH=handoff.

Declare a component and start

Write a declaration next to a component, for example src/components/button/button.handoff.ts. See Catalog items. Then run npm run handoff:dev.

Ignore generated files

Add handoff/public/api, handoff/out, handoff/.handoff, and handoff/.vercel to .gitignore. Commit handoff/exported if you fetch from Figma.

Project root layout

The working directory is the project root, so one tsconfig.json does both jobs. Handoff writes its generated documentation API to public/api, so a bundler that serves public/ would copy it into the production build. Move the bundler's static directory:

vite.config.ts
export default defineConfig({
  publicDir: 'static',
})

A Next.js application cannot move its public directory, so it cannot use this layout.

Workspace package layout

handoff/ has its own package.json and scripts. The root forwards to it:

package.json
{
  "workspaces": ["handoff"],
  "scripts": { "handoff:dev": "npm run dev --workspace handoff" }
}
handoff/package.json
{
  "name": "handoff",
  "private": true,
  "scripts": { "dev": "handoff-app start", "build": "handoff-app build" },
  "dependencies": { "handoff-app": "^2.1.0" }
}

The declarations in src/ belong to the application package, so both packages declare handoff-app.

pnpm

build and build --package vercel work with the default pnpm layout. build --target registry cannot follow pnpm symlinks when it copies runtime dependencies into the standalone bundle. Use a flat layout:

pnpm-workspace.yaml
nodeLinker: hoisted

On this page