A type-safe library for defining structured data as JSON and rendering it into multiple output formats: HTML, Markdown, PDFs, and images. Useful for rendering arbitrary data from any source (AI outputs, reports, APIs) into consistent, styled views.
npm i structured-render
Structured Render uses a three-level hierarchy:
StructuredRenderData: the top-level type. An array of cards.StructuredRenderCard: a container with an optional title, optional title icon, and an array of sections.StructuredRenderSection: a union of all renderable content types (text, table, list, tag, markdown, code block, etc.).All data types are defined with object-shape-tester, providing both runtime validation and compile-time TypeScript types.
Every section has a type field (discriminant), an optional sectionTitle, and optional sources. Use StructuredRenderSectionType to reference type values.
| Type | Description | Key fields |
|---|---|---|
text |
Plain or styled text | text, style?, icon? |
markdown |
Raw markdown content | markdown |
codeBlock |
Fenced code block | code, syntax? |
inlineCode |
Inline code span | code |
table |
Horizontal or vertical table | direction, headers, entries, footerRows? |
list |
Bulleted list of items | items (each with content, icon?, sources?) |
tag |
Colored label / badge | text, color?, useBigTag? |
icon |
SVG icon reference | iconKey, strokeColor?, fillColor? |
collapsible |
Collapsible wrapper around sections | header, content |
source |
Reference to a source document | fileName?, pageNumbers?, quote?, fileBoundingBoxes? |
empty |
Renders nothing (placeholder) | |
processing |
Loading indicator |
Anything producing outputs to be consumed by this package should produce StructuredRenderData instances.
import {StructuredRenderSectionType, StructuredRenderTextStyle} from 'structured-render';
// Basic text
const textSection = {
type: StructuredRenderSectionType.text,
sectionTitle: 'Summary',
text: 'Overall project health is good.',
};
// Styled text
const boldText = {
type: StructuredRenderSectionType.text,
text: 'Important note.',
style: StructuredRenderTextStyle.Bold,
};
const faintText = {
type: StructuredRenderSectionType.text,
text: 'Additional details.',
style: StructuredRenderTextStyle.Faint,
};
// Text with an icon
const textWithIcon = {
type: StructuredRenderSectionType.text,
text: 'Completed task.',
icon: {
type: StructuredRenderSectionType.icon,
iconKey: 'StatusSuccess24Icon',
strokeColor: 'green',
},
};
const markdownSection = {
type: StructuredRenderSectionType.markdown,
sectionTitle: 'Key Findings',
markdown:
'### Strengths\n\n- **Code coverage** improved to 89%.\n- Build times remain under 3 minutes.',
};
// Fenced code block with syntax highlighting
const codeBlock = {
type: StructuredRenderSectionType.codeBlock,
sectionTitle: 'Config',
syntax: 'json',
code: JSON.stringify({project: 'my-app', version: '1.0.0'}, null, 4),
};
// Inline code
const inlineCode = {
type: StructuredRenderSectionType.inlineCode,
code: 'const x = 42;',
};
Tables support two directions: Horizontal (headers as a top row, entries add rows) and Vertical (headers as a left column, entries add columns). Use the createRenderDataTable helper for type-safe table construction.
import {
createRenderDataTable,
StructuredRenderCellDirection,
StructuredRenderSectionType,
} from 'structured-render';
const table = createRenderDataTable(
StructuredRenderCellDirection.Horizontal,
[
{key: 'name'},
{key: 'status'},
],
[
{
data: {
name: {
type: StructuredRenderSectionType.text,
text: 'Auth module',
},
status: {
type: StructuredRenderSectionType.tag,
text: 'Healthy',
color: {variant: 'positive'},
},
},
},
{
data: {
name: {
type: StructuredRenderSectionType.text,
text: 'API layer',
},
status: {
type: StructuredRenderSectionType.tag,
text: 'Degraded',
color: {variant: 'warning'},
},
},
},
],
);
Table cells can contain text, inlineCode, markdown, tag, list, empty, or processing sections.
Headers support custom display text and can be hidden:
const headers = [
{key: 'name', text: {type: StructuredRenderSectionType.text, text: 'Module Name'}},
{key: 'internal', hidden: true},
];
Use emptyStructuredRenderTableFallback to show a fallback section when a table has no data:
import {emptyStructuredRenderTableFallback} from 'structured-render';
const section = emptyStructuredRenderTableFallback({
table: createRenderDataTable(StructuredRenderCellDirection.Vertical, headers, []),
fallback: {
type: StructuredRenderSectionType.text,
text: 'No data available.',
},
sectionTitle: 'Results',
});
const list = {
type: StructuredRenderSectionType.list,
sectionTitle: 'Action Items',
items: [
{
content: {
type: StructuredRenderSectionType.text,
text: 'Increase test coverage.',
},
icon: {
type: StructuredRenderSectionType.icon,
iconKey: 'StatusWarning24Icon',
strokeColor: 'orange',
},
},
{
content: {
type: StructuredRenderSectionType.text,
text: 'Update documentation.',
},
},
],
};
List item content supports text, tag, or empty sections. Each item can have optional sources.
// Tag with a color variant (from Vira)
const tag = {
type: StructuredRenderSectionType.tag,
text: 'Active',
color: {variant: 'positive'},
};
// Tag with custom colors
const customTag = {
type: StructuredRenderSectionType.tag,
text: 'Critical',
color: {
custom: {
backgroundColor: '#e74c3c',
foregroundColor: '#ffffff',
},
},
};
// Big tag
const bigTag = {
type: StructuredRenderSectionType.tag,
text: 'Featured',
useBigTag: true,
};
Sources provide reference/citation metadata and can be attached to any section or list item via the sources field.
const sectionWithSources = {
type: StructuredRenderSectionType.text,
text: 'Patient requires follow-up.',
sources: [
{
type: StructuredRenderSectionType.source,
fileName: 'intake-report.pdf',
pageNumbers: [
3,
5,
],
quote: 'Follow-up recommended within 30 days.',
fileBoundingBoxes: [
{x1: 0.1, y1: 0.2, x2: 0.9, y2: 0.3},
],
},
],
};
const collapsible = {
type: StructuredRenderSectionType.collapsible,
header: 'Details',
content: [
{
type: StructuredRenderSectionType.text,
text: 'This content is hidden by default.',
},
{
type: StructuredRenderSectionType.list,
items: [
{
content: {
type: StructuredRenderSectionType.text,
text: 'Nested list inside collapsible.',
},
},
],
},
],
};
Cards group sections under an optional title and optional title icon.
import {type StructuredRenderData} from 'structured-render';
const data: StructuredRenderData = [
{
cardTitle: 'Project Health Report',
cardTitleIcon: {
type: StructuredRenderSectionType.icon,
iconKey: 'StatusSuccess24Icon',
strokeColor: 'green',
},
sections: [
{
type: StructuredRenderSectionType.text,
sectionTitle: 'Summary',
text: 'All systems operational.',
},
{
type: StructuredRenderSectionType.table,
sectionTitle: 'Module Metrics',
direction: 'horizontal',
headers: [
{key: 'module'},
{key: 'coverage'},
],
entries: [
{
data: {
module: {type: StructuredRenderSectionType.text, text: 'Core'},
coverage: {type: StructuredRenderSectionType.inlineCode, code: '94%'},
},
},
],
},
{
type: StructuredRenderSectionType.text,
sectionTitle: 'Next Review',
text: 'Scheduled for Q3 2026.',
style: StructuredRenderTextStyle.Faint,
},
],
},
];
VirStructuredRender)The easiest way to render structured data in a web app using element-vir.
import {html} from 'element-vir';
import {VirStructuredRender} from 'structured-render';
html`
<${VirStructuredRender.assign({
data,
options: {
useCardStyles: true,
expandAllCards: true,
isPhoneSize: false,
},
})}></${VirStructuredRender}>
`;
All options are optional and have sensible defaults.
| Option | Default | Description |
|---|---|---|
useCardStyles |
false |
Wrap cards in collapsible card components with borders |
expandAllCards |
false |
Start all cards expanded |
expandFirstCard |
false |
Start only the first card expanded |
blockCardExpansion |
false |
Expand all cards and disable toggling |
isPhoneSize |
false |
Use phone-optimized layout (tables become stacked cards) |
useDrawerForSources |
false |
Render sources inside a drawer instead of collapsible |
isTabletSize |
false |
Use tablet-optimized layout (VirStructuredRender only) |
expandSourcesOnPrint |
false |
Auto-expand sources when printing |
hideViewOnPageButtons |
false |
Hide "view on page" buttons in sources |
currentlyExpanded |
{} |
Track which sections/sources are expanded |
icons |
Vira's allIconsByName |
Override the icon set for iconKey lookups |
sourceIcon |
DocumentSearch24Icon |
Icon shown for source expansion |
processingIcon |
LoaderAnimated24Icon |
Icon shown next to processing text |
viewOnPageIcon |
EyeOpen24Icon |
Icon for "view on page" buttons |
markdownStyles |
Built-in styles | CSS for internal markdown rendering |
createViewOnPageString |
`View on page ${n}` |
Custom text for page navigation buttons |
The element emits SourceExpansionEvent when a source is expanded or collapsed. You can also provide custom icons by passing an icons map keyed by icon name.
VirStructuredRender exposes CSS variables for font size customization:
--vir-structured-render-h1-font-size (default: 24px)--vir-structured-render-h2-font-size (default: 18px)--vir-structured-render-h3-font-size (default: 16px)--vir-structured-render-small-font-size (default: 12px)VirExpandableSourceA standalone element for rendering content with an expandable source citation, without needing a full VirStructuredRender wrapper.
import {html} from 'element-vir';
import {VirExpandableSource, StructuredRenderSectionType} from 'structured-render';
html`
<${VirExpandableSource.assign({
sources: [
{
type: StructuredRenderSectionType.source,
fileName: 'report.pdf',
pageNumbers: [
1,
2,
],
quote: 'Relevant excerpt from the document.',
},
],
})}>
<span>Content that has a source citation.</span>
</${VirExpandableSource}>
`;
renderStructuredHtmlFor lower-level control, render directly to HTML templates without a web component:
import {renderStructuredHtml} from 'structured-render';
const htmlTemplate = renderStructuredHtml(data, {
useCardStyles: true,
expandAllCards: true,
});
import {renderStructuredMarkdown} from 'structured-render';
const markdown = renderStructuredMarkdown(data);
// Returns a markdown string with headings, tables, lists, etc.
// Node.js
import {renderToNodePdf} from 'structured-render';
const outputPath = await renderToNodePdf(data, {
saveLocationPath: '/tmp/report.pdf',
});
// Browser - download
import {renderToBrowserPdf} from 'structured-render';
await renderToBrowserPdf(data, {
fileName: 'report',
});
// Browser - print dialog
import {printPdf} from 'structured-render';
await printPdf(data, {
fileName: 'report',
});
// Node.js
import {renderToNodeImage} from 'structured-render';
const outputPath = await renderToNodeImage(data, {
saveLocationPath: '/tmp/report.png',
});
// Browser - download
import {renderToBrowserImage} from 'structured-render';
await renderToBrowserImage(data, {
fileName: 'report',
});
doesSectionHaveContentRecursively checks if a section has any meaningful content. Useful for conditionally rendering sections.
import {doesSectionHaveContent} from 'structured-render';
if (doesSectionHaveContent(section)) {
// render the section
}
createCleanSourcesNormalizes source input (single or array, with possible nulls) into a clean array or undefined.
import {createCleanSources} from 'structured-render';
const sources = createCleanSources(rawSources);
// Returns AtLeastTuple<StructuredRenderSource, 1> | undefined
sourceHasContentType guard that checks whether a source has any meaningful content (quote, file name, or page numbers).
VirMarkdownA web component that safely renders markdown as sanitized HTML using marked and DOMPurify.
import {html} from 'element-vir';
import {VirMarkdown} from 'structured-render';
html`
<${VirMarkdown.assign({
markdownString: '**Hello** world',
})}></${VirMarkdown}>
`;
All rendering functions accept RenderInput, which is flexible about what you pass in:
type RenderInput = MaybeArray<
StructuredRenderData | StructuredRenderCard | StructuredRenderSection | null | undefined
>;
You can pass a full StructuredRenderData array, a single card, a single section, or even an array mixing these. This makes it easy to render just a piece of structured data without wrapping it in the full hierarchy.