On this page

Variable basics

npm install @react-markdown-kit/variables
import Markdown from '@react-markdown-kit/renderer'
import { variables } from '@react-markdown-kit/variables'


<Markdown extensions={[variables({ data: { user: { name: 'Chatis' } } })]}>
  {'# Hello {{user.name}}'}
</Markdown>

@react-markdown-kit/variables is a plugin package, so there's no separate engine to call. variables({ data }) is an extension and the renderer runs it while parsing. Put it in an extensions prop for a single render, or in a preset if a whole surface is personalized. Outside React, compileMarkdown runs the same extension and returns the resolved MarkdownDocument:

import { compileMarkdown } from '@react-markdown-kit/renderer'
import { variables } from '@react-markdown-kit/variables'


const document = compileMarkdown(source, { extensions: [variables({ data })] })

The plugin doesn't use React, so this also works in a Node service, a worker, a CLI or an email job.

One document, many customers#

The authored source on the left stays the same and only the data changes.

Invoice header
Authored sourcenever changes
## Invoice for {{customer.name}}

Amount due: **{{amount | currency:"USD"}}** by {{dueDate | date:"long"}}.

Account manager: {{owner.name}}.
Resolved for Acmechanges

Invoice for Acme Industrial

Amount due: $4,250.00 by March 1, 2026.

Account manager: Dana Reyes.

Switching the dataset recompiles the same source with the new data. The authored source itself is never rewritten.

Options#

OptionPurpose
dataThe values placeholders resolve to
schemaA Standard Schema validator; rejected data is an error
locale, timeZoneFormatting locale (default en-US) and zone (default UTC)
formattersFormatters on top of the built-ins and those other extensions contribute
variablesPer-path metadata: required, default, label, group
fallbackMarkdown shown instead when resolution fails. Default: nothing
onDiagnosticsCalled with every diagnostic, on success and on failure

Failure renders nothing#

A missing required value, a rejected schema or an unsafe path is an error. On any error you get fallback (or an empty document if you didn't set one). The idea is you never ship a report with a blank where the account number should be.

<Markdown
  extensions={[
    variables({
      data,
      fallback: 'This report is temporarily unavailable.',
      onDiagnostics: (diagnostics) => logger.warn('invoice', { diagnostics }),
    }),
  ]}
>
  {source}
</Markdown>

With compileMarkdown the diagnostics are also on the document:

const document = compileMarkdown(source, { extensions: [variables({ data })] })
const failed = document.diagnostics.some((d) => d.severity === 'error')

You get diagnostics on success too. A warning, like a missing optional value, is reported without failing the resolution.

Diagnostics#

Every diagnostic has a stable code, a severity, a message and usually the data path it's about. Messages include the path but never the runtime value, so they're safe to log.

import { VARIABLE_DIAGNOSTIC_CODES } from '@react-markdown-kit/variables'


const missing = diagnostics.filter((item) => item.code === VARIABLE_DIAGNOSTIC_CODES.requiredValue)
CodeSeverityMeaning
VARIABLE_REQUIRED_VALUEerrorA required variable had no value in the data.
VARIABLE_OPTIONAL_VALUE_MISSINGwarningA variable declared optional had no value and resolved to empty text.
VARIABLE_VALUE_NOT_SCALARerrorThe value was an object, an array or a function.
VARIABLE_UNKNOWN_FORMATTERerrorNo formatter is registered under that name.
VARIABLE_UNSAFE_PATHerrorA path segment was __proto__, constructor or prototype.
VARIABLE_PARTIAL_URLerrorA placeholder filled part of a URL instead of all of it.
VARIABLE_SCHEMA_INVALIDerrorThe schema rejected the data.

The full list is exported as VARIABLE_DIAGNOSTIC_CODES. New codes may be added in a minor release, but existing ones won't change meaning.

Values become text, never structure#

Data is inserted into the parsed tree. It's never substituted into the source text and re-parsed, so any Markdown punctuation inside a value stays literal.

A value that tries to be bold
Authored sourcenever changes
## Hello {{user.name}}

Signed by {{user.name}}.
Resolved for Plain namechanges

Hello Chatis

Signed by Chatis.

The second dataset renders the asterisks as plain characters. A value can't create a heading, a table row, a link destination, an HTML tag or a code fence.

The security guide goes through the rules behind this, including literal code contexts, escaped delimiters and the newline boundary.

Which Markdown the document speaks#

The plugin doesn't have its own dialect. It resolves whatever tree the preset parsed, so a given source means the same thing everywhere in the kit. Tables and task lists come from the renderer's gfm(), same as for rendering:

import { gfmPreset } from '@react-markdown-kit/renderer/gfm'


<Markdown preset={gfmPreset} extensions={[variables({ data })]}>{source}</Markdown>

Other plugins hook in through the extension contract's variables capability. For example mermaid() marks its payload as literal, and any extension can add formatters.

Resolved Markdown text#

If you need Markdown text instead of a rendered document (for an email body or a file on disk, say), serialize the compiled document:

import { compileMarkdown, documentToMarkdown } from '@react-markdown-kit/renderer'


const document = compileMarkdown(source, { extensions: [variables({ data })] })
await writeFile('report.md', await documentToMarkdown(document))

Serializing re-escapes, so a value of **Administrator** comes back out as \*\*Administrator\*\* and still reads literally.

Finding placeholders without data#

variableChips() from the same package turns every placeholder into a variable node, so compiling with it and no data lets you inspect the placeholders:

import { variableChips, isVariableNode } from '@react-markdown-kit/variables'


const { tree } = compileMarkdown(source, { extensions: [variableChips()] })
// walk `tree` for nodes where isVariableNode(node): path, formatter, argument

No conditions or loops in v1#

There's no if, no each and no expression language for now. Do the branching in your application by picking the data or picking the source.

Next#

The overview, from placeholder syntax to schemas: Markdown variables.

Last updated on