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
{
"title": "Invoice INV-0001",
"customerName": "Ada Lovelace",
"invoiceNumber": "INV-0001",
"totalCents": 129900
}{
"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.
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>
)
}<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.
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:
<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 zodpnpm add zodyarn add zodbun add zodDefine the schema
Add a schema.ts file inside the data directory, which exports the full schema as a default object.
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
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:
export type Invoice = {
title: string
customerName: string
invoiceNumber: string
totalCents: number
}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.
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>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>
)
}Which frameworks support types?
Types for dynamic data are supported in React and Vue.