Github
Getting started

Upgrade from 1.x

The breaking changes between Handoff 1.x and 2.x, and the steps to migrate a project.

Handoff 2 changes the runtime requirements, the declaration API, the Figma settings, and the output directories. Upgrades within 2.x are covered in each release note.

Before you start

  1. Make sure the project builds with its current Handoff version.
  2. Commit the project files, the package version, and the lockfile.
  3. Install the 2.x version you plan to use: npm install handoff-app@^2.1.0.
  4. Apply the sections below that affect your project.
ChangeAffects
Node.js 22All projects
Figma settingsProjects that set Figma values in config
Catalog itemsProjects with component or pattern declarations
Output directoriesProjects that deploy the static build, commit exported/, or use public-<projectId>

Node.js 22

Handoff 2 requires Node.js 22 or newer and npm 10 or newer. Update Node.js, CI images, and hosting runtimes first. Projects that init created in 1.x declare the old minimum. Change it:

package.json
"engines": {
  "node": ">=22",
  "npm": ">=10"
}

Figma settings

The Figma file ID and access token moved to integrations.figma. Handoff ignores the old top-level figmaProjectId, figma_project_id, devAccessToken, and dev_access_token. If you use HANDOFF_FIGMA_PROJECT_ID and HANDOFF_DEV_ACCESS_TOKEN and set no Figma values in config, no change is needed.

The access token accepts only an environment reference. A literal token throws HandoffConfigError, in TypeScript, JavaScript, and JSON configs alike.

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

export default defineConfig({
  integrations: {
    figma: {
      projectId: 'your-figma-file-id',
      accessToken: fromEnv('HANDOFF_DEV_ACCESS_TOKEN'),
    },
  },
})

In JSON, write "accessToken": { "$env": "HANDOFF_DEV_ACCESS_TOKEN" }.

Catalog items

Handoff 2 requires defineCatalogItem for components and compositions.

Register catalog directories

Move component and pattern directories from entries to catalog.include. Global entries.js and entries.scss still work.

1.x
entries: { components: ['components'], patterns: ['patterns'], scss: 'styles/main.scss' }
2.x
catalog: { include: ['components', 'patterns'] },
entries: { scss: 'styles/main.scss' }

Rename declaration files

  • Use <name>.handoff.ts, .handoff.tsx, .handoff.js, or .handoff.cjs.
  • Replace legacy file names such as button.json or button.js.
  • JSON declarations, including .handoff.json, are no longer read. handoff.config.json still works.
  • If runtime.workspace.declarationFormat is json, change it to ts, js, or cjs.

Replace the helpers

RemovedReplacement
defineReactComponent(Component, config)defineCatalogItem({ ...config, implementation: Component }) from handoff-app/react
defineHandlebarsComponent(config)defineCatalogItem({ ...config, implementation: './Button.hbs' }) from handoff-app/handlebars
defineCsfComponent(config)defineCatalogItem({ ...config, implementation: fromCSF('./Button.stories.tsx') }) from handoff-app/react
defineComponent(config)defineCatalogItem({ ...config, implementation: './Button.tsx' }) from the module for its renderer
definePattern({ components })defineCatalogItem({ composition }) from handoff-app/pattern

Remove the renderer, entries.component, entries.template, and entries.story fields; the implementation supplies them. Keep entries.js, entries.scss, entries.schema, and entries.templates if the item uses them.

Old field2.x field
titlename
should_doshouldDo
should_not_doshouldNotDo
components (pattern)composition
member id (pattern)ref

Export previews

Remove the inline previews map. Export each preview next to the default export, rename preview title to name, and rename values to args:

button.handoff.ts
export const Primary = { name: 'Primary action', args: { children: 'Save' } }
  • Keep a preview key's spelling if it is a valid export name. Rename it otherwise, and update the composition references to it.
  • Previews appear in source export order.
  • In CommonJS, use exports.default for the item and exports.Primary for each preview.

Replace removed types

Removed typeReplacement
ComponentCatalogItem
DeclarationPreviewCatalogPreview or Preview<typeof item>
PatternComponentRefCompositionRef
ReactDeclarationConfig, HandlebarsDeclarationConfig, CsfDeclarationConfigThe input type of the catalog helper
GenericDeclarationConfig, GenericPatternDeclarationConfigThe input type of the catalog helper, or CatalogItemMeta

Output directories

1.x named directories after the project id (the Figma file ID, or a name from the project path). 2.x uses fixed names:

Content1.x2.x
Static site from buildout/<projectId>out/static
Tokens and assets from fetch and checkoutexported/<projectId>exported
Project-specific public filespublic-<projectId>Removed. Use public
  1. Update deploy scripts and CI steps that read out/<projectId>. With sitesOutputDirectory or HANDOFF_SITES_DIR set, the site is in static under that directory.
  2. Move exported/<projectId> up to exported, or run npm run fetch again. Commit the result.
  3. Merge public-<projectId> into public, then remove it.

A repository that builds several Figma files sets HANDOFF_SITES_DIR and HANDOFF_OUTPUT_DIR, or the matching config values in a profile, per build.

Removed features

  • The make:template command.
  • The legacy /assets pages. The foundation pages show logos and icons.

Verify the upgrade

  1. Build the project.
  2. Check that every catalog item appears, with the expected preview order, args, and composition references.
  3. Run npm run start, edit a declaration, and check that its previews update.
  4. With a registry: publish the migrated items, check them out into a scratch workspace, and build that workspace.

On this page