Skip to main content

Export

This guide covers how to turn the current calendar into a downloadable file - a PDF or PNG rendered on a remote service, or a local format such as XLSX that you build yourself. You trigger everything through a single action and describe the output with one config object.

The functionality is available in PRO Edition only

PRO

How export works

Export is an action, not a prop. You call api.exec("export-data", config) and the store does three things: it snapshots the current view, date, and raw events; it captures any custom presentation you've rendered (see below); and it builds a serializable request. Where that request goes depends on the url you pass.

You have two destinations:

  • A URL string - the store serializes the request and submits it as an ephemeral POST form targeting _blank. The field name is data, the value is the serialized request. The form is added to the page, submitted, and removed right away. This is the path for the hosted PDF/PNG renderer.
  • A callback function - the store hands you the prepared request, the event records, and a couple of helpers, then steps back. You decide what to do. This is the path for formats the widget can't render itself, like XLSX.

One detail matters for both paths. The snapshot calls getEvents() on the store without a range, so you get every stored event - not the slice currently on screen. In recurring mode that means the raw masters, exceptions, and one-off events, not the expanded occurrences. The renderer expands them again on its side.

Dates get special treatment during serialization. Each Date is converted to a wall-clock ISO string, computed as new Date(d.getTime() - d.getTimezoneOffset() * 60000).toISOString(). That keeps the local calendar time the user sees - 9:00 stays 9:00 - instead of preserving the original UTC instant. The remote renderer draws exactly what was on the grid.

Triggering an export

Pass the hosted renderer URL and a format. The version export lets you pin the request to the widget build:

import { useState } from "react";
import { Calendar, version } from "@svar-ui/react-calendar";

function App() {
const [api, setApi] = useState(null);
const url = "https://export.svar.dev/calendar/" + version;

function toPDF() {
api.exec("export-data", { url, format: "pdf", paper: { size: "auto" } });
}
function toPNG() {
api.exec("export-data", { url, format: "png", paper: { size: "auto" } });
}

return (
<>
<button onClick={toPDF}>Export to PDF</button>
<button onClick={toPNG}>Export to PNG</button>
<Calendar init={setApi} events={[]} date={new Date()} />
</>
);
}

The action never mutates calendar state, and it resolves even when url is omitted - in that case nothing is sent, which is handy if you want to build the request and inspect it first.

If you'd rather let the user choose the format and the settings, skip the hand-built config and use the ready-made export dialog.

Export configuration

ExportConfig is the single argument to the action. Every field is optional except the destination url:

FieldTypeDefaultPurpose
urlstring or callback-Destination: a POST endpoint or a function you handle yourself.
formatstring"pdf"Output format ("pdf", "png", or your own id for the callback path).
fileNamestring"events"Base name for the generated file.
skinstring"willow"Theme the renderer uses (see below).
paperobject-Page setup for PDF/PNG: size, landscape, margins, header, footer, scale, styles.
excelobject-Column and sheet setup you read in a callback (columns, sheetNames, dateFormat).
dataRecord<string, any>-Extra fields; note the handler overwrites this with the snapshot it prepares.

Excel

Spreadsheets can be created on a client side, so to have one, you pass a function that receives the calendar's events and turns them into a file. We recommend to use the xlsx-writer-lite, though any other excel file writer will work

import { useState } from "react";
import { Calendar } from "@svar-ui/react-calendar";
import { downloadBlob, writeWorkbook } from "xlsx-writer-lite";

function App() {
const [api, setApi] = useState(null);

async function toExcel(cfg, records) {
const blob = await writeWorkbook(records, cfg.excel.columns, {
header: true,
dateFormat: cfg.excel.dateFormat,
});
downloadBlob(blob, cfg.fileName + ".xlsx");
}

function startExport() {
api.exec("export-data", {
url: toExcel,
format: "xlsx",
fileName: "events",
excel: {
dateFormat: "dd/mm/yyyy hh:mm",
columns: [
{ id: "text", label: "Title", width: 30 },
{ id: "start", label: "Start", width: 20 },
{ id: "end", label: "End", width: 20 },
{ id: "priority", label: "Priority" },
],
},
});
}

return (
<>
<button onClick={startExport}>Export to Excel</button>
<Calendar init={setApi} events={[]} date={new Date()} />
</>
);
}

Your function is called with the export request, the event records, and a helpers object. The records are plain event objects, ready to feed into a writer.

The excel block describes the sheet:

OptionPurpose
columns[].idEvent field to put in the column - built-in (text, start, end) or your own.
columns[].labelHeader caption. Falls back to the field id.
columns[].widthColumn width in characters.
columns[].hiddenKeeps the column out of the output.
sheetNamesNames for the generated sheets.
dateFormatFormat for date cells, e.g. "dd/mm/yyyy hh:mm".

Column order follows the array, so reordering the entries reorders the spreadsheet.

For the other direction - reading events out of a spreadsheet - see Excel import.

PNG and PDF

Image and PDF output is rendered by server-side export service: the calendar posts its snapshot to the url you provide and the service returns the file. Set format to "png" or "pdf".

import { version } from "@svar-ui/react-calendar";

