openpencil/packages/docs/programmable/cli/exporting.md
Danila Poyarkov fb0cb9ee5f
feat: write variable-bound properties as tokens in exported code (#895)
* feat: write variable-bound properties as tokens in exported code

HTML and Tailwind JSX export reference the design tokens a layer is bound to, var(--color-primary) or bg-primary, instead of baking in their values. A binding becomes a reference only where the stylesheet resolves it as the canvas draws it: the layer still draws the value, the token's unit fits the property, and an attribute such as data-theme="dark" can put the element in the layer's mode. Standalone HTML includes the stylesheet for the tokens it uses, compiled into Tailwind's @theme for Tailwind pages.

* refactor: share the collection variable list and tighten token helpers

Read every variable in collection order through one collectionVariables helper built on es-toolkit's compact, used by the stylesheet and the token references, and map padding sides and color channels instead of repeating them.
2026-10-05 09:15:44 +00:00

11 KiB
Raw Blame History

title description
Exporting Export document content to images, PDF, PowerPoint, `.fig`, JSX, HTML, or Storybook stories, with font-substitution policies for raster and PDF output.

Exporting

Export designs from the terminal — raster images, vectors, PDF, editable PowerPoint, .fig subsets, JSX code, HTML, or Storybook stories.

Image Export

openpencil export design.fig                           # PNG (default)
openpencil export design.fig -f jpg -s 2 -q 90        # JPG at 2×, quality 90
openpencil export design.fig -f webp -s 3             # WEBP at 3×
openpencil export design.fig -f svg                   # SVG vector
openpencil export design.fig -f pdf                   # PDF
openpencil export design.fig -f pptx                  # editable PowerPoint
openpencil export design.fig -f fig --page "Page 1"   # export one page as .fig
openpencil export design.fig -f fig --node 1:23        # export one node as .fig
openpencil export design.fig -f html --css tailwind    # export an HTML fragment with Tailwind classes

Options:

  • -f — format: png, jpg, webp, svg, pdf, pptx, jsx, html, fig, storybook
  • -s — export scale (default: 1)
  • -q — quality: 0–100 (JPG/WEBP only)
  • -o — output path
  • --page — page name
  • --node — specific node ID

Font Substitution Policy

For file-backed PNG, JPG, WEBP, and PDF exports, choose how missing or substituted fonts are handled:

openpencil export design.fig -f pdf --font-policy strict
openpencil export design.fig -f png --font-policy warn
openpencil export design.fig -f webp --font-policy allow
  • warn (default) — report substitutions and continue exporting.
  • strict — stop with a nonzero exit status if font preparation cannot faithfully resolve the requested faces.
  • allow — export without the additional font-fidelity check.

Run openpencil fonts first to inspect affected faces. Export preparation may use configured font providers, so its result can differ from the offline file diagnostic.

This policy does not apply to exports from the running app, or to SVG, PowerPoint, JSX, HTML, and .fig output. It checks font resolution, not complete visual equivalence with Figma.

JSX Export

Export as JSX with Tailwind utility classes:

openpencil export design.fig -f tailwind-jsx    # or: -f jsx --style tailwind

Output:

<div className="flex flex-col gap-4 p-6 bg-white rounded-xl">
  <p className="text-2xl font-bold text-[#1D1B20]">Card Title</p>
  <p className="text-sm text-[#49454F]">Description text</p>
</div>

Also supports --style openpencil for the native JSX format (see JSX Renderer).

HTML Export

Export as an HTML fragment with inline styles by default, or Tailwind utility classes:

openpencil export design.fig -f html
openpencil export design.fig -f html --css tailwind

Use --html standalone for a browser-openable HTML document with reset styles and a page wrapper. Standalone HTML is intended as a useful visual/code handoff, not a pixel-perfect renderer replacement:

openpencil export design.fig -f html --html standalone --css inline
openpencil export design.fig -f html --html standalone --css tailwind
openpencil export design.fig -f html --html standalone --css tailwind --assets external

Standalone Tailwind output is compiled during export; it does not depend on the Tailwind browser runtime. Use --assets external to write CSS and extracted image assets next to the HTML file. Use --fonts assets with external assets to resolve detected SceneGraph text fonts through OpenPencil's configured web-font providers and emit local @font-face files.

HTML export is available in file mode.

Tokens in exported code

HTML and Tailwind JSX write variable-bound properties as the design tokens they come from: a fill bound to Primary is background-color: var(--color-primary), and in Tailwind bg-primary, or bg-(--name) for a token outside Tailwind's namespaces. A layer set to another mode gets that mode's attribute, such as data-theme="dark", so the tokens resolve as the canvas draws them.

A value stays literal where CSS would not reproduce it: the layer no longer draws the variable's value, the token is unitless where a length is needed, or the layer sits in a mode that only its own condition, such as a @media query, can select. Standalone HTML includes the stylesheet for the tokens it uses; for fragments and JSX, generate it with openpencil tokens (below).

Design Tokens

Write the document's variables as CSS custom properties:

openpencil tokens design.fig > tokens.css
openpencil tokens design.fig --format tailwind > theme.css
openpencil tokens design.fig --collection Theme --type COLOR

Each collection's default mode goes in :root. Every other mode overrides the values that differ under its condition: [data-theme="dark"] for a Theme collection's Dark mode, or the selector or @media query the mode names. Aliases stay var() references and are declared again in each mode that changes what they point to, so switching data-theme on any element restyles everything built on it.

:root {
  --color-blue-500: #3B82F5;
  --color-primary: var(--color-blue-500);
}

/* Theme: Dark */
[data-theme="dark"] {
  --color-primary: var(--color-blue-300);
}

--format tailwind puts tokens with a Tailwind v4 namespace (--color-*, --spacing-*, --radius-*, --text-*, …) in @theme, so bg-primary and rounded-card work, and adds a @custom-variant per mode, so dark:bg-surface follows the same condition. Import the file after @import "tailwindcss";. Names come from the variable's code syntax when it names a custom property (--x or var(--x)), otherwise from its name and type: Blue/500 as a color is --color-blue-500.

Tokens that cannot be written, such as boolean variables or a condition that is not a selector or query, are listed on stderr and left out. The command works in file mode and against the running app.

Storybook Export

Generate one CSF3 .stories.ts file per component set or component:

openpencil export design.fig -f storybook                      # React stories in ./design-stories/
openpencil export design.fig -f storybook --framework vue -o src/stories
openpencil export design.fig -f storybook --framework html --page "Components"
openpencil export design.pen -f storybook -o src/stories --watch  # re-export on every save
openpencil export 'src/**/*.pen' -f storybook --beside --watch    # stories next to each design

Designs next to their stories

Keep each component's design file in the component's folder and export with --beside: each document's stories, design images, and .openpencil-stories.json manifest go into that document's own folder, next to the component's code. A Storybook stories glob such as ../src/**/*.stories.ts in .storybook/main.ts then picks them up without further configuration.

Pass several documents, or a quoted glob such as 'src/**/*.pen' that OpenPencil expands itself (Node.js 22 or later). Several documents need --beside or --output; --page works with one document only. Documents are exported one after another, and when one fails the rest are still exported before the command exits with an error. --watch watches every matched document; a document created after the watch started needs another run.

Each variant of a component set becomes a story, and its variant properties become select controls, so switching a control shows the matching variant. Standalone components named with slashes, such as Button/Primary and Button/Secondary, are grouped into one Button file with a Variant control. A combination the design has no variant for throws a named error in Storybook rather than showing a different variant.

Stories render the component as HTML with inline styles, like -f html, so they need no OpenPencil runtime; --framework (react, vue, or html) only changes the wrapper and the Meta/StoryObj import from @storybook/react-vite, @storybook/vue3-vite, or @storybook/html-vite. Text uses the document's font families, which Storybook has to load itself. Text, boolean, and instance-swap properties are not exported yet.

Stories carry parameters.design entries for @storybook/addon-designs:

  • OpenPencil — when the document path is inside the current directory, an openpencil:// link that opens the document in the desktop app and selects the variant, or its component set when another layer shares the variant's name. The scheme addresses layers by name, so a story whose variant and component names are both shared by other layers gets no link.
  • Design — a 2× PNG of the variant, written to <Name>.design/ next to the story and referenced with new URL(…, import.meta.url), so Vite bundles it. Copy the parameter onto the story of your own component to compare the implementation with the design. --no-design-images skips rendering; font substitution follows --font-policy as in raster export.

-o names the output directory. A .openpencil-stories.json manifest there records which document, as a path relative to the output directory, and which page generated each story and design image; commit it with the stories. An export replaces the files that a previous export of the same document generated there, including those of components since deleted or renamed; with --page, only that page's. It refuses, before changing anything, to overwrite any other file — a hand-written story, another document's, or a stray image. A one-page export whose file names shifted onto another page's stories asks for a full export instead. Without the manifest, existing stories count as someone else's, so remove them before exporting again. --watch keeps the command running and re-exports whenever the document is saved, so Storybook's hot reload follows the design; a save that cannot be read is reported and the watch continues. A missing --page or a --font-policy strict substitution still ends the command.

Live App Mode

Omit the file to export from the running app:

openpencil export -f png                       # export the selection in the active document
openpencil export --page "Components" -f png   # export every layer of a page
openpencil export --node 1:23 -f png           # export one layer, on any page

--page takes a page name and --page-id a page ID from openpencil documents list; either exports that page without switching the app to it. Add --document-id to export from a document other than the active one.

Live app mode supports PNG, JPG, WEBP, SVG, and PDF. PowerPoint, JSX, HTML, Storybook, and .fig exports require a file argument. File-mode thumbnail export is not currently supported.