Private betav0.1.0
Docs
Format developer documentation

Data and variants

Learn how to work with dynamic data in your Format documents.

You can pass dynamic data into your templates and render it in Studio and during runtime when generating PDFs.

image placeholder

Data example image (or video/gif)

Replace this with the final image asset.

Variants

In Studio, we refer to different data sets as variants. Using variants to test dynamic data in Studio helps you to build more resilient and robust PDFs. For example:

  • Testing different length text strings to see how your templates respond
  • Trying out differing locales or languages
  • Dynamically rendering different templates or components based on a data variant

You can create a variants by adding a [variant-name].json file in the data directory or your document. The default variant has a reserved name of default.json.

If not obvious, it's expected that the data shape across variants is the same. To add more safety around your dynamic data, consider adding types for hints in development and a schema for development and runtime safety.

Example:

documents
invoice
data
default.json
alternative.json
index.tsx
documents/invoice/data/default.json
{
	"title": "Invoice INV-0001",
	"customerName": "Ada Lovelace",
	"invoiceNumber": "INV-0001",
	"totalCents": 129900
}
documents/invoice/data/alt.json
{
	"title": "Invoice INV-0002",
	"customerName": "Grace Hopper",
	"invoiceNumber": "INV-0002",
	"totalCents": 49900
}

The variants you create will be available to switch between in Studio from the sidebar.

image placeholder

Image of Studio sidebar

Replace this with the final image asset.

Using data in your documents

Your document entry point receives the selected variant as data.

documents/invoice/index.tsx
import { Document, Layout } from '@format.dev/react'

export default function Invoice ({ data }) {
	return (
		<Document title={data.title}>
			<Layout id="invoice" width={793.71} height={1122.52}>
				<small>{data.invoiceNumber}</small>
				<h1>{data.customerName}</h1>
				<p>{data.totalCents / 100}</p>
			</Layout>
		</Document>
	)
}
documents/invoice/index.vue
<script setup lang="ts">
import { Document, Layout } from '@format.dev/vue'

const props = defineProps<{
	data: {
		title: string
		customerName: string
		invoiceNumber: string
		totalCents: number
	}
}>()
</script>

<template>
	<Document :title="props.data.title">
		<Layout id="invoice" :width="793.71" :height="1122.52">
			<small>{{ props.data.invoiceNumber }}</small>
			<h1>{{ props.data.customerName }}</h1>
			<p>{{ props.data.totalCents / 100 }}</p>
		</Layout>
	</Document>
</template>

HTML documents get passed through Eta. This is how the data variable is populated.

documents/invoice/index.html
<template data-type="document" data-title="<%= data.title %>">
	<template data-type="layout" data-id="invoice" data-width="793.71px" data-height="1122.52px">
		<small><%= data.invoiceNumber %></small>
		<h1><%= data.customerName %></h1>
		<p><%= data.totalCents / 100 %></p>
	</template>
</template>

Eta's include() works too, so you can split a document into partials. Reference sibling .html files by relative path, and pass each include its own data:

documents/invoice/index.html
<template data-type="document" data-title="<%= data.title %>">
	<template data-type="layout" data-id="invoice" data-width="793.71px" data-height="1122.52px">
		<%~ include('./header.html', data) %>
		<% data.lineItems.forEach(item => { %>
			<%~ include('./line-item.html', { item }) %>
		<% }) %>
	</template>
</template>

Paths resolve relative to the file doing the including, and partials can include other partials. Format bundles every partial referenced by a literal path. Includes work the same in the Studio preview, at compile time, and when you generate PDFs at runtime.

Schemas

You can define a schema for the dynamic data shape of each document. This is optional but recommended. Adding a schema will produce useful errors in development when your data does not match the schema.

This same schema will automatically be used at runtime when you generate PDFs, unless disabled. This means you're more likely to catch data errors without rendering a potentially broken PDF.

Format supports any schema library that follows the Standard Schema spec. Popular libraries that do include:

  • Zod
  • Valibot
  • Joi
  • Yup

A complete list can be found here.

Install your schema library

This example uses Zod.

npm install zod
pnpm add zod
yarn add zod
bun add zod

Define the schema

Add a schema.ts file inside the data directory, which exports the full schema as a default object.

documents/invoice/data/schema.ts
import { z } from 'zod'

export default z.object({
	title: z.string(),
	customerName: z.string(),
	invoiceNumber: z.string(),
	totalCents: z.number().int().nonnegative()
})
documents
invoice
data
default.json
alternative.json
schema.ts
index.tsx

All variants are validated against the schema in Studio.

Types

Add types to your dynamic data using TypeScript. You can add types manually and import them into your component files:

documents/invoice/types.ts
export type Invoice = {
  title: string
  customerName: string
  invoiceNumber: string
  totalCents: number
}
documents/invoice/index.tsx
import { Document, Layout } from '@format.dev/react'
import type { Invoice } from './types'

export default function Invoice ({ data }: Invoice) {
	return (
		<Document title={data.title}>
			<Layout id="invoice" width={793.71} height={1122.52}>
				<small>{data.invoiceNumber}</small>
				<h1>{data.customerName}</h1>
				<p>{data.totalCents / 100}</p>
			</Layout>
		</Document>
	)
}

Or, you can use your schema library's type inference to generate types from the schema definition.

documents/invoice/data/schema.ts
import { z } from 'zod'

const InvoiceSchema = z.object({
	title: z.string(),
	customerName: z.string(),
	invoiceNumber: z.string(),
	totalCents: z.number().int().nonnegative()
})

export default InvoiceSchema

export type Invoice = z.infer<typeof InvoiceSchema>
documents/invoice/index.tsx
import { Document, Layout } from '@format.dev/react'
import type { Invoice } from './data/schema'

export default function Invoice ({ data }: Invoice) {
	return (
		<Document title={data.title}>
			<Layout id="invoice" width={793.71} height={1122.52}>
				<small>{data.invoiceNumber}</small>
				<h1>{data.customerName}</h1>
				<p>{data.totalCents / 100}</p>
			</Layout>
		</Document>
	)
}
Useful

Which frameworks support types?

Types for dynamic data are supported in React and Vue.

Was this page helpful?