PDF Export Support
Make expandable sections work correctly in PDF exports
When your custom visual has expandable sections -- accordions, collapsible panels, expandable table rows -- Semaphor's PDF exporter needs to know how to expand them before capturing the page. Add three data attributes to your toggle elements to enable this.
This is the Print State Protocol. Without it, the PDF captures whatever is visible at the time, leaving collapsed content hidden in the export.
How It Works
During PDF export, Semaphor:
- Scans the page for elements with
data-spr-expand-id - Programmatically clicks each one to expand it
- Waits for any
data-transitioningflags to clear - Captures the fully expanded state
Your component does not need to detect PDF mode or change its rendering. The exporter handles everything through the DOM attributes.
Under the Hood
The protocol works in three stages:
-
Capture -- When a user exports a PDF, Semaphor reads all elements with
data-spr-expand-idfrom the current page and records which ones are expanded (those witharia-expanded="true"). -
Transfer -- The captured state is serialized as JSON and sent to Semaphor's PDF generation service. No state is stored permanently -- it exists only for the duration of the export.
-
Apply -- Semaphor renders the dashboard in a headless browser, finds every element with
data-spr-expand-id, and clicks each one that needs to match the captured state. If any element hasdata-transitioningset to true (async loading), Semaphor waits for it to finish before continuing. Once all sections are settled, the page is captured as a PDF.
Because this happens automatically, your component never needs to know whether it is being rendered for screen or PDF. Just add the attributes, and Semaphor handles the rest.
Required Attributes
| Attribute | Required | Purpose |
|---|---|---|
data-spr-expand-id | Yes | Unique, stable identifier for the expandable section |
aria-expanded | Yes | Indicates current expand/collapse state (also improves accessibility) |
data-transitioning | No | Signals that async data is loading after expansion |
Place all attributes on the clickable toggle element (the button, row header, or link that expands the section).
Basic Example
A table where each row expands to show detail fields:
The three key lines are on the button:
data-spr-expand-id-- stable ID derived from the row's first column valuearia-expanded-- tracks whether the row is currently open or closedonClick-- handler that responds to programmatic.click()events from the exporter
ID Naming Conventions
IDs must be stable and data-derived. The exporter relies on consistent IDs across renders.
Good IDs
These produce the same ID every time the component renders with the same data.
Bad IDs
Random or time-based IDs break the exporter because it cannot reliably target the same element twice.
Async Expansion
Some sections load data when expanded (fetching details from an API, running a sub-query). Use data-transitioning to tell the exporter to wait before capturing.
The exporter clicks the button, sees data-transitioning set to true, and waits. Once loading completes and data-transitioning becomes false, it proceeds to the next expandable or captures the page.
Nested Expandables
For expandable sections inside other expandable sections, use hierarchical IDs separated by /. This tells the exporter the correct expansion order -- parent first, then children.
The / separator is a convention, not enforced syntax. It keeps IDs readable and ensures the exporter can infer parent-child relationships.
Checklist
When adding expandable sections to your custom visual:
- Add
data-spr-expand-idwith a stable, data-derived ID to every toggle element - Add
aria-expandedto reflect the current open/closed state - Ensure the toggle element responds to
.click()events - For async expansion, add
data-transitioningwhile data is loading - For nested sections, use hierarchical IDs with
/separator - Never use random or time-based values for IDs