const url = "https://export.svar.dev/calendar/" + version;

api.exec("export-data", {
url,
format: "png",
fileName: "may-schedule",
paper: { size: "auto" },
});

https://export.svar.dev/calendar/ is a public instance we maintain for demos and evaluation. It has no defined limits or availability guarantees, so don't build a production feature on it - PRO includes the service source and a Docker image, and you point url at your own deployment. The version export pins the request to the widget build you ship.

Page setup

paper shapes the output page:

api.exec("export-data", {
url,
format: "pdf",
paper: {
landscape: true,
size: "auto",
margins: { top: 20, bottom: 20, left: 16, right: 16 },
header: "Team calendar",
footer: "Exported from our app",
},
});
OptionPurpose
sizePaper size - a named size, "auto" to fit the content, or { width, height }.
fitSizeScales the calendar to fit the page instead of cropping it.
landscapeLandscape orientation.
marginsPage margins in pixels: top, bottom, left, right.
headerText drawn above the calendar.
footerText drawn below the calendar.
scaleZoom factor applied before rendering.
stylesExtra CSS: a string or an array of them.

Entries in styles that start with http://, https://, or / are fetched and their content is used; anything else is treated as inline CSS. Order is preserved, so later rules win.

Skins

skin picks the theme the file is drawn with:

skinResult
"willow" (default)The standard light theme.
"willow-dark" (or "dark")The dark theme.
"print"Keeps color accents on a white print surface, hides nav buttons.
"bw"Grayscale / black-and-white palette, hides nav buttons.

Custom event and cell rendering

Your eventContent components, eventCss classes, and cellCss classes are correctly preserved during export, just be sure to include the related style definitions through styles props.

const cellCss = ev => (ev.start < new Date() ? "past" : "");

api.exec("export-data", {
url,
format: "pdf",
paper: {
styles: [".past { color:silver; }"],
},
});

or a full project stylesheet

api.exec("export-data", {
url,
format: "pdf",
paper: {
styles: ["https://mysite.com/allstyles.css"],
},
});

Export dialog

Everything above assumes your app decides the format and the options. If you'd rather let the user pick, SVAR Export Popup from @svar-ui/react-export-popup is a ready-made dialog: it shows a tab per format, collects the settings, and hands you back a config object you pass straight to export-data.

import { useState, useRef } from "react";
import { Calendar, version } from "@svar-ui/react-calendar";
import { ExportPopup } from "@svar-ui/react-export-popup";
import { downloadBlob, writeWorkbook } from "xlsx-writer-lite";

function App() {
const [api, setApi] = useState(null);
const anchorRef = useRef(null);
const [popupOpen, setPopupOpen] = useState(false);

const url = "https://export.svar.dev/calendar/" + version;

const initial = {
format: "pdf",
paper: { landscape: true },
excel: {
columns: [
{ id: "text", label: "Title", width: 30 },
{ id: "start", label: "Start", width: 20, dateFormat: "dd/mm/yyyy hh:mm" },
{ id: "end", label: "End", width: 20, dateFormat: "dd/mm/yyyy hh:mm" },
{ id: "priority", label: "Priority" },
],
},
};

async function exportData(cfg, records, helpers) {
if (cfg.format === "xlsx") {
const blob = await writeWorkbook(records, cfg.excel.columns, { header: true });
downloadBlob(blob, "events.xlsx");
} else {
helpers.post(url, { data: helpers.serialize(cfg) });
}
}

function doExport(request) {
api.exec("export-data", { url: exportData, ...request });
setPopupOpen(false);
}

return (
<>
<button ref={anchorRef} onClick={() => setPopupOpen(true)}>Export</button>
<Calendar init={setApi} events={[]} date={new Date()} />

{popupOpen && (
<ExportPopup
tabs={["pdf", "png", "xlsx"]}
parent={anchorRef.current}
initial={initial}
onExport={doExport}
onClose={() => setPopupOpen(false)}
/>
)}
</>
);
}

The dialog produces the same config object this guide describes, so api.exec("export-data", { url, ...request }) is the whole integration. A few points worth knowing when wiring it to the calendar:

  • tabs picks the formats offered - "pdf", "png", and "xlsx" each come with their own settings UI. The default is ["pdf", "png"], so list "xlsx" explicitly when you handle spreadsheets.
  • initial seeds the controls once, at mount. This is where you put the Excel columns and any page defaults; later changes to the same object are ignored.
  • onExport receives a fresh request built from what the user chose, and the request only carries the group that applies - paper for PDF/PNG, excel for XLSX. Neither onExport nor onClose closes the dialog, so unmount it yourself.
  • parent anchors the popup to the element that opened it; pass left/top instead to place it at fixed coordinates.
  • Appearance comes back as skin: "light" | "dark" | "print" | "bw". The renderer maps dark, print, and bw to the matching themes and draws everything else - including light - with the standard Willow theme, so the value passes through unchanged.
  • Excel columns are the user's to reorder and toggle. The request contains only the ones left checked, and the original hidden flag is stripped, so a column you never want exported should be left out of initial.excel.columns entirely rather than marked hidden.

The dialog also supports custom tabs, localization, and a page-count hint - see its own documentation for those